Follow-up from PR #290 review.
Current state
apps/vps/src/services/crypto.service.ts derives the AES-256-GCM key by hashing the configured root key with a single SHA-256 pass:
createHash("sha256").update(rootKey, "utf8").digest()
Encryption itself is fine: AES-256-GCM with a fresh 12-byte random IV per message and the auth tag persisted in the envelope. The weak part is only the key derivation.
Why it should change
SHA-256 is a fast hash, not a key derivation function. It has no salt and no work factor, so if the root key is ever low-entropy (an operator-typed secret rather than 32 random bytes), the derived key is cheap to brute force offline given a stolen ciphertext. A proper KDF, HKDF-SHA256 with a salt and context info, or scrypt/argon2 if we want a work factor, removes that.
This is currently low severity: the only secrets stored are the maintainers own Bluesky app passwords, and the root key comes from config rather than user input. It becomes real if external accounts ever hold third-party user credentials.
Migration path is already open
The envelope carries a keyId field, currently the literal "sha256":
{ keyId: "sha256", iv, authTag, payload }
So this can be rolled forward without a destructive migration:
- Add an HKDF derivation path writing
keyId: "hkdf-v1"
- Keep the
sha256 branch for decryption only
- Re-encrypt existing rows on next successful sync, or with a one-off script
- Drop the
sha256 branch once no rows reference it
Not doing now
Not switching to effect/Crypto or @effect/platform. @effect/platform@0.96.2 ships no crypto module, and effect/Crypto in 4.0.0-beta.99 exposes only randomBytes, digest, UUIDs, and random numbers. There is no AES-GCM cipher, so createCipheriv/createDecipheriv stay on node:crypto regardless. effect/Crypto could supply randomBytes and digest here, but mixing sources for one primitive is not worth the extra service dependency.
Follow-up from PR #290 review.
Current state
apps/vps/src/services/crypto.service.tsderives the AES-256-GCM key by hashing the configured root key with a single SHA-256 pass:Encryption itself is fine: AES-256-GCM with a fresh 12-byte random IV per message and the auth tag persisted in the envelope. The weak part is only the key derivation.
Why it should change
SHA-256 is a fast hash, not a key derivation function. It has no salt and no work factor, so if the root key is ever low-entropy (an operator-typed secret rather than 32 random bytes), the derived key is cheap to brute force offline given a stolen ciphertext. A proper KDF, HKDF-SHA256 with a salt and context info, or scrypt/argon2 if we want a work factor, removes that.
This is currently low severity: the only secrets stored are the maintainers own Bluesky app passwords, and the root key comes from config rather than user input. It becomes real if external accounts ever hold third-party user credentials.
Migration path is already open
The envelope carries a
keyIdfield, currently the literal"sha256":So this can be rolled forward without a destructive migration:
keyId: "hkdf-v1"sha256branch for decryption onlysha256branch once no rows reference itNot doing now
Not switching to
effect/Cryptoor@effect/platform.@effect/platform@0.96.2ships no crypto module, andeffect/Cryptoin4.0.0-beta.99exposes onlyrandomBytes,digest, UUIDs, and random numbers. There is no AES-GCM cipher, socreateCipheriv/createDecipherivstay onnode:cryptoregardless.effect/Cryptocould supplyrandomBytesanddigesthere, but mixing sources for one primitive is not worth the extra service dependency.