Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sync encryption (crypto module)

The end-to-end encryption for cloud sync in crypto/, on libsodium: per-note keys wrapped under the root key, record envelopes, the QR enrollment wrap of the root key, the devices’ X25519 encryption keys, the recovery key, Ed25519 signing keys and the platform key stores. Design in sync-system-design.md. Decisions: D-27, D-45, D-47, D-51, D-55, D-64, D-66, D-74, D-75.

Tests are in crypto/src/commonTest and run on JVM, iOS simulator and an Android device or emulator (connectedAndroidDeviceTest; libsodium has no Android host build). Known answers come from an independent Python script: HChaCha20 checked against draft-irtf-cfrg-xchacha, ChaCha20-Poly1305, X25519 and Ed25519 from the cryptography package, BLAKE2b from hashlib, HKDF from hmac (checked against RFC 5869). Recovery words use the BIP-39 reference vectors (trezor/python-mnemonic) and signatures RFC 8032. The platform key stores have their own tests: crypto/src/androidDeviceTest on a device or emulator, and crypto/src/iosTest on a booted simulator (the test binary is linked with Keychain entitlements; see crypto/build.gradle.kts).

BehaviorShared
A wrapped note key matches a known answer, both waysNoteKeysTest.wrapMatchesKnownAnswer, NoteKeysTest.unwrapsKnownAnswer
A wrapped note key doesn’t open for another note, a changed byte or another root keyNoteKeysTest.otherNoteIsRejected, NoteKeysTest.everyFlippedByteIsRejected, NoteKeysTest.otherRootKeyWithSameIdIsRejected
Rotating the root key rewraps note keys and records still open, for every key generationNoteKeysTest.rotationRewrapsUnderTheNewRootKey, NoteKeysTest.rewrapKeepsEveryKeyGenerationReadable
After a note key rotation, new records use the new key and old ones keep the old; a device holding only the old key can’t read the new recordsNoteKeysTest.rotatedNoteKeySealsNewRecordsAndOldKeysStillOpenOldOnes
Keys must be 32 bytesNoteKeysTest.keysMustBe32Bytes
A record envelope matches a known answer; a known Unicode envelope opensRecordCipherTest.sealMatchesKnownAnswer, RecordCipherTest.opensKnownAnswerWithUnicode
Sealing round-trips, and two seals of the same text differRecordCipherTest.roundTripsWithRandomNonce, RecordCipherTest.emptyPlaintextRoundTrips
Any changed byte, record id, context or note key is rejected; so are truncated envelopesRecordCipherTest.everyFlippedByteIsRejected, RecordCipherTest.otherRecordIdContextOrKeyIsRejected, RecordCipherTest.truncatedEnvelopesAreRejected
An envelope names its note key; an unknown key id or the old version 1 is rejectedRecordCipherTest.envelopeNamesItsNoteKey, RecordCipherTest.oldVersionIsRejected
The enrollment public key and wrap match known answers, both waysDeviceEnrollmentTest.publicKeyMatchesKnownAnswer, DeviceEnrollmentTest.wrapMatchesKnownAnswer, DeviceEnrollmentTest.unwrapsKnownAnswer
A new device receives the root key and reads existing notesDeviceEnrollmentTest.newDeviceReadsExistingNotes
Another device, another group or any changed byte cannot unwrapDeviceEnrollmentTest.otherDeviceOrGroupCannotUnwrap, DeviceEnrollmentTest.everyFlippedByteIsRejected
Wrong sizes and a low-order public key are rejectedDeviceEnrollmentTest.wrongSizesAndLowOrderKeysAreRejected
A sealed device name matches a known answer, both ways; it names its root key (D-80)DeviceNamesTest.sealMatchesKnownAnswer, DeviceNamesTest.opensKnownAnswer
A name moved to another device or group, a changed byte or a root key the device never had does not open; a long name is trimmed and cut to 256 bytesDeviceNamesTest.aNameMovedToAnotherDeviceOrGroupDoesNotOpen, DeviceNamesTest.everyFlippedByteIsRejected, DeviceNamesTest.aRootKeyTheDeviceNeverHadCannotOpenIt, DeviceNamesTest.aLongNameIsTrimmedAndCutToFit

Device encryption key (D-74)

BehaviorShared
A key opens a root key sealed to its public keyDeviceEncryptionKeyTest.aKeyOpensWhatIsSealedToItsPublicKey
Only the 32-byte seed is stored; the key is rebuilt from itDeviceEncryptionKeyTest.theSeedIsAllThatIsStored, DeviceEncryptionKeyTest.aSeedOfTheWrongSizeIsRefused
It opens only for its own group and only with its own key; a value that is not a public key is refusedDeviceEncryptionKeyTest.itOpensOnlyForItsOwnGroupAndOnlyWithItsOwnKey, DeviceEncryptionKeyTest.aKeyThatIsNotAPublicKeyIsRefused

Recovery key (D-55, D-66, D-75)

BehaviorShared
HKDF-SHA-256 matches RFC 5869 test case 3RecoveryKeyTest.hkdfMatchesRfc5869TestCase3
24 words match the BIP-39 reference vectors, both waysRecoveryKeyTest.wordsMatchTheBip39Vectors, RecoveryKeyTest.theBip39VectorsReadBackToTheirEntropy
The lookup id, recovery signing key and recovery encryption key (seed and public key) match known answersRecoveryKeyTest.derivedValuesMatchKnownAnswers
Anyone with the public key can seal a root key for the server to keep, as a member does when it rotates; only the words open itRecoveryKeyTest.anyoneWithThePublicKeyCanSealARootKeyAndOnlyTheWordsOpenIt
A generated key round-trips through its wordsRecoveryKeyTest.aGeneratedKeyRoundTripsThroughItsWords
Typing ignores case, numbering and punctuation, and accepts four-letter prefixesRecoveryKeyTest.typingIsForgiving
A wrong word count, an unknown word (with its position) and a failed checksum are told apartRecoveryKeyTest.aWrongWordCountIsReported, RecoveryKeyTest.anUnknownWordIsReportedWithItsPosition, RecoveryKeyTest.aSwappedOrWrongWordFailsTheChecksum
The server’s copy opens only with its key, for its group, unchangedRecoveryKeyTest.theCopyOnlyOpensWithItsKeyForItsGroup
The recovery signing key signs a known answer that verifiesRecoveryKeyTest.theRecoverySigningKeyProvesTheKey

Signing keys and key storage (D-51)

BehaviorTest
Ed25519 matches RFC 8032 tests 1 and 2SigningKeyTest.matchesRfc8032Test1, SigningKeyTest.matchesRfc8032Test2
A key is rebuilt from its 32-byte seedSigningKeyTest.aKeyIsRebuiltFromItsSeed, SigningKeyTest.aSeedIs32Bytes
Verify refuses another message, signature, key or wrong sizes, without throwingSigningKeyTest.verifyRefusesAnythingElse
The vault keeps the root key with its id and the signing key’s seed, and clearsKeyVaultTest.keysRoundTripAndClear
A rotated root key becomes the newest and the old one is kept under its id; an older key is kept without becoming the newest; keys newer than the server’s can be dropped (D-75)KeyVaultTest.aRotatedRootKeyBecomesTheNewestAndTheOldOneIsKept, KeyVaultTest.anOlderKeyIsKeptWithoutBecomingTheNewest, KeyVaultTest.keysNewerThanTheServersCanBeDropped
The encryption key’s seed is kept, and clear forgets every key including the archived root keysKeyVaultTest.theEncryptionKeyRoundTripsAndClearForgetsEveryKey
Android Keystore: a secret round-trips, is replaced and deletedAndroidSecretStoreTest.aSecretRoundTripsAndIsReplacedAndDeleted
Android Keystore: the file is sealed, in noBackupFilesDir, and does not open under another nameAndroidSecretStoreTest.theFileOnDiskIsSealedAndOutOfBackups, AndroidSecretStoreTest.aFileRenamedToAnotherNameDoesNotOpen
Android Keystore and iOS Keychain: a vault’s keys survive a new store instanceAndroidSecretStoreTest.aKeyVaultKeepsTheKeysAcrossInstances, IosSecretStoreTest.aKeyVaultKeepsTheKeysAcrossInstances
iOS Keychain: a secret round-trips, is replaced and deleted; services are separateIosSecretStoreTest.aSecretRoundTripsAndIsReplacedAndDeleted, IosSecretStoreTest.servicesAreSeparate

Gaps

  • Not built: the desktop key store (D-51: macOS Keychain, Windows DPAPI, Linux Secret Service, passphrase fallback) and the optional keychain backup of D-57.
  • The Keychain item’s accessibility (AfterFirstUnlockThisDeviceOnly) is not asserted; the simulator does not enforce it.