← Home

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.

Why the Watch?

A HSM on 200-300 million wrists. To build Apple Pay for watchOS, Apple added their Secure Enclave TEE/HSM to what was otherwise a fitness device. This enables unique root-of-trust possibilities, separate but connected to a full smartphone. It's also attached to a device with a large, clear screen suitable for strong passphrase entry and detailed on-device transaction decoding.

No browser based exploits. The most common way to compromise an iPhone is through WebKit, the rendering engine behind Safari, in-app browsers, and web views. Every major iOS exploit chain in recent years has started with a WebKit vulnerability: visit a malicious page, and the attacker gains code execution.

Discretion and anonymity. Legacy hardware wallets' form factors are now well-known. Using one in public, or even carrying one on your person / in your luggage can be a risk to your safety in low-trust societies. The Apple Watch, with its huge worldwide sales volume, is a perfect way to secure your wallet hidden in plain sight. And unfortunately, hardware wallet vendors have proven to be poor stewards of our shipping addresses, with the market leaders both having had their CRMs compromised, some multiple times.

Can a Compromised Phone Install Malware on the Watch?

The natural question for a two-device architecture: if the phone is fully compromised, can the attacker push a malicious app to the watch? It is extremely difficult. Multiple independent gates stand between a compromised phone and code execution on the watch.

watchOS: Code Signing + Sealed System

Bundling gate. Watch apps are delivered as extensions bundled inside the iPhone app. Every binary in the bundle must be signed by a certificate Apple trusts. If malware modifies even one byte of the watch binary on the phone, the signature becomes invalid. Critically, it is the watch's own amfid (Apple Mobile File Integrity Daemon) that checks the hash when the binary arrives — not the phone's. Even if an attacker patches out amfid on a compromised phone, the watch's independent integrity check still rejects the tampered binary.

Provisioning gate. Even with a stolen Enterprise certificate, watchOS requires the user to physically navigate to Settings → General → VPN & Device Management and manually trust the developer. This cannot be triggered programmatically.

Independent boot chain. A kernel-level exploit on the phone could hook system daemons like installd in memory (as chains like Coruna demonstrate), potentially bypassing phone-side integrity checks for the duration of that session. But the watch boots and verifies its own OS independently via its own Secure Boot chain. Phone-side compromise does not propagate to the watch's kernel or system daemons.

Signature verification at every launch. Unlike platforms that only check signatures at install time, watchOS re-verifies code signatures against the Secure Enclave at every app launch. A binary that passes installation but is later tampered with will not run.

Wear OS: Separate Kernel, Separate Trust

On Wear OS, the phone and watch communicate over Bluetooth via Google Play Services. A compromised phone can transfer files to the watch, but cannot silently install them — the watch's Package Installer requires explicit user approval. Root access on the phone does not grant root on the watch; the Bluetooth protocol does not pass kernel-level permissions. An attacker would need a separate local privilege escalation exploit targeting the watch's own kernel.

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:

Wrist detection is enabled by default on watchOS. If a user disables it (Settings → Passcode → Wrist Detection), signing is blocked entirely until it is re-enabled.

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).

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; blocked entirely if wrist detection is disabled
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, which takes 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:

Solana Transaction Decoding & Threat Detection

Solana transactions are bundles of arbitrary program instructions. Unlike EVM, there's no single approve() pattern. A malicious transaction might appear as "Create Account" on a legacy hardware wallet while actually transferring token ownership to an attacker. Cryptograph decodes the wire-format bytes directly on the watch and runs multi-layer threat detection before displaying anything to the user.

Instruction-Level Decoding

The watch parses every instruction in a Solana transaction from the raw wire format (both legacy and versioned v0 messages). For known programs, each instruction is decoded to its specific type and parameters:

Program Decoded Instructions
System Program Transfer (amount in SOL), AdvanceNonceAccount (durable nonce detection)
SPL Token / Token-2022 Transfer, TransferChecked, Approve (delegate amount), SetAuthority (authority type + new authority address), CloseAccount
Associated Token Account CreateATA
Compute Budget SetComputeUnitLimit, SetComputeUnitPrice
Jupiter (v4, v5, v5.1, v6, DCA, Limit, Perps) Anchor IDL-decoded with named parameters (amounts, slippage, direction)
Orca, Raydium, Marinade Anchor IDL-decoded with named parameters

For DeFi programs that use Anchor, the watch decodes instruction parameters from bundled IDL schemas, showing the user actual amounts, slippage settings, and swap directions rather than opaque byte blobs. Instructions that can't be fully decoded are flagged.

Threat Detection

Six independent detectors run against every decoded Solana transaction:

Detector What It Catches Risk Level
Authority Transfer SPL Token SetAuthority changing account owner, mint authority, freeze authority, or close authority to an address other than the signer Critical
Delayed Execution Durable nonce (AdvanceNonceAccount as first instruction): Transaction never expires and can be executed at any future time. Hard-blocked at signing (see below) Blocked
Batch Drain 3+ transfers to the same non-signer address in a single transaction High / Critical
Unknown Program Interaction with a program not in the trusted registry. Transaction effects cannot be determined Medium (escalates to Critical)
Upgradeable Program RPC verification that a program can be modified by its upgrade authority. What you sign today may behave differently when executed Medium (escalates to Critical)
Simulation Failure Transaction fails RPC simulation. May indicate anti-simulation techniques or time-dependent exploits Medium / High

Compound Escalation

Individual threats are dangerous. Combinations are worse. When multiple risk signals appear in the same transaction (for example, an unknown program combined with an upgradeable program), the aggregate risk escalates to Critical. This catches TOCTOU (time-of-check/time-of-use) patterns where an attacker could change a program's behavior between when you sign and when the transaction executes.

Durable Nonce Blocking

Durable nonce transactions (AdvanceNonceAccount as the first instruction) allow signed transactions to be held indefinitely and executed at any arbitrary future time. They never expire. This is a key vector in real-world Solana drainer exploits: the victim signs what looks routine, and the attacker holds the signed transaction until conditions are favorable (e.g., after upgrading a program's behavior).

Cryptograph refuses to sign any Solana transaction that uses the durable nonce pattern. The transaction is rejected before reaching the approval screen, and the watch displays a non-dismissable explanation. This is a hard block, not a warning. Users cannot override it.

Why block instead of warn:

Address Table Lookup Resolution

Versioned Solana transactions (v0) can reference accounts via Address Lookup Tables, which hide the actual addresses being interacted with. The watch resolves these lookup tables via RPC before running threat detection, so account-index-based detectors see the same addresses the Solana runtime will use at execution time. Unresolved lookups are flagged.

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

Approval Revocation

Token approvals are a persistent attack surface. A single approve(spender, MAX_UINT256) grants indefinite access to your tokens, and most dApps request exactly that. Cryptograph lets you audit and revoke these approvals directly from your phone, with the watch signing the revocation transaction.

Discovery

The app queries on-chain approval state for each EVM chain (Ethereum, Base, BSC, Arbitrum, Optimism, Polygon, Avalanche). Both standard ERC-20 Approval events and Permit2 Permit events are indexed. Each approval is classified by risk:

Risk Level Criteria
Critical Unlimited amount + unverified spender
High Unlimited amount + verified spender
Medium Limited amount + unverified spender
Low Limited amount + verified spender

Revocation Flow

Revoking an approval is an on-chain transaction: it requires gas and a signature from the watch. The phone builds the unsigned transaction; the watch displays what it's signing and requires a 1.5-second press-and-hold to approve.

Phone                                    Watch
  │                                        │
  │  1. Build unsigned revoke TX           │
  │     (estimate gas, encode calldata)    │
  │                                        │
  │  2. Send to watch ──────────────────►  │
  │                                        │  3. Display: token, spender,
  │                                        │     amount → 0, network fee
  │                                        │
  │                                        │  4. User holds to approve
  │                                        │     (1.5s press-and-hold)
  │                                        │
  │  5. Receive signed TX  ◄────────────── │
  │     Broadcast to network               │
  │                                        │
  │  6. Refresh approval list              │

Two revocation methods, depending on the approval type:

Type On-Chain Call Target Contract
ERC-20 approve(spender, 0) The token contract
Permit2 approve(token, spender, 0, 0) Canonical Permit2 contract (0x000...22D473, same on all EVM chains)

Both methods emit standard events that block explorers and tools like revoke.cash recognize. The phone never constructs or modifies the transaction after the watch signs it. It broadcasts the signed bytes as-is.

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

Read the source: Open Source

Available on the App Store