← Back to Docs

Technical Security Overview

A deep dive into Cryptograph's security architecture

Architecture

Cryptograph is built on a fundamental principle: private spending keys never leave the Apple Watch. The iPhone acts as a display and network proxy; all signing operations occur on the watch.

┌─────────────────┐          ┌─────────────────┐
│   iPhone App    │◄────────►│  Apple Watch    │
│                 │ WCSession│                 │
│  • Display UI   │          │  • Key storage  │
│  • WalletConnect│          │  • Signing      │
│  • Network/API  │          │  • Mnemonic     │
│  • QR scanning  │          │  • PIN decrypt  │
└─────────────────┘          └─────────────────┘

The iPhone never sees private spending keys or mnemonics. The mnemonic is generated on the watch, stored in the watch Keychain (encrypted by the Secure Enclave), and signing requests are sent to the watch for approval.

For Zcash shielded transactions, the phone holds a Unified Full Viewing Key (UFVK) that allows it to read balances and transaction history. See Zcash Shielded Transactions for details.

Supported Chains

Bitcoin, Ethereum, Base, Solana, Zcash (including shielded transactions via Orchard and Sapling), Litecoin, Dogecoin, XRP, Tron, and EVM-compatible networks (Arbitrum, Optimism, Polygon, Avalanche, BSC).

Key Management

Key Derivation Paths

Standard BIP-32/BIP-44 derivation paths are used for each chain:

Chain Derivation Path Standard
Ethereum / EVM chains m/44'/60'/0'/0/0 BIP-44 (shared by Base, Arbitrum, Optimism, Polygon, Avalanche, BSC)
Bitcoin m/84'/0'/0'/0/0 BIP-84 (native SegWit)
Litecoin m/84'/2'/0'/0/0 BIP-84 (native SegWit)
Dogecoin m/44'/3'/0'/0/0 BIP-44
Solana m/44'/501'/0'/0/0 Phantom-compatible
XRP m/44'/144'/0'/0/0 BIP-44
Tron m/44'/195'/0'/0/0 BIP-44
Zcash (transparent) m/44'/133'/0'/0/0 BIP-44
Zcash (shielded) ZIP-32 UFVK Orchard + Sapling

Storage

The mnemonic is stored in the watchOS Keychain with kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly. This means:

Key Lifecycle

Derived private keys are never cached. Each signing operation follows this flow:

  1. Retrieve encrypted mnemonic from Keychain
  2. Decrypt using Secure Enclave key (see below)
  3. Derive the chain-specific private key
  4. Sign the transaction
  5. Zero all key material before deallocation

Only addresses are cached. The mnemonic and derived keys exist in memory only for the duration of the signing operation.

Secure Enclave Encryption

While the mnemonic is protected by Keychain access controls, we add an additional layer: encryption with a key that lives in the Secure Enclave and can never be exported.

Why ECDH?

The Secure Enclave only supports P-256 (not secp256k1 or Ed25519), so we use the SE for encryption at rest rather than direct signing. This provides hardware-backed protection for the mnemonic.

Storage Flow

STORE MNEMONIC:
  1. Generate ephemeral P-256 key pair
  2. Load SE P-256 key (created on first use, never exportable)
  3. ECDH: ephemeral_private + SE_public → shared_secret
  4. HKDF(shared_secret) → symmetric_key
  5. ChaCha20-Poly1305(mnemonic, symmetric_key) → ciphertext
  6. Store: version || ephemeral_public || nonce || ciphertext || tag

RETRIEVE MNEMONIC:
  1. Parse stored blob
  2. ECDH: SE_private + ephemeral_public → shared_secret
     (SE encryption key never leaves enclave)
  3. HKDF(shared_secret) → symmetric_key
  4. Decrypt ciphertext → mnemonic
  5. Return mnemonic in secure buffer (zeros on dealloc)

Security Properties

Property Implementation
SE key isolation P-256 key in Secure Enclave via SecKeyCreateRandomKey with kSecAttrTokenIDSecureEnclave — never exportable
Key access control kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly + .privateKeyUsage
Mnemonic zeroing Secure buffer with explicit zeroing on deallocation

Threat Model Impact

Attack Vector Without SE Encryption With SE Encryption
Keychain extraction (device unlocked) Plaintext mnemonic Encrypted blob, requires SE key
Keychain extraction (device locked) Blocked by access control Blocked by access control
Memory dump during signing Mnemonic in heap Secure buffer zeros on dealloc

Signing Authorization

Before any signature is produced, the watch enforces multiple independent checks in two phases: pre-flight checks when the request arrives, and authentication checks when the user approves.

Transaction Request Received
        │
        ▼
┌───────────────────────┐
│  1. Location Lock     │  ← Geofence check (if enabled); blocks if outside
│     (if enabled)      │     trusted zone and above Away Limit
└───────────────────────┘
        │ ✓
        ▼
┌───────────────────────┐
│  2. Time Lock         │  ← Configurable delay (1/3/7 days) + spend limits
│     + Spend Limit     │     Blocks if exceeds limit or pending setting change
└───────────────────────┘
        │ ✓
        ▼
   Display to User
        │
  User taps "Approve"
        │
        ▼
┌───────────────────────┐
│  3. Passcode Check    │  ← Keychain probe: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
│                       │     Blocks if no watch passcode configured
└───────────────────────┘
        │ ✓
        ▼
┌───────────────────────┐
│  4. On-Wrist Check    │  ← LAContext.deviceOwnerAuthenticationWithWristDetection
│                       │     Blocks if watch locked OR off-wrist
└───────────────────────┘
        │ ✓
        ▼
    Sign Transaction

Layer 1: Location Lock (Optional)

Checked when the signing request first arrives on the watch:

Reduces attack surface when traveling. Even if coerced away from home, attacker is limited to the Away Limit.

Layer 2: Time Lock

Checked when the signing request first arrives, before displaying to the user:

Layer 3: Passcode Enforcement

Checked when the user taps to approve. Attempts to store a test item in Keychain with kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly. If the device has no passcode, the operation fails and signing is blocked.

Without a passcode, an attacker with physical access to an unlocked watch could sign arbitrary transactions.

Layer 4: On-Wrist Authorization

Checked when the user taps to approve. Uses LAContext.evaluatePolicy(.deviceOwnerAuthenticationWithWristDetection). This blocks signing if:

On hardware where wrist detection is unavailable, this falls back to deviceOwnerAuthentication (passcode only).

This prevents "grab watch off nightstand" attacks. Even with physical possession, an attacker must know the passcode and put the watch on their wrist (which triggers a lock if it was on the victim's wrist).

Layer 3: Time Lock

Configurable anti-coercion protection:

Layer 4: Location Lock (Optional)

Geographic awareness for additional protection:

Reduces attack surface when traveling. Even if coerced away from home, attacker is limited to the Away Limit.

Security Properties

Property Guarantee
No signing without passcode Enforced by Keychain probe at approval time
No signing while locked Enforced by LAContext at approval time
No signing while off-wrist Enforced by wrist detection policy (falls back to passcode-only on unsupported hardware)
Time Lock enforced on-watch Phone cannot bypass; checked at request receipt
Location Lock enforced on-watch Geofence check on watch at receipt and approval
Setting changes require delay Cannot disable Time Lock or raise limits instantly

Recovery Encryption

Cryptograph offers two encrypted backup methods: a printed Recovery Sheet (encrypted QR code) and Photo Backup (encrypted data steganographically hidden in ordinary photos). In both cases, the phone never sees the mnemonic — it only handles opaque ciphertext.

BACKUP:
  Watch: mnemonic → encrypt(PIN or passphrase, PBKDF2 + ChaCha20-Poly1305) → ciphertext
  Watch → Phone: ciphertext only
  Phone: prints QR (Recovery Sheet) or embeds in photos (Photo Backup)

RECOVERY:
  Phone: scans QR or extracts from photos → forwards ciphertext to watch (never decrypts)
  Watch: prompts for PIN/passphrase → decrypts → validates BIP-39 → imports
  Watch: shows fingerprint for user verification (Recovery Sheet)

Cryptographic Stack

Component Algorithm
KDF PBKDF2-HMAC-SHA256 (1,000,000 iterations)
AEAD ChaCha20-Poly1305 (via CryptoKit)
Payload encoding CBOR, base64url, CGREC1: prefix
Salt 16 random bytes per encryption

Authentication Modes

Two credential modes are supported for recovery backup encryption:

Both modes use the same underlying PBKDF2 + ChaCha20-Poly1305 stack. The choice affects only the entropy of the input secret.

Security Features

KDF Hardening

We benchmarked offline cracking cost to validate the iteration count.

RTX 4090 PBKDF2-HMAC-SHA256 performance: ~8.87 MH/s at 1000 iterations, which translates to ~8,870 attempts/second at 1M iterations.

Credential Combinations Crack Time (1x RTX 4090)
6-digit PIN 106 ~2 minutes
8-digit PIN 108 ~3 hours
10-digit PIN 1010 ~13 days
8-char passphrase (random lowercase) 268 ~9 months
Important: A 6-digit PIN alone provides limited protection against offline attacks if an attacker obtains your Recovery Sheet or Photo Backup. Use a longer PIN or a passphrase for high-value wallets.

That said, we believe both recovery methods are more threat-resistant than industry alternatives. A plaintext seed phrase grants immediate access to funds. Some wallets store backups in iCloud with names like "WALLET BACKUP — DO NOT DELETE," which is directionally wrong: it tells an attacker exactly what to target.

A printed Recovery Sheet is encrypted — an attacker who finds it must still crack the PIN or passphrase. Photo Backup goes further: the encrypted data is steganographically hidden inside ordinary photos. An attacker with a compromised iCloud account would need to:

  1. Download every photo in the library
  2. Run steganalysis tools to identify which images contain embedded data
  3. Crack the PBKDF2 + ChaCha20-Poly1305 encryption — months or longer with a well-chosen passphrase

The encryption is the security boundary, not the steganography. But steganography means the backup doesn't advertise itself. Your photos look like photos.

Transaction Verification

A critical security property: the watch displays exactly what it signs. The phone cannot manipulate what appears on the watch screen.

Canonical Payload Parsing

The watch decodes the signing payload directly. All display data, spend limit values, and signing inputs are derived from the parsed payload — phone-supplied summaries are never trusted.

Method Display Source Signing Source
eth_sendTransaction / eth_signTransaction Decoded from protobuf Same payload
eth_signTypedData_v4 Parsed from JSON in payload Same payload
personal_sign Raw bytes from payload Same payload
eth_sign Raw 32-byte hash from payload Same payload
solana_signTransaction / solana_signAndSendTransaction Decoded via transaction parser Same payload
solana_signMessage Raw bytes from payload Same payload
xrp_sendTransaction Decoded via XRP transaction parser Same payload
Native sends (BTC, ETH, SOL, ZEC, LTC, DOGE, XRP) Same struct Same struct

Permit2 Detection

Permit2 (Uniswap's signature-based approval system) receives special handling:

Verified vs. Unverified Contracts

The watch checks contracts against an embedded registry that can only change with an app update:

When Time Lock is enabled, interactions with unverified contracts are blocked entirely until you wait through your delay.

Embedded Registry

129 contracts across Ethereum, Base, BSC, Solana, and NEAR:

Chain Contracts
Ethereum 71
Base 20
BSC 21
Solana 16
NEAR 1

Full registry with contract addresses: contract-registry.json

Zcash Shielded Transactions

Cryptograph supports both transparent and shielded Zcash transactions via our lightweight open-source no_std signing library, zcash-signer.

Transparent Transactions

Standard BIP-44 derivation on watch. The watch builds and signs the entire transaction, similar to Bitcoin.

Shielded Transactions (Orchard)

Shielded transactions use the PCZT (Partially Created Zcash Transaction) pattern:

1. Phone builds transaction structure via ZcashLightClientKit SDK
2. Phone sends unsigned PCZT to watch
3. Watch signs with RedPallas (Orchard) or RedJubjub (Sapling)
4. Watch returns signed PCZT (spending key never leaves watch)
5. Phone adds zero-knowledge proofs after signing
6. Phone broadcasts completed transaction

Key Separation

Zcash uses two distinct key types for shielded addresses:

This separation allows the phone to sync with the Zcash blockchain, decrypt incoming transactions, and display your shielded balance—all without ever having the ability to spend your funds.

Security Model

The critical property is maintained: the spending key never leaves the watch. The phone handles the computationally expensive proof generation, but cannot spend funds without the watch's signature.

Component Location
Authorizing Spend Key (ASK) Watch only (never leaves)
Unified Full Viewing Key (UFVK) Generated on watch, shared with phone
Transaction building Phone (ZcashLightClientKit)
RedPallas/RedJubjub signing Watch (via Rust FFI)
Zero-knowledge proofs Phone (after signing)
Blockchain sync & balance viewing Phone (using UFVK)

Dependencies

We rely on well-audited third-party cryptographic libraries. Where we've made modifications, our forks are public. Unmodified upstream dependencies are pinned to specific versions.

Library Purpose Source
WalletCore BIP-39/BIP-32 key derivation, BTC/EVM/SOL/LTC/DOGE/XRP/Tron signing perpetua-engineering/wallet-core (fork)
zcash-signer Minimal no_std Zcash signing for watchOS (ZIP-32, RedPallas, transparent) perpetua-engineering/zcash-signer (original)
zcash-swift-wallet-sdk iOS Zcash light client framework Electric-Coin-Company/zcash-swift-wallet-sdk (upstream, unmodified)
zcash-light-client-ffi Light client FFI layer for librustzcash Electric-Coin-Company/zcash-light-client-ffi (upstream, unmodified)
pczt PCZT crate with external signer support zcash/librustzcash (upstream, pinned rev)
orchard Zcash Orchard protocol implementation perpetua-engineering/orchard (fork, debug-tools feature); upstream v0.12 for PCZT signing
sapling-crypto Zcash Sapling cryptography Upstream crate v0.6 (via crates.io)
ReownWalletKit WalletConnect v2 protocol perpetua-engineering/reown-swift (fork)
CryptoKit ChaCha20-Poly1305, HMAC-SHA256, HKDF Apple (system framework)
Security.framework Secure Enclave key generation, ECDH, Keychain Apple (system framework)

Known Limitations


Install Integrity Check

The app verifies its own installation origin approximately once per day using StoreKit 2's AppTransaction.shared. Verification is entirely on-device — the OS validates the JWS-signed app transaction against Apple's root certificates locally. No network call, no server, no device identifiers transmitted.

The result is a verdict: official (App Store or TestFlight), not_official, or unknown. If not_official, a persistent warning advises the user to install only from the App Store or TestFlight. unknown triggers no user-facing warning — only a subtle status in settings. The check backs off from 6 hours to 24 hours on repeated unknowns, and caches known verdicts for 24 hours.

Limitations

This is informational only and does not gate any functionality. It cannot detect sophisticated runtime hooking or jailbreak environments. Client-side rate limiting is trivially bypassable by a modified binary, but the check's purpose is to warn unaware users of tampered installs, not to resist determined adversaries.


Security Improvements

Summary of security hardening since initial design:

Area Before After
Recovery backup Plaintext mnemonic Encrypted (ChaCha20-Poly1305); steganographic Photo Backup option
Recovery authentication None PIN or passphrase + PBKDF2 (1M iterations)
Mnemonic at rest Keychain only SE-encrypted + Keychain
Key provenance Unknown Serial proves on-device generation
Weak credentials Allowed Rejected (sequential/repeated PINs)
Passcode requirement None Keychain probe blocks signing
Wrist detection None LAContext blocks if off-wrist
Spend limits None Configurable with time delay
Approval gating None Warns on unverified; blocks when Time Lock active

Questions about our security architecture? Email security@perpetua.watch