diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-21 03:55:10 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-21 03:55:10 +0200 |
| commit | 5e8ac1e2f5a660e55ea132996e1d5730cfa4fa64 (patch) | |
| tree | 9bb99bdca1ef8f33c93c5100816448681329d810 /source/crypto | |
| parent | 88f1cac4fa530fc30fd6ef01970af11c7a959b8e (diff) | |
feat(glance): show the favourite code in the glance (opt-in)
With "Show the favourite code in the glance" on, the glance shows the
code of the first favourite TOTP or Steam entry without the PIN, with a
countdown bar. The glance can't open the vault, so the About scan seals
that entry under "g" (device key, aad OTPW-GL-1) after every change, and
removes it when the option is off or no entry qualifies; the glance also
reads the option itself. HKDF moves to its own module so the glance loads
only the crypto it needs (37 % of its 64 KB). Manage entry gains "Mark as
favourite (glance)" and "Remove favourite", queued for locked entries
like the other changes. Lines are inset on round screens so the
Instinct 3's curved glance corner doesn't cut them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/crypto')
| -rw-r--r-- | source/crypto/Aes.mc | 2 | ||||
| -rw-r--r-- | source/crypto/Hashes.mc | 2 | ||||
| -rw-r--r-- | source/crypto/Hkdf.mc | 39 | ||||
| -rw-r--r-- | source/crypto/Hmac.mc | 2 | ||||
| -rw-r--r-- | source/crypto/Kdf.mc | 33 | ||||
| -rw-r--r-- | source/crypto/SealedBox.mc | 6 | ||||
| -rw-r--r-- | source/crypto/SelfTest.mc | 2 | ||||
| -rw-r--r-- | source/crypto/Sha512.mc | 2 |
8 files changed, 57 insertions, 31 deletions
diff --git a/source/crypto/Aes.mc b/source/crypto/Aes.mc index e77cb86..242448b 100644 --- a/source/crypto/Aes.mc +++ b/source/crypto/Aes.mc @@ -15,6 +15,8 @@ import Toybox.Cryptography; import Toybox.Lang; +// (:glance): also loaded by the glance, which shows the favourite code (GlanceCode). +(:glance) module Aes { const BLOCK_BYTES = 16; diff --git a/source/crypto/Hashes.mc b/source/crypto/Hashes.mc index 14e2cf2..321ea2d 100644 --- a/source/crypto/Hashes.mc +++ b/source/crypto/Hashes.mc @@ -9,6 +9,8 @@ import Toybox.Cryptography; import Toybox.Lang; +// (:glance): also loaded by the glance, which shows the favourite code (GlanceCode). +(:glance) module Hashes { // OTP and HMAC hash algorithms. The values are stored in entry records, so diff --git a/source/crypto/Hkdf.mc b/source/crypto/Hkdf.mc new file mode 100644 index 0000000..97814b9 --- /dev/null +++ b/source/crypto/Hkdf.mc @@ -0,0 +1,39 @@ +// Author: cirakg & claude ai +// Summary: HKDF-SHA256 (RFC 5869) on the native HMAC, small enough for the glance. +// +// The key derivation SealedBox and KeyManager use for every sub-key. It +// lives apart from Kdf (PBKDF2, which needs the job system) so the glance +// can open the sealed favourite code (GlanceCode) with only the crypto it +// needs. Verified with RFC 5869 test cases 1 to 3 (test/crypto/KdfAesTest.mc) +// and in the start-up self-test. + +import Toybox.Lang; + +(:glance) +module Hkdf { + + const HASH_BYTES = 32; + + // Extract with salt (empty means 32 zero bytes), then expand to + // `length` bytes with info. length <= 255 * 32. + function derive(ikm as ByteArray, salt as ByteArray, info as ByteArray, length as Number) as ByteArray { + if (length < 1 || length > 255 * HASH_BYTES) { + throw new Lang.InvalidValueException("HKDF: bad key length"); + } + var prk = Hmac.nativeSha256(salt.size() > 0 ? salt : new [HASH_BYTES]b, ikm); + var okm = new [0]b; + var block = new [0]b; + for (var counter = 1; okm.size() < length; counter++) { + var input = Bytes.concat(block, info); + input.add(counter); + Bytes.wipe(block); + block = Hmac.nativeSha256(prk, input); + okm.addAll(block); + } + Bytes.wipe(prk); + Bytes.wipe(block); + var key = okm.slice(0, length); + Bytes.wipe(okm); + return key; + } +} diff --git a/source/crypto/Hmac.mc b/source/crypto/Hmac.mc index ff620a4..736d2d4 100644 --- a/source/crypto/Hmac.mc +++ b/source/crypto/Hmac.mc @@ -16,6 +16,8 @@ import Toybox.Cryptography; import Toybox.Lang; +// (:glance): also loaded by the glance, which shows the favourite code (GlanceCode). +(:glance) module Hmac { const IPAD = 0x36; diff --git a/source/crypto/Kdf.mc b/source/crypto/Kdf.mc index 5bace1c..e64c8ff 100644 --- a/source/crypto/Kdf.mc +++ b/source/crypto/Kdf.mc @@ -1,5 +1,5 @@ // Author: cirakg & claude ai -// Summary: Key derivation: resumable PBKDF2-HMAC-SHA256 (RFC 8018) with calibration, and HKDF-SHA256 (RFC 5869). +// Summary: PIN key derivation: resumable PBKDF2-HMAC-SHA256 (RFC 8018) with calibration. // // PBKDF2 turns the PIN into a key (README 7.2). It runs as a Jobs.Job, a // bounded number of iterations per step, because the iteration count is @@ -7,11 +7,11 @@ // watchdog budget. It uses the native HMAC-SHA256: the object keeps the key // and digest() resets it, so each iteration is one native call. // -// HKDF splits one master key into separate encryption and MAC keys -// (README 7.3). +// HKDF, which splits one master key into separate encryption and MAC keys +// (README 7.3), is in Hkdf.mc (the glance needs it, but not PBKDF2). // -// Verified with RFC 7914 section 11 (PBKDF2) and RFC 5869 A.1-A.3 (HKDF), -// plus seeded vectors from Python's hashlib (test/crypto/KdfTest.mc). +// Verified with RFC 7914 section 11 (PBKDF2) plus seeded vectors from +// Python's hashlib (test/crypto/KdfAesTest.mc). import Toybox.Cryptography; import Toybox.Lang; @@ -191,27 +191,4 @@ module Kdf { } return estimate > MAX_ITERATIONS ? MAX_ITERATIONS : estimate.toNumber(); } - - // HKDF-SHA256 (RFC 5869): extract with salt (empty means 32 zero bytes), - // then expand to `length` bytes with info. length <= 255 * 32. - function hkdf(ikm as ByteArray, salt as ByteArray, info as ByteArray, length as Number) as ByteArray { - if (length < 1 || length > 255 * SHA256_BYTES) { - throw new Lang.InvalidValueException("HKDF: bad key length"); - } - var prk = Hmac.nativeSha256(salt.size() > 0 ? salt : new [SHA256_BYTES]b, ikm); - var okm = new [0]b; - var block = new [0]b; - for (var counter = 1; okm.size() < length; counter++) { - var input = Bytes.concat(block, info); - input.add(counter); - Bytes.wipe(block); - block = Hmac.nativeSha256(prk, input); - okm.addAll(block); - } - Bytes.wipe(prk); - Bytes.wipe(block); - var key = okm.slice(0, length); - Bytes.wipe(okm); - return key; - } } diff --git a/source/crypto/SealedBox.mc b/source/crypto/SealedBox.mc index 0804b96..765a1d8 100644 --- a/source/crypto/SealedBox.mc +++ b/source/crypto/SealedBox.mc @@ -20,6 +20,8 @@ import Toybox.Cryptography; import Toybox.Lang; +// (:glance): also loaded by the glance, which shows the favourite code (GlanceCode). +(:glance) module SealedBox { const VERSION = 1; @@ -34,8 +36,8 @@ module SealedBox { var macKey as ByteArray; function initialize(masterKey as ByteArray) { - encKey = Kdf.hkdf(masterKey, []b, Bytes.fromUtf8("OTPW enc v1"), KEY_BYTES); - macKey = Kdf.hkdf(masterKey, []b, Bytes.fromUtf8("OTPW mac v1"), KEY_BYTES); + encKey = Hkdf.derive(masterKey, []b, Bytes.fromUtf8("OTPW enc v1"), KEY_BYTES); + macKey = Hkdf.derive(masterKey, []b, Bytes.fromUtf8("OTPW mac v1"), KEY_BYTES); } // Wipe both keys (when the session locks). diff --git a/source/crypto/SelfTest.mc b/source/crypto/SelfTest.mc index 0d7eae4..87515b6 100644 --- a/source/crypto/SelfTest.mc +++ b/source/crypto/SelfTest.mc @@ -54,7 +54,7 @@ module SelfTest { for (var i = 0; i < 22; i++) { ikm[i] = 0x0b; } - if (!check(Kdf.hkdf(ikm, []b, []b, 42), "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8")) { // lint: allow-literal + if (!check(Hkdf.derive(ikm, []b, []b, 42), "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8")) { // lint: allow-literal return "HKDF"; } // RFC 4226 Appendix D, counter 0. diff --git a/source/crypto/Sha512.mc b/source/crypto/Sha512.mc index 4248462..37e6607 100644 --- a/source/crypto/Sha512.mc +++ b/source/crypto/Sha512.mc @@ -16,6 +16,8 @@ import Toybox.Lang; +// (:glance): also loaded by the glance, which shows the favourite code (GlanceCode). +(:glance) module Sha512 { // Round constants: the first 64 bits of the fractional parts of the cube |
