mobile/native/darwin/Packages/EnteCrypto/CRYPTO_SPEC.md
This document specifies the cryptographic operations used in the Ente ecosystem, with particular focus on the tvOS/Swift implementation via the EnteCrypto package. All crypto operations use libsodium for consistency across platforms (Mobile, Web, CLI, tvOS).
Purpose: Convert user password into encryption keys
Algorithm: Argon2id
Implementation: EnteCrypto.deriveArgonKey()
// Parameters (configurable based on device capability)
memLimit: Int // Memory limit in bytes
opsLimit: Int // Time/CPU cost parameter
salt: String // Base64-encoded 16-byte salt
output: 32 bytes // keyEncryptionKey (KEK)
Cross-Platform Notes:
Purpose: Derive authentication key from KEK for SRP
Algorithm: libsodium KDF (crypto_kdf_derive_from_key)
Implementation: EnteCrypto.deriveLoginKey()
input: keyEncryptionKey (32 bytes)
kdf_id: 1
context: "loginctx" (8 bytes)
output: 16 bytes (first half of 32-byte derived key)
Purpose: Zero-knowledge password authentication
Implementation: SRPClient class
Parameters:
a, computes public key A = g^a mod NB, saltS using password-derived xSHA-256(S)Purpose: Generate keypair for cast device pairing
Algorithm: X25519 ECDH
Implementation: EnteCrypto.generateCastKeyPair()
// Uses CryptoKit for X25519 generation (compatible with libsodium)
privateKey: Curve25519.KeyAgreement.PrivateKey
publicKey: derived from privateKey
output: base64-encoded key pair
Purpose: Secure transmission of collection metadata from mobile to tvOS
Algorithm: X25519 + XSalsa20Poly1305 (NaCl sealed box)
Implementation: EnteCrypto.decryptCastPayload()
Mobile Client (Dart):
final encPayload = CryptoUtil.sealSync(
CryptoUtil.base642bin(base64Encode(payload.codeUnits)),
CryptoUtil.base642bin(publicKey)
);
tvOS Client (Swift):
let decryptedBytes = sodium.box.open(
anonymousCipherText: cipherText,
recipientPublicKey: publicKey,
recipientSecretKey: privateKey
)
Payload Structure:
{
"collectionID": 12345,
"castToken": "authentication-token",
"collectionKey": "base64-encoded-collection-key"
}
Purpose: Decrypt file-specific encryption keys
Algorithm: XSalsa20Poly1305 (libsodium secretbox)
Implementation: EnteCrypto.secretBoxOpen()
input: encryptedKey (base64) + nonce (base64) + collectionKey (32 bytes)
output: fileKey (32 bytes)
Purpose: Decrypt actual file data and metadata
Algorithm: XChaCha20Poly1305 (libsodium secretstream)
Implementation: EnteCrypto.decryptSecretStream()
Inputs:
fileKey: 32 bytes (random; derived via key hierarchy)
header: 24 bytes (emitted once by secretstream initPush; MUST be stored)
cipher: Concatenation of encrypted chunks (each chunk = plaintextChunk + 17 bytes overhead)
Output:
Plaintext file bytes (exact original size)
All full-size files use libsodium secretstream XChaCha20-Poly1305 with FIXED PLAINTEXT CHUNK SIZE:
Encryption steps (producer):
fileKey (32 random bytes) if not already present.state, header = initPush(fileKey); persist header alongside encrypted data record.MESSAGE for all but last; FINAL for last slice.cipherChunk = push(state, plaintextSlice, tag) (cipherChunk length = sliceLen + 17)cipherChunk to output blob.header (24B), concatenated cipher blob, original size metadata (optional but useful for validations), and hash (BLAKE2b) if required.Decryption steps (consumer):
fileKey, header, full cipher blob.state = initPull(header, fileKey).plaintext, tag = pull(state, cipherChunk); append plaintext.FINAL tag on the last chunk and no trailing bytes after a FINAL.decryptionFailed.Notes:
Purpose: Verify file content integrity
Algorithm: BLAKE2b (crypto_generichash)
Output Length: 64 bytes (512 bits)
Implementation: EnteCrypto.computeBlake2bHash()
input: file_content (Data)
process: sodium.genericHash.hash(message, outputLength: 64)
output: hex_string (128 characters)
Challenge: Server stores hashes as base64, client computes as hex
Solution: Dual-format comparison in EnteCrypto.verifyFileHash()
// 1. Try direct hex comparison
if computedHex == expectedHash { return true }
// 2. Try base64→hex conversion
if let base64Data = Data(base64Encoded: expectedHash) {
let expectedHex = base64Data.hexString
if computedHex == expectedHex { return true }
}
| Operation | Mobile (Dart) | Web (TypeScript) | CLI (Go) | tvOS (Swift) |
|---|---|---|---|---|
| Key Derivation | argon2-browser | argon2-browser | go-argon2 | swift-sodium |
| SRP Auth | custom | custom | custom | SRPClient |
| Cast Payload | sealSync() | boxSeal() | SealedBoxOpen() | box.open() |
| File Key | secretBox | secretBox | SecretBoxOpen() | secretBox.open() |
| File Content | decryptChaCha() | decryptStreamBytes() | DecryptFile() | secretStream.xchacha20poly1305 |
| Hash | blake2b (64B) | blake2b (64B) | blake2b (64B) | genericHash (64B) |
masterKey → collectionKey → fileKey → contentinvalidSalt: Base64 decoding or format errorsinvalidParameters: Wrong key/nonce lengths, malformed inputinvalidKeyLength: Key size validation failuresderivationFailed: Argon2id, KDF, or random generation failuresdecryptionFailed: Authentication tag verification failuresencryptionFailed: Encryption operation failuresv1.0 (August 2025): Initial implementation with working cast functionality
This specification ensures cryptographic consistency across all Ente client platforms while maintaining the security guarantees of the zero-knowledge architecture.