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/Aes.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/Aes.mc')
| -rw-r--r-- | source/crypto/Aes.mc | 72 |
1 files changed, 72 insertions, 0 deletions
diff --git a/source/crypto/Aes.mc b/source/crypto/Aes.mc new file mode 100644 index 0000000..e77cb86 --- /dev/null +++ b/source/crypto/Aes.mc @@ -0,0 +1,72 @@ +// Author: cirakg & claude ai +// Summary: AES-CBC encryption with PKCS#7 padding on top of the native Connect IQ cipher. +// +// The native Cipher offers AES-128 and AES-256 in ECB and CBC mode, without +// padding (CBC input must be a multiple of 16 bytes). This module adds PKCS#7 +// padding. AES-256 is used for records (decided in M2: it works in the +// simulator on all four test devices and costs nothing extra; the PIN, not +// the cipher, limits the security of locked entries). +// +// Security note: CBC alone does not detect tampering. Records are always +// sealed with an HMAC as well (SealedBox.mc), and the MAC is checked before +// anything is decrypted, so padding errors never reach an attacker. +// Verified with NIST SP 800-38A F.2.1 and F.2.5 (test/crypto/AesTest.mc). + +import Toybox.Cryptography; +import Toybox.Lang; + +module Aes { + + const BLOCK_BYTES = 16; + + // A CBC cipher for a 16- or 32-byte key and a 16-byte IV. + function cipher(key as ByteArray, iv as ByteArray) as Cryptography.Cipher { + if ((key.size() != 16 && key.size() != 32) || iv.size() != BLOCK_BYTES) { + throw new Lang.InvalidValueException("AES: key must be 16 or 32 bytes, IV 16 bytes"); + } + return new Cryptography.Cipher({ + :algorithm => key.size() == 32 ? Cryptography.CIPHER_AES256 : Cryptography.CIPHER_AES128, + :mode => Cryptography.MODE_CBC, + :key => key, + :iv => iv + }); + } + + // CBC without padding; data must be a multiple of 16 bytes. + function cbcRaw(key as ByteArray, iv as ByteArray, data as ByteArray, encrypt as Boolean) as ByteArray { + var c = cipher(key, iv); + return encrypt ? c.encrypt(data) : c.decrypt(data); + } + + // Encrypt with PKCS#7 padding: 1 to 16 bytes, each holding the pad length. + function cbcEncrypt(key as ByteArray, iv as ByteArray, plaintext as ByteArray) as ByteArray { + var padLength = BLOCK_BYTES - (plaintext.size() % BLOCK_BYTES); + var padded = Bytes.copy(plaintext); + for (var i = 0; i < padLength; i++) { + padded.add(padLength); + } + var out = cbcRaw(key, iv, padded, true); + Bytes.wipe(padded); + return out; + } + + // Decrypt and remove PKCS#7 padding; null when the length or the padding is + // wrong. + function cbcDecrypt(key as ByteArray, iv as ByteArray, ciphertext as ByteArray) as ByteArray? { + var size = ciphertext.size(); + if (size == 0 || size % BLOCK_BYTES != 0) { + return null; + } + var padded = cbcRaw(key, iv, ciphertext, false); + var padLength = padded[size - 1]; + var valid = padLength >= 1 && padLength <= BLOCK_BYTES; + if (valid) { + for (var i = size - padLength; i < size; i++) { + valid = valid && padded[i] == padLength; + } + } + var plaintext = valid ? padded.slice(0, size - padLength) : null; + Bytes.wipe(padded); + return plaintext; + } +} |
