From e47ae684101981a7c20f3270994a61944059c825 Mon Sep 17 00:00:00 2001 From: cirakg Date: Sun, 18 Oct 2026 23:44:18 +0200 Subject: 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 --- source/crypto/Aes.mc | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 source/crypto/Aes.mc (limited to 'source/crypto/Aes.mc') 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; + } +} -- cgit v1.2.3