aboutsummaryrefslogtreecommitdiff
path: root/test/crypto
diff options
context:
space:
mode:
Diffstat (limited to 'test/crypto')
-rw-r--r--test/crypto/HashHmacTest.mc54
-rw-r--r--test/crypto/KdfAesTest.mc116
-rw-r--r--test/crypto/SealedBoxTest.mc96
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;
+}