diff options
Diffstat (limited to 'test/crypto')
| -rw-r--r-- | test/crypto/HashHmacTest.mc | 54 | ||||
| -rw-r--r-- | test/crypto/KdfAesTest.mc | 116 | ||||
| -rw-r--r-- | test/crypto/SealedBoxTest.mc | 96 |
3 files changed, 266 insertions, 0 deletions
diff --git a/test/crypto/HashHmacTest.mc b/test/crypto/HashHmacTest.mc new file mode 100644 index 0000000..15f166c --- /dev/null +++ b/test/crypto/HashHmacTest.mc @@ -0,0 +1,54 @@ +// Author: cirakg & claude ai +// Summary: SHA-1/256/512 and HMAC tests against FIPS 180-4, RFC 2202, RFC 4231 and seeded padding-boundary vectors. + +import Toybox.Lang; +import Toybox.Test; + +// Every hash vector (FIPS 180-4 examples + messages around the padding boundaries). +(:test) +function hashMatchesVectors(logger as Test.Logger) as Boolean { + var ok = true; + for (var i = 0; i < TestVectors.HASH.size(); i++) { + var r = TestSupport.row(TestVectors.HASH, i); + var actual = Hashes.digest(TestSupport.alg(r, 0), TestSupport.bytes(r, 1)); + ok = TestSupport.sameBytes(logger, "hash vector " + i, actual, TestSupport.bytes(r, 2)) && ok; + } + logger.debug(TestVectors.HASH.size() + " hash vectors"); + return ok; +} + +// Generic RFC 2104 HMAC for all three hashes, and the native HMAC-SHA256. +(:test) +function hmacMatchesRfcVectors(logger as Test.Logger) as Boolean { + var ok = true; + for (var i = 0; i < TestVectors.HMAC.size(); i++) { + var r = TestVectors.HMAC[i] as Array; + var alg = TestSupport.alg(r, 0); + var key = TestSupport.bytes(r, 1); + var data = TestSupport.bytes(r, 2); + var expected = TestSupport.bytes(r, 3); + ok = TestSupport.sameBytes(logger, "generic HMAC " + i, Hmac.generic(alg, key, data), expected) && ok; + ok = TestSupport.sameBytes(logger, "HMAC " + i, Hmac.compute(alg, key, data), expected) && ok; + } + return ok; +} + +// HMAC must not change the caller's key, even for keys longer than a block. +(:test) +function hmacLeavesKeyUntouched(logger as Test.Logger) as Boolean { + var key = new [131]b; + for (var i = 0; i < key.size(); i++) { + key[i] = 0xaa; + } + var before = Bytes.copy(key); + Hmac.generic(Hashes.SHA512, key, [1, 2, 3]b); + Hmac.generic(Hashes.SHA1, key.slice(0, 20), [1, 2, 3]b); + return Bytes.equal(key, before); +} + +(:test) +function hashSizesAndValidity(logger as Test.Logger) as Boolean { + return Hashes.digestSize(Hashes.SHA1) == 20 && Hashes.digestSize(Hashes.SHA256) == 32 + && Hashes.digestSize(Hashes.SHA512) == 64 && Hashes.blockSize(Hashes.SHA512) == 128 + && Hashes.blockSize(Hashes.SHA1) == 64 && Hashes.isValid(2) && !Hashes.isValid(3) && !Hashes.isValid(-1); +} diff --git a/test/crypto/KdfAesTest.mc b/test/crypto/KdfAesTest.mc new file mode 100644 index 0000000..6587bc9 --- /dev/null +++ b/test/crypto/KdfAesTest.mc @@ -0,0 +1,116 @@ +// Author: cirakg & claude ai +// Summary: PBKDF2, HKDF and AES-CBC tests (RFC 7914, RFC 5869, NIST SP 800-38A) plus padding and job edge cases. + +import Toybox.Lang; +import Toybox.Test; + +(:test) +function pbkdf2MatchesVectors(logger as Test.Logger) as Boolean { + var ok = true; + for (var i = 0; i < TestVectors.PBKDF2.size(); i++) { + var r = TestSupport.row(TestVectors.PBKDF2, i); + var key = Kdf.pbkdf2(TestSupport.bytes(r, 0), TestSupport.bytes(r, 1), TestSupport.num(r, 2), TestSupport.num(r, 3)); + ok = TestSupport.sameBytes(logger, "PBKDF2 vector " + i, key, TestSupport.bytes(r, 4)) && ok; + } + return ok; +} + +// The result must not depend on how the work is split into steps. +(:test) +function pbkdf2StepSizeDoesNotMatter(logger as Test.Logger) as Boolean { + var password = Bytes.fromUtf8("123456"); + var salt = new [16]b; + var oneByOne = new Kdf.Pbkdf2(password, salt, 37, 40); + var steps = 0; + while (!oneByOne.step(1)) { + steps++; + } + // 2 output blocks x 37 iterations, the last step returns true. + return Bytes.equal(oneByOne.result(), Kdf.pbkdf2(password, salt, 37, 40)) && steps == 73; +} + +(:test) +function pbkdf2RejectsBadParameters(logger as Test.Logger) as Boolean { + var rejected = 0; + var cases = [[0, 32], [1, 0], [1, 255 * 32 + 1]]; + for (var i = 0; i < cases.size(); i++) { + var c = cases[i] as Array<Number>; + try { + new Kdf.Pbkdf2([1]b, [2]b, c[0], c[1]); + } catch (e instanceof Lang.InvalidValueException) { + rejected++; + } + } + return rejected == cases.size(); +} + +(:test) +function pbkdf2CalibrationStaysInBounds(logger as Test.Logger) as Boolean { + var iterations = Kdf.calibrate(1000, 500); + logger.debug("calibrated iterations for 1 s in this simulator: " + iterations); + return iterations >= Kdf.MIN_ITERATIONS && iterations <= Kdf.MAX_ITERATIONS; +} + +(:test) +function hkdfMatchesRfcVectors(logger as Test.Logger) as Boolean { + var ok = true; + for (var i = 0; i < TestVectors.HKDF.size(); i++) { + var r = TestSupport.row(TestVectors.HKDF, i); + var okm = Kdf.hkdf(TestSupport.bytes(r, 0), TestSupport.bytes(r, 1), TestSupport.bytes(r, 2), TestSupport.num(r, 3)); + ok = TestSupport.sameBytes(logger, "HKDF vector " + i, okm, TestSupport.bytes(r, 4)) && ok; + } + return ok; +} + +(:test) +function aesCbcMatchesNistVectors(logger as Test.Logger) as Boolean { + var ok = true; + for (var i = 0; i < TestVectors.AES_CBC.size(); i++) { + var r = TestSupport.row(TestVectors.AES_CBC, i); + var key = TestSupport.bytes(r, 0); + var iv = TestSupport.bytes(r, 1); + var plain = TestSupport.bytes(r, 2); + var cipher = TestSupport.bytes(r, 3); + ok = TestSupport.sameBytes(logger, "AES encrypt " + i, Aes.cbcRaw(key, iv, plain, true), cipher) && ok; + ok = TestSupport.sameBytes(logger, "AES decrypt " + i, Aes.cbcRaw(key, iv, cipher, false), plain) && ok; + } + return ok; +} + +// PKCS#7 round trips for every length from 0 to 40 bytes, both key sizes. +(:test) +function aesPaddingRoundTrips(logger as Test.Logger) as Boolean { + var iv = new [16]b; + var keys = [new [16]b, new [32]b]; + for (var k = 0; k < keys.size(); k++) { + for (var length = 0; length <= 40; length++) { + var plain = new [length]b; + for (var i = 0; i < length; i++) { + plain[i] = (i * 7 + length) & 0xFF; + } + var cipher = Aes.cbcEncrypt(keys[k] as ByteArray, iv, plain); + var back = Aes.cbcDecrypt(keys[k] as ByteArray, iv, cipher); + if (cipher.size() != (length / 16 + 1) * 16 || back == null || !Bytes.equal(back, plain)) { + logger.error("padding round trip failed at length " + length); + return false; + } + } + } + return true; +} + +(:test) +function aesRejectsBadPaddingAndLength(logger as Test.Logger) as Boolean { + var key = new [32]b; + var iv = new [16]b; + // A block whose decryption ends in 0x00, which is never valid padding. + var badPadding = Aes.cbcRaw(key, iv, new [16]b, true); + var badKey = false; + try { + Aes.cbcEncrypt(new [24]b, iv, [1]b); + } catch (e instanceof Lang.InvalidValueException) { + badKey = true; + } + return Aes.cbcDecrypt(key, iv, badPadding) == null && Aes.cbcDecrypt(key, iv, new [15]b) == null + && Aes.cbcDecrypt(key, iv, []b) == null && badKey; +} diff --git a/test/crypto/SealedBoxTest.mc b/test/crypto/SealedBoxTest.mc new file mode 100644 index 0000000..810e512 --- /dev/null +++ b/test/crypto/SealedBoxTest.mc @@ -0,0 +1,96 @@ +// Author: cirakg & claude ai +// Summary: Tests for SealedBox (encrypt-then-MAC records): round trips, tamper detection, wrong key or aad, and the self-test. + +import Toybox.Lang; +import Toybox.Test; + +(:test) +module SealedBoxFixtures { + function keys(seed as Number) as SealedBox.Keys { + var master = new [32]b; + for (var i = 0; i < 32; i++) { + master[i] = (i * 13 + seed) & 0xFF; + } + return new SealedBox.Keys(master); + } +} + +(:test) +function sealedBoxRoundTrips(logger as Test.Logger) as Boolean { + var keys = SealedBoxFixtures.keys(1); + var aad = Bytes.fromUtf8("K7F2|header"); + for (var length = 0; length <= 70; length += 7) { + var plain = new [length]b; + for (var i = 0; i < length; i++) { + plain[i] = (i * 31) & 0xFF; + } + var box = SealedBox.seal(keys, aad, plain); + var back = SealedBox.open(keys, aad, box); + if (back == null || !Bytes.equal(back, plain) || box[0] != SealedBox.VERSION) { + logger.error("round trip failed at length " + length); + return false; + } + } + return true; +} + +// Flipping any single byte of the box (version, IV, ciphertext or tag) must +// make open() return null. +(:test) +function sealedBoxDetectsEveryTamperedByte(logger as Test.Logger) as Boolean { + var keys = SealedBoxFixtures.keys(2); + var aad = [9, 9]b; + var box = SealedBox.seal(keys, aad, Bytes.fromUtf8("JBSWY3DPEHPK3PXP fake secret")); + for (var i = 0; i < box.size(); i++) { + var tampered = Bytes.copy(box); + tampered[i] = tampered[i] ^ 0x01; + if (SealedBox.open(keys, aad, tampered) != null) { + logger.error("tampering byte " + i + " was not detected"); + return false; + } + } + return true; +} + +(:test) +function sealedBoxRejectsWrongKeyAadAndTruncation(logger as Test.Logger) as Boolean { + var keys = SealedBoxFixtures.keys(3); + var aad = [1, 2, 3]b; + var box = SealedBox.seal(keys, aad, [42]b); + // Moving a byte from the aad into the box must not verify either. + var shifted = Bytes.concat([3]b, box); + return SealedBox.open(SealedBoxFixtures.keys(4), aad, box) == null + && SealedBox.open(keys, [1, 2]b, box) == null + && SealedBox.open(keys, [1, 2]b, shifted) == null + && SealedBox.open(keys, aad, box.slice(0, box.size() - 1)) == null + && SealedBox.open(keys, aad, []b) == null + && SealedBox.open(keys, aad, box) != null; +} + +// The same IV gives the same box; random IVs make every seal different. +(:test) +function sealedBoxIvHandling(logger as Test.Logger) as Boolean { + var keys = SealedBoxFixtures.keys(5); + var iv = new [16]b; + var a = SealedBox.sealWithIv(keys, []b, [7]b, iv); + var b = SealedBox.sealWithIv(keys, []b, [7]b, iv); + var c = SealedBox.seal(keys, []b, [7]b); + var d = SealedBox.seal(keys, []b, [7]b); + return Bytes.equal(a, b) && !Bytes.equal(c, d); +} + +(:test) +function sealedBoxKeysWipe(logger as Test.Logger) as Boolean { + var keys = SealedBoxFixtures.keys(6); + keys.wipe(); + return Bytes.equal(keys.encKey, new [32]b) && Bytes.equal(keys.macKey, new [32]b); +} + +(:test) +function selfTestPasses(logger as Test.Logger) as Boolean { + var failed = SelfTest.run(); + if (failed != null) { + logger.error("self-test failed: " + failed); + } + return failed == null; +} |
