summaryrefslogtreecommitdiff
path: root/source/crypto/Sha512.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-18 23:44:18 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-18 23:44:18 +0200
commite47ae684101981a7c20f3270994a61944059c825 (patch)
treec2baab8baafa1be446dad7f224f1a1cf68493825 /source/crypto/Sha512.mc
parentb70cde45b67e1f6bb65d598d25c1a5184bdab520 (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/Sha512.mc')
-rw-r--r--source/crypto/Sha512.mc182
1 files changed, 182 insertions, 0 deletions
diff --git a/source/crypto/Sha512.mc b/source/crypto/Sha512.mc
new file mode 100644
index 0000000..4248462
--- /dev/null
+++ b/source/crypto/Sha512.mc
@@ -0,0 +1,182 @@
+// Author: cirakg & claude ai
+// Summary: SHA-512 in pure Monkey C (FIPS 180-4), because Connect IQ has no native SHA-512.
+//
+// Only SHA-512 OTP entries use this (most services use SHA-1). It works on
+// 64-bit Long values: Monkey C Longs wrap around on overflow like unsigned
+// arithmetic modulo 2^64, but `>>` copies the sign bit, so every right shift
+// masks off the copied bits (see the Mn constants).
+//
+// Verified against the FIPS 180-4 example messages and 15 seeded messages
+// around the 128-byte padding boundary (test/crypto/HashTest.mc).
+//
+// Performance note: one 128-byte block is roughly ten thousand VM operations,
+// and an OTP code needs four blocks (HMAC). The cost is measured in M2's
+// benchmark tests; long inputs must be split into steps by the caller so the
+// watchdog never trips.
+
+import Toybox.Lang;
+
+module Sha512 {
+
+ // Round constants: the first 64 bits of the fractional parts of the cube
+ // roots of the first 80 primes (FIPS 180-4 section 4.2.3).
+ const K = [
+ 0x428a2f98d728ae22l, 0x7137449123ef65cdl, 0xb5c0fbcfec4d3b2fl,
+ 0xe9b5dba58189dbbcl, 0x3956c25bf348b538l, 0x59f111f1b605d019l,
+ 0x923f82a4af194f9bl, 0xab1c5ed5da6d8118l, 0xd807aa98a3030242l,
+ 0x12835b0145706fbel, 0x243185be4ee4b28cl, 0x550c7dc3d5ffb4e2l,
+ 0x72be5d74f27b896fl, 0x80deb1fe3b1696b1l, 0x9bdc06a725c71235l,
+ 0xc19bf174cf692694l, 0xe49b69c19ef14ad2l, 0xefbe4786384f25e3l,
+ 0x0fc19dc68b8cd5b5l, 0x240ca1cc77ac9c65l, 0x2de92c6f592b0275l,
+ 0x4a7484aa6ea6e483l, 0x5cb0a9dcbd41fbd4l, 0x76f988da831153b5l,
+ 0x983e5152ee66dfabl, 0xa831c66d2db43210l, 0xb00327c898fb213fl,
+ 0xbf597fc7beef0ee4l, 0xc6e00bf33da88fc2l, 0xd5a79147930aa725l,
+ 0x06ca6351e003826fl, 0x142929670a0e6e70l, 0x27b70a8546d22ffcl,
+ 0x2e1b21385c26c926l, 0x4d2c6dfc5ac42aedl, 0x53380d139d95b3dfl,
+ 0x650a73548baf63del, 0x766a0abb3c77b2a8l, 0x81c2c92e47edaee6l,
+ 0x92722c851482353bl, 0xa2bfe8a14cf10364l, 0xa81a664bbc423001l,
+ 0xc24b8b70d0f89791l, 0xc76c51a30654be30l, 0xd192e819d6ef5218l,
+ 0xd69906245565a910l, 0xf40e35855771202al, 0x106aa07032bbd1b8l,
+ 0x19a4c116b8d2d0c8l, 0x1e376c085141ab53l, 0x2748774cdf8eeb99l,
+ 0x34b0bcb5e19b48a8l, 0x391c0cb3c5c95a63l, 0x4ed8aa4ae3418acbl,
+ 0x5b9cca4f7763e373l, 0x682e6ff3d6b2b8a3l, 0x748f82ee5defb2fcl,
+ 0x78a5636f43172f60l, 0x84c87814a1f0ab72l, 0x8cc702081a6439ecl,
+ 0x90befffa23631e28l, 0xa4506cebde82bde9l, 0xbef9a3f7b2c67915l,
+ 0xc67178f2e372532bl, 0xca273eceea26619cl, 0xd186b8c721c0c207l,
+ 0xeada7dd6cde0eb1el, 0xf57d4f7fee6ed178l, 0x06f067aa72176fbal,
+ 0x0a637dc5a2c898a6l, 0x113f9804bef90dael, 0x1b710b35131c471bl,
+ 0x28db77f523047d84l, 0x32caab7b40c72493l, 0x3c9ebe0a15c9bebcl,
+ 0x431d67c49c100d4cl, 0x4cc5d4becb3e42b6l, 0x597f299cfc657e2al,
+ 0x5fcb6fab3ad6faecl, 0x6c44198c4a475817l
+ ];
+
+ // Initial hash value: the first 64 bits of the fractional parts of the
+ // square roots of the first 8 primes (FIPS 180-4 section 5.3.5).
+ const IV = [
+ 0x6a09e667f3bcc908l, 0xbb67ae8584caa73bl, 0x3c6ef372fe94f82bl,
+ 0xa54ff53a5f1d36f1l, 0x510e527fade682d1l, 0x9b05688c2b3e6c1fl,
+ 0x1f83d9abfb41bd6bl, 0x5be0cd19137e2179l
+ ];
+
+ const BLOCK_BYTES = 128;
+ const LENGTH_FIELD_BYTES = 16;
+
+ // Masks that clear the bits `>>` copies from the sign: Mn keeps the low
+ // 64 - n bits, so ((x >> n) & Mn) is a logical shift and
+ // (((x >> n) & Mn) | (x << (64 - n))) a right rotation by n. The rotations
+ // are written out inline in compress(), because function calls are
+ // expensive in the Monkey C VM and compress() is the hot loop.
+ const M1 = 0x7fffffffffffffffl;
+ const M6 = 0x03ffffffffffffffl;
+ const M7 = 0x01ffffffffffffffl;
+ const M8 = 0x00ffffffffffffffl;
+ const M14 = 0x0003ffffffffffffl;
+ const M18 = 0x00003fffffffffffl;
+ const M19 = 0x00001fffffffffffl;
+ const M28 = 0x0000000fffffffffl;
+ const M34 = 0x000000003fffffffl;
+ const M39 = 0x0000000001ffffffl;
+ const M41 = 0x00000000007fffffl;
+ const M61 = 0x0000000000000007l;
+
+ // Read 8 bytes at offset as a big-endian 64-bit value (two native 32-bit reads).
+ function read64(data as ByteArray, offset as Number) as Long {
+ var hi = data.decodeNumber(Lang.NUMBER_FORMAT_UINT32, {:offset => offset, :endianness => Lang.ENDIAN_BIG}) as Number or Long;
+ var lo = data.decodeNumber(Lang.NUMBER_FORMAT_UINT32, {:offset => offset + 4, :endianness => Lang.ENDIAN_BIG}) as Number or Long;
+ return (hi.toLong() << 32) | (lo.toLong() & 0xFFFFFFFFl);
+ }
+
+ // The message followed by FIPS 180-4 padding: a 1 bit, zeros, and the
+ // message length in bits as a 128-bit big-endian number.
+ function pad(data as ByteArray) as ByteArray {
+ var length = data.size();
+ var total = ((length + 1 + LENGTH_FIELD_BYTES + BLOCK_BYTES - 1) / BLOCK_BYTES) * BLOCK_BYTES;
+ var msg = new [total]b;
+ for (var i = 0; i < length; i++) {
+ msg[i] = data[i];
+ }
+ msg[length] = 0x80;
+ // Monkey C ByteArrays are limited far below 2^61 bytes, so the upper 64
+ // bits of the length field are always zero.
+ var bits = length.toLong() * 8l;
+ for (var i = 0; i < 8; i++) {
+ msg[total - 1 - i] = ((bits >> (8 * i)) & 0xFFl).toNumber();
+ }
+ return msg;
+ }
+
+ // Process one 128-byte block at offset into the hash state h.
+ function compress(h as Array<Long>, msg as ByteArray, offset as Number, w as Array<Long>) as Void {
+ for (var t = 0; t < 16; t++) {
+ w[t] = read64(msg, offset + 8 * t);
+ }
+ for (var t = 16; t < 80; t++) {
+ var x = w[t - 15];
+ var y = w[t - 2];
+ var s0 = (((x >> 1) & M1) | (x << 63)) ^ (((x >> 8) & M8) | (x << 56)) ^ ((x >> 7) & M7);
+ var s1 = (((y >> 19) & M19) | (y << 45)) ^ (((y >> 61) & M61) | (y << 3)) ^ ((y >> 6) & M6);
+ w[t] = w[t - 16] + s0 + w[t - 7] + s1;
+ }
+ var a = h[0];
+ var b = h[1];
+ var c = h[2];
+ var d = h[3];
+ var e = h[4];
+ var f = h[5];
+ var g = h[6];
+ var hh = h[7];
+ for (var t = 0; t < 80; t++) {
+ var bigS1 = (((e >> 14) & M14) | (e << 50)) ^ (((e >> 18) & M18) | (e << 46)) ^ (((e >> 41) & M41) | (e << 23));
+ var choose = (e & f) ^ (~e & g);
+ var temp1 = hh + bigS1 + choose + (K[t] as Long) + w[t];
+ var bigS0 = (((a >> 28) & M28) | (a << 36)) ^ (((a >> 34) & M34) | (a << 30)) ^ (((a >> 39) & M39) | (a << 25));
+ var majority = (a & b) ^ (a & c) ^ (b & c);
+ hh = g;
+ g = f;
+ f = e;
+ e = d + temp1;
+ d = c;
+ c = b;
+ b = a;
+ a = temp1 + bigS0 + majority;
+ }
+ h[0] = h[0] + a;
+ h[1] = h[1] + b;
+ h[2] = h[2] + c;
+ h[3] = h[3] + d;
+ h[4] = h[4] + e;
+ h[5] = h[5] + f;
+ h[6] = h[6] + g;
+ h[7] = h[7] + hh;
+ }
+
+ // The 64-byte SHA-512 digest of data. The padded copy of the message is
+ // wiped before returning, because HMAC feeds keys through here.
+ function digest(data as ByteArray) as ByteArray {
+ var msg = pad(data);
+ var h = [] as Array<Long>;
+ for (var i = 0; i < 8; i++) {
+ h.add(IV[i] as Long);
+ }
+ var w = [] as Array<Long>;
+ for (var i = 0; i < 80; i++) {
+ w.add(0l);
+ }
+ for (var offset = 0; offset < msg.size(); offset += BLOCK_BYTES) {
+ compress(h, msg, offset, w);
+ }
+ Bytes.wipe(msg);
+ for (var i = 0; i < 80; i++) {
+ w[i] = 0l;
+ }
+ var out = new [64]b;
+ for (var i = 0; i < 8; i++) {
+ var word = h[i];
+ for (var j = 7; j >= 0; j--) {
+ out[8 * i + j] = (word & 0xFFl).toNumber();
+ word = word >> 8;
+ }
+ }
+ return out;
+ }
+}