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).
Behavior Shared
A wrapped note key matches a known answer, both ways NoteKeysTest.wrapMatchesKnownAnswer, NoteKeysTest.unwrapsKnownAnswer
A wrapped note key doesn’t open for another note, a changed byte or another root key NoteKeysTest.otherNoteIsRejected, NoteKeysTest.everyFlippedByteIsRejected, NoteKeysTest.otherRootKeyWithSameIdIsRejected
Rotating the root key rewraps note keys and records still open, for every key generation NoteKeysTest.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 records NoteKeysTest.rotatedNoteKeySealsNewRecordsAndOldKeysStillOpenOldOnes
Keys must be 32 bytes NoteKeysTest.keysMustBe32Bytes
A record envelope matches a known answer; a known Unicode envelope opens RecordCipherTest.sealMatchesKnownAnswer, RecordCipherTest.opensKnownAnswerWithUnicode
Sealing round-trips, and two seals of the same text differ RecordCipherTest.roundTripsWithRandomNonce, RecordCipherTest.emptyPlaintextRoundTrips
Any changed byte, record id, context or note key is rejected; so are truncated envelopes RecordCipherTest.everyFlippedByteIsRejected, RecordCipherTest.otherRecordIdContextOrKeyIsRejected, RecordCipherTest.truncatedEnvelopesAreRejected
An envelope names its note key; an unknown key id or the old version 1 is rejected RecordCipherTest.envelopeNamesItsNoteKey, RecordCipherTest.oldVersionIsRejected
The enrollment public key and wrap match known answers, both ways DeviceEnrollmentTest.publicKeyMatchesKnownAnswer, DeviceEnrollmentTest.wrapMatchesKnownAnswer, DeviceEnrollmentTest.unwrapsKnownAnswer
A new device receives the root key and reads existing notes DeviceEnrollmentTest.newDeviceReadsExistingNotes
Another device, another group or any changed byte cannot unwrap DeviceEnrollmentTest.otherDeviceOrGroupCannotUnwrap, DeviceEnrollmentTest.everyFlippedByteIsRejected
Wrong sizes and a low-order public key are rejected DeviceEnrollmentTest.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 bytes DeviceNamesTest.aNameMovedToAnotherDeviceOrGroupDoesNotOpen, DeviceNamesTest.everyFlippedByteIsRejected, DeviceNamesTest.aRootKeyTheDeviceNeverHadCannotOpenIt, DeviceNamesTest.aLongNameIsTrimmedAndCutToFit
Behavior Shared
A key opens a root key sealed to its public key DeviceEncryptionKeyTest.aKeyOpensWhatIsSealedToItsPublicKey
Only the 32-byte seed is stored; the key is rebuilt from it DeviceEncryptionKeyTest.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 refused DeviceEncryptionKeyTest.itOpensOnlyForItsOwnGroupAndOnlyWithItsOwnKey, DeviceEncryptionKeyTest.aKeyThatIsNotAPublicKeyIsRefused
Behavior Shared
HKDF-SHA-256 matches RFC 5869 test case 3 RecoveryKeyTest.hkdfMatchesRfc5869TestCase3
24 words match the BIP-39 reference vectors, both ways RecoveryKeyTest.wordsMatchTheBip39Vectors, RecoveryKeyTest.theBip39VectorsReadBackToTheirEntropy
The lookup id, recovery signing key and recovery encryption key (seed and public key) match known answers RecoveryKeyTest.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 it RecoveryKeyTest.anyoneWithThePublicKeyCanSealARootKeyAndOnlyTheWordsOpenIt
A generated key round-trips through its words RecoveryKeyTest.aGeneratedKeyRoundTripsThroughItsWords
Typing ignores case, numbering and punctuation, and accepts four-letter prefixes RecoveryKeyTest.typingIsForgiving
A wrong word count, an unknown word (with its position) and a failed checksum are told apart RecoveryKeyTest.aWrongWordCountIsReported, RecoveryKeyTest.anUnknownWordIsReportedWithItsPosition, RecoveryKeyTest.aSwappedOrWrongWordFailsTheChecksum
The server’s copy opens only with its key, for its group, unchanged RecoveryKeyTest.theCopyOnlyOpensWithItsKeyForItsGroup
The recovery signing key signs a known answer that verifies RecoveryKeyTest.theRecoverySigningKeyProvesTheKey
Behavior Test
Ed25519 matches RFC 8032 tests 1 and 2 SigningKeyTest.matchesRfc8032Test1, SigningKeyTest.matchesRfc8032Test2
A key is rebuilt from its 32-byte seed SigningKeyTest.aKeyIsRebuiltFromItsSeed, SigningKeyTest.aSeedIs32Bytes
Verify refuses another message, signature, key or wrong sizes, without throwing SigningKeyTest.verifyRefusesAnythingElse
The vault keeps the root key with its id and the signing key’s seed, and clears KeyVaultTest.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 keys KeyVaultTest.theEncryptionKeyRoundTripsAndClearForgetsEveryKey
Android Keystore: a secret round-trips, is replaced and deleted AndroidSecretStoreTest.aSecretRoundTripsAndIsReplacedAndDeleted
Android Keystore: the file is sealed, in noBackupFilesDir, and does not open under another name AndroidSecretStoreTest.theFileOnDiskIsSealedAndOutOfBackups, AndroidSecretStoreTest.aFileRenamedToAnotherNameDoesNotOpen
Android Keystore and iOS Keychain: a vault’s keys survive a new store instance AndroidSecretStoreTest.aKeyVaultKeepsTheKeysAcrossInstances, IosSecretStoreTest.aKeyVaultKeepsTheKeysAcrossInstances
iOS Keychain: a secret round-trips, is replaced and deleted; services are separate IosSecretStoreTest.aSecretRoundTripsAndIsReplacedAndDeleted, IosSecretStoreTest.servicesAreSeparate
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.