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/Kdf.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/Kdf.mc')
| -rw-r--r-- | source/crypto/Kdf.mc | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/source/crypto/Kdf.mc b/source/crypto/Kdf.mc new file mode 100644 index 0000000..07c97ba --- /dev/null +++ b/source/crypto/Kdf.mc @@ -0,0 +1,159 @@ +// Author: cirakg & claude ai +// Summary: Key derivation: resumable PBKDF2-HMAC-SHA256 (RFC 8018) with calibration, and HKDF-SHA256 (RFC 5869). +// +// PBKDF2 turns the PIN into a key (README 7.2). It runs as a Jobs.Job, a +// bounded number of iterations per step, because the iteration count is +// calibrated to about one second on the watch, far beyond one event's +// watchdog budget. It uses the native HMAC-SHA256: the object keeps the key +// and digest() resets it, so each iteration is one native call. +// +// HKDF splits one master key into separate encryption and MAC keys +// (README 7.3). +// +// Verified with RFC 7914 section 11 (PBKDF2) and RFC 5869 A.1-A.3 (HKDF), +// plus seeded vectors from Python's hashlib (test/crypto/KdfTest.mc). + +import Toybox.Cryptography; +import Toybox.Lang; +import Toybox.System; + +module Kdf { + + const SHA256_BYTES = 32; + // Bounds for the calibrated PBKDF2 iteration count. The lower bound keeps + // a fast watch from choosing a trivially cheap PIN key; the upper bound + // keeps the simulator (a PC) from choosing a count a watch can't afford. + const MIN_ITERATIONS = 2000; + const MAX_ITERATIONS = 200000; + // Iterations per job step: small enough for the 120,000-bytecode watchdog. + const ITERATIONS_PER_STEP = 200; + + // PBKDF2-HMAC-SHA256 as a resumable job (RFC 8018 section 5.2). + class Pbkdf2 extends Jobs.Job { + + private var _mac as Cryptography.HashBasedMessageAuthenticationCode; + private var _salt as ByteArray; + private var _iterations as Number; + private var _length as Number; + private var _blockCount as Number; + private var _block as Number = 1; // current output block, 1-based + private var _done as Number = 0; // iterations done for the current block + private var _u as ByteArray = []b; + private var _t as ByteArray = []b; + private var _out as ByteArray = []b; + + // iterations >= 1; 1 <= length <= 255 * 32 bytes. + function initialize(password as ByteArray, salt as ByteArray, iterations as Number, length as Number) { + Job.initialize(); + if (iterations < 1 || length < 1 || length > 255 * SHA256_BYTES) { + throw new Lang.InvalidValueException("PBKDF2: bad iteration count or key length"); + } + _mac = new Cryptography.HashBasedMessageAuthenticationCode({ + :algorithm => Cryptography.HASH_SHA256, + :key => password + }); + _salt = Bytes.copy(salt); + _iterations = iterations; + _length = length; + _blockCount = (length + SHA256_BYTES - 1) / SHA256_BYTES; + } + + // Run up to `budget` iterations. Returns true when the key is complete. + function step(budget as Number) as Boolean { + var left = budget; + while (left > 0 && _block <= _blockCount) { + if (_done == 0) { + // U1 = HMAC(P, S || INT(i)) + _mac.update(Bytes.concat(_salt, Bytes.uint32BigEndian(_block))); + _u = _mac.digest(); + _t = Bytes.copy(_u); + } else { + // Uj = HMAC(P, Uj-1); T ^= Uj. The previous Uj isn't wiped + // every iteration: it is useless without the password, and + // a wipe loop per iteration would halve the iterations the + // watch can afford in its time budget. The final values + // are wiped below. + _mac.update(_u); + _u = _mac.digest(); + var t = _t; + var u = _u; + for (var i = 0; i < SHA256_BYTES; i++) { + t[i] = t[i] ^ u[i]; + } + } + _done++; + left--; + if (_done == _iterations) { + _out.addAll(_t); + Bytes.wipe(_t); + Bytes.wipe(_u); + _block++; + _done = 0; + } + } + return _block > _blockCount; + } + + // The derived key; only valid after step() returned true. + function result() as ByteArray { + return _out.slice(0, _length); + } + + // Wipe every intermediate value. + function wipe() as Void { + Bytes.wipe(_u); + Bytes.wipe(_t); + Bytes.wipe(_out); + Bytes.wipe(_salt); + } + } + + // PBKDF2-HMAC-SHA256 in one call. Only for tests and small iteration counts. + function pbkdf2(password as ByteArray, salt as ByteArray, iterations as Number, length as Number) as ByteArray { + var job = new Pbkdf2(password, salt, iterations, length); + Jobs.runToCompletion(job, ITERATIONS_PER_STEP); + var key = job.result(); + job.wipe(); + return key; + } + + // Iteration count for which one derivation takes about targetMs on this + // device, clamped to [MIN_ITERATIONS, MAX_ITERATIONS]. Measures a short + // run of `sample` iterations (call it again from a job if a longer sample + // would trip the watchdog). + function calibrate(targetMs as Number, sample as Number) as Number { + var job = new Pbkdf2([0, 0, 0, 0, 0, 0]b, new [16]b, sample, SHA256_BYTES); + var start = System.getTimer(); + Jobs.runToCompletion(job, ITERATIONS_PER_STEP); + var elapsed = System.getTimer() - start; + job.wipe(); + var estimate = elapsed > 0 ? (sample.toLong() * targetMs / elapsed) : MAX_ITERATIONS.toLong(); + if (estimate < MIN_ITERATIONS) { + return MIN_ITERATIONS; + } + return estimate > MAX_ITERATIONS ? MAX_ITERATIONS : estimate.toNumber(); + } + + // HKDF-SHA256 (RFC 5869): extract with salt (empty means 32 zero bytes), + // then expand to `length` bytes with info. length <= 255 * 32. + function hkdf(ikm as ByteArray, salt as ByteArray, info as ByteArray, length as Number) as ByteArray { + if (length < 1 || length > 255 * SHA256_BYTES) { + throw new Lang.InvalidValueException("HKDF: bad key length"); + } + var prk = Hmac.nativeSha256(salt.size() > 0 ? salt : new [SHA256_BYTES]b, ikm); + var okm = new [0]b; + var block = new [0]b; + for (var counter = 1; okm.size() < length; counter++) { + var input = Bytes.concat(block, info); + input.add(counter); + Bytes.wipe(block); + block = Hmac.nativeSha256(prk, input); + okm.addAll(block); + } + Bytes.wipe(prk); + Bytes.wipe(block); + var key = okm.slice(0, length); + Bytes.wipe(okm); + return key; + } +} |
