diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-18 23:44:18 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-18 23:44:18 +0200 |
| commit | e47ae684101981a7c20f3270994a61944059c825 (patch) | |
| tree | c2baab8baafa1be446dad7f224f1a1cf68493825 /source/crypto/SealedBox.mc | |
| parent | b70cde45b67e1f6bb65d598d25c1a5184bdab520 (diff) | |
feat(crypto): add SHA-512, HMAC, PBKDF2, HKDF, AES-256 records and self-test
- Sha512: FIPS 180-4 in Monkey C (no native SHA-512), rotations inlined
with precomputed masks; constants derived from the primes, not typed.
- Hmac: RFC 2104 over SHA-1/256/512 (native HMAC only supports SHA-256).
- Kdf: resumable PBKDF2-HMAC-SHA256 job with calibration bounds
(2,000-200,000 iterations) and HKDF-SHA256.
- Aes: CBC with PKCS#7 over the native cipher; AES-256 chosen.
- SealedBox: encrypt-then-MAC records (HKDF-split keys, length-prefixed
aad, constant-time tag check before decryption).
- SelfTest: known-answer tests for every primitive at start-up.
- Bytes (wipe, constant-time equal), Clock (2038-safe widening, trusted
clock), Jobs (bounded steps against the watchdog, timer runner).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/crypto/SealedBox.mc')
| -rw-r--r-- | source/crypto/SealedBox.mc | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/source/crypto/SealedBox.mc b/source/crypto/SealedBox.mc new file mode 100644 index 0000000..0804b96 --- /dev/null +++ b/source/crypto/SealedBox.mc @@ -0,0 +1,89 @@ +// Author: cirakg & claude ai +// Summary: Authenticated encryption of records: AES-256-CBC plus HMAC-SHA256, encrypt-then-MAC (README 7.3). +// +// Format of a sealed box (one ByteArray): +// version (1 byte, = 1) | IV (16 bytes) | AES-256-CBC ciphertext | tag (32 bytes) +// tag = HMAC-SHA256(macKey, "OTPW-REC-1" || len(aad) as 4 bytes || aad || version || IV || ciphertext) +// +// * Two keys come from one master key with HKDF (labels "OTPW enc v1" and +// "OTPW mac v1"), so encryption and authentication never share a key. +// * aad ("additional authenticated data") is authenticated but not encrypted; +// storage puts the record ID and header there (M3), so a record can't be +// swapped for another one or have its header changed unnoticed. Its length +// is part of the MAC input, so aad and ciphertext can't be shifted against +// each other. +// * open() checks the tag in constant time BEFORE decrypting, and returns +// null for any problem (wrong key, tampering, truncation, unknown version) +// without saying which. +// * Every seal uses a fresh random IV from Cryptography.randomBytes. + +import Toybox.Cryptography; +import Toybox.Lang; + +module SealedBox { + + const VERSION = 1; + const IV_BYTES = 16; + const TAG_BYTES = 32; + const KEY_BYTES = 32; + const MIN_SIZE = 1 + IV_BYTES + 16 + TAG_BYTES; // ciphertext is at least one block + + // The encryption and MAC keys derived from one master key. + class Keys { + var encKey as ByteArray; + var macKey as ByteArray; + + function initialize(masterKey as ByteArray) { + encKey = Kdf.hkdf(masterKey, []b, Bytes.fromUtf8("OTPW enc v1"), KEY_BYTES); + macKey = Kdf.hkdf(masterKey, []b, Bytes.fromUtf8("OTPW mac v1"), KEY_BYTES); + } + + // Wipe both keys (when the session locks). + function wipe() as Void { + Bytes.wipe(encKey); + Bytes.wipe(macKey); + } + } + + // Seal plaintext with a fresh random IV. + function seal(keys as Keys, aad as ByteArray, plaintext as ByteArray) as ByteArray { + return sealWithIv(keys, aad, plaintext, Cryptography.randomBytes(IV_BYTES)); + } + + // Seal with a given IV. Only tests call this directly (fixed IVs make the + // output reproducible); reusing an IV in real use would be a mistake. + function sealWithIv(keys as Keys, aad as ByteArray, plaintext as ByteArray, iv as ByteArray) as ByteArray { + var ciphertext = Aes.cbcEncrypt(keys.encKey, iv, plaintext); + var box = [VERSION]b; + box.addAll(iv); + box.addAll(ciphertext); + box.addAll(tag(keys, aad, box)); + return box; + } + + // The plaintext, or null if the box is malformed, was sealed with another + // key or aad, or was changed in any way. + function open(keys as Keys, aad as ByteArray, box as ByteArray) as ByteArray? { + var size = box.size(); + if (size < MIN_SIZE || (size - 1 - IV_BYTES - TAG_BYTES) % Aes.BLOCK_BYTES != 0 || box[0] != VERSION) { + return null; + } + var body = box.slice(0, size - TAG_BYTES); + var expected = tag(keys, aad, body); + var valid = Bytes.equal(expected, box.slice(size - TAG_BYTES, size)); + Bytes.wipe(expected); + if (!valid) { + return null; + } + return Aes.cbcDecrypt(keys.encKey, box.slice(1, 1 + IV_BYTES), box.slice(1 + IV_BYTES, size - TAG_BYTES)); + } + + // HMAC over the label, the aad (with its length) and the box body. + function tag(keys as Keys, aad as ByteArray, body as ByteArray) as ByteArray { + var input = Bytes.fromUtf8("OTPW-REC-1"); + input.addAll(Bytes.uint32BigEndian(aad.size())); + input.addAll(aad); + input.addAll(body); + return Hmac.nativeSha256(keys.macKey, input); + } +} |
