aboutsummaryrefslogtreecommitdiff
path: root/source/crypto
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-27 01:01:18 +0100
committercirakg <cirakg@cirakg.xyz>2026-10-27 01:01:18 +0100
commitbc4cc19df5f53519ca1aabd3cf161b6d78ffc16c (patch)
tree3242a63f603882d723bd0d718d043c30a9f11784 /source/crypto
parent34daecaa25e5bdd995179a2b4b9d922f8bbb3b54 (diff)
fix(pin): the work after a PIN and the change queue run in steps
After a correct or new PIN, promoting pending entries and applying waiting "Require PIN" changes ran in the event of the last PBKDF2 step; 16 waiting changes tripped the watchdog on the Forerunner 70. ChangeQueue.LockJob now does it in steps while the PIN screen shows "Checking...", saves what it did to the results and refreshes the glance after Set PIN too. The list title decrypted every waiting change at each start. Queue items are now [action, box] (aad OTPW-Q-2 + action code), so they are counted without decrypting; 0.9.0 items are still read. A replacement is checked when it arrives and its material is sealed apart (qm.<n>). Large buffers are wiped and UTF-8-checked in pieces (Jobs.Wiper, Bytes.utf8Prefix, SealedBox.openKeep/sealKeep). "Phone changes" reads behind "Reading...", and Confirm all / Reject all act only on the changes shown. Storage full no longer stops the app at the PIN. Found by the review of b8addc8/096b5a4 and a new make watchdog test; the diff was reviewed again and its findings fixed. fr70: make lint, make test (176), make watchdog (19 cases at two steps per event), make screenshots pass; list_locked and list_reset goldens show "Results ready (menu)". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/crypto')
-rw-r--r--source/crypto/Aes.mc23
-rw-r--r--source/crypto/SealedBox.mc21
2 files changed, 40 insertions, 4 deletions
diff --git a/source/crypto/Aes.mc b/source/crypto/Aes.mc
index 242448b..9e4f320 100644
--- a/source/crypto/Aes.mc
+++ b/source/crypto/Aes.mc
@@ -42,19 +42,34 @@ module Aes {
// 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 {
+ return cbcEncryptKeep(key, iv, plaintext, null);
+ }
+
+ // As cbcEncrypt(); with `leftovers`, the padded copy is added there for
+ // the caller to wipe (in steps, for kilobytes) instead of wiped here.
+ function cbcEncryptKeep(key as ByteArray, iv as ByteArray, plaintext as ByteArray, leftovers as Array<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);
+ if (leftovers != null) {
+ leftovers.add(padded);
+ } else {
+ 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? {
+ return cbcDecryptKeep(key, iv, ciphertext, null);
+ }
+
+ // As cbcDecrypt(); with `leftovers`, the padded plaintext is added there.
+ function cbcDecryptKeep(key as ByteArray, iv as ByteArray, ciphertext as ByteArray, leftovers as Array<ByteArray>?) as ByteArray? {
var size = ciphertext.size();
if (size == 0 || size % BLOCK_BYTES != 0) {
return null;
@@ -68,7 +83,11 @@ module Aes {
}
}
var plaintext = valid ? padded.slice(0, size - padLength) : null;
- Bytes.wipe(padded);
+ if (leftovers != null) {
+ leftovers.add(padded);
+ } else {
+ Bytes.wipe(padded);
+ }
return plaintext;
}
}
diff --git a/source/crypto/SealedBox.mc b/source/crypto/SealedBox.mc
index 765a1d8..3877e1a 100644
--- a/source/crypto/SealedBox.mc
+++ b/source/crypto/SealedBox.mc
@@ -55,7 +55,19 @@ module SealedBox {
// Seal with a given IV. Only tests call this directly (fixed IVs make the
// output reproducible); reusing an IV in real use would be a mistake.
function sealWithIv(keys as Keys, aad as ByteArray, plaintext as ByteArray, iv as ByteArray) as ByteArray {
- var ciphertext = Aes.cbcEncrypt(keys.encKey, iv, plaintext);
+ return sealKeepWithIv(keys, aad, plaintext, iv, null);
+ }
+
+ // As seal(); with `leftovers`, the padded plaintext copy is added there
+ // for the caller to wipe in steps (zeroing 4 KB is a large part of a
+ // watchdog step) instead of wiped here.
+ function sealKeep(keys as Keys, aad as ByteArray, plaintext as ByteArray, leftovers as Array<ByteArray>?) as ByteArray {
+ return sealKeepWithIv(keys, aad, plaintext, Cryptography.randomBytes(IV_BYTES), leftovers);
+ }
+
+ function sealKeepWithIv(keys as Keys, aad as ByteArray, plaintext as ByteArray, iv as ByteArray,
+ leftovers as Array<ByteArray>?) as ByteArray {
+ var ciphertext = Aes.cbcEncryptKeep(keys.encKey, iv, plaintext, leftovers);
var box = [VERSION]b;
box.addAll(iv);
box.addAll(ciphertext);
@@ -66,6 +78,11 @@ module SealedBox {
// The plaintext, or null if the box is malformed, was sealed with another
// key or aad, or was changed in any way.
function open(keys as Keys, aad as ByteArray, box as ByteArray) as ByteArray? {
+ return openKeep(keys, aad, box, null);
+ }
+
+ // As open(); with `leftovers`, the padded plaintext is added there.
+ function openKeep(keys as Keys, aad as ByteArray, box as ByteArray, leftovers as Array<ByteArray>?) as ByteArray? {
var size = box.size();
if (size < MIN_SIZE || (size - 1 - IV_BYTES - TAG_BYTES) % Aes.BLOCK_BYTES != 0 || box[0] != VERSION) {
return null;
@@ -77,7 +94,7 @@ module SealedBox {
if (!valid) {
return null;
}
- return Aes.cbcDecrypt(keys.encKey, box.slice(1, 1 + IV_BYTES), box.slice(1 + IV_BYTES, size - TAG_BYTES));
+ return Aes.cbcDecryptKeep(keys.encKey, box.slice(1, 1 + IV_BYTES), box.slice(1 + IV_BYTES, size - TAG_BYTES), leftovers);
}
// HMAC over the label, the aad (with its length) and the box body.