diff options
Diffstat (limited to 'source/settings/ManageForm.mc')
| -rw-r--r-- | source/settings/ManageForm.mc | 265 |
1 files changed, 216 insertions, 49 deletions
diff --git a/source/settings/ManageForm.mc b/source/settings/ManageForm.mc index 70d707f..ba2d45f 100644 --- a/source/settings/ManageForm.mc +++ b/source/settings/ManageForm.mc @@ -14,7 +14,11 @@ // move twice can't hit another entry. // Replacing a code's contents runs CodeCheck with the entry's own options // (a sub-job, so a large 2D code can't trip the watchdog). For a locked -// entry the options are encrypted, so that check runs when the PIN confirms. +// entry the options are encrypted, so that check runs when the PIN confirms; +// everything else about a REPLACE (Base32, the contents' size) is checked +// before it is queued. With 4 KB of contents every byte loop (UTF-8 check, +// opening and re-sealing the record, wiping) gets a step of its own, and +// the wipes at the end run in pieces (Jobs.Wiper). import Toybox.Lang; @@ -46,6 +50,7 @@ class ManageAction { static const T_COUNTER = 8; static const T_LABEL = 9; static const T_DECODED = 10; + static const T_REF = 11; // In "Edit details", this value clears a field; empty keeps it. static const CLEAR = "-"; @@ -57,10 +62,18 @@ class ManageAction { var group as String = ""; var position as Number = 0; // REPLACE: the decoded OTP secret or the new code contents, once - // checked against the entry (raw text before). Wiped after use. + // checked against the entry (a 0.9.0 queue item may hold the raw text + // of a code's contents instead). Wiped after use. Queued, it is a + // storage value of its own (ChangeQueue, `ref`). var material as ByteArray? = null; + // REPLACE from the form: the raw field text until ManageRun checks it. + // Never queued. + var text as String? = null; + // A queued REPLACE: the number of its material record, -1 for none. + var ref as Number = -1; var counter as Long = 0l; - // True once an OTP secret in `material` is decoded from Base32. + // True once `material` holds the checked secret (decoded from Base32) + // or contents (CodeForm.contents), not raw text. var decoded as Boolean = false; // "Bank (carol)", kept so the queue can be shown while locked. var label as String = ""; @@ -84,6 +97,7 @@ class ManageAction { function wipe() as Void { Bytes.wipe(material); material = null; + text = null; } function toBytes() as ByteArray { @@ -101,6 +115,9 @@ class ManageAction { w.addInt(T_COUNTER, counter); w.addText(T_LABEL, label); w.addBool(T_DECODED, decoded); + if (ref >= 0) { + w.addInt(T_REF, ref); + } return w.bytes; } @@ -133,6 +150,8 @@ class ManageAction { a.material = m instanceof ByteArray ? m : null; var decoded = map[T_DECODED]; a.decoded = decoded instanceof Boolean && decoded; + var ref = map[T_REF]; + a.ref = ref instanceof Long && ref >= 0 && ref < 0x10000 ? ref.toNumber() : -1; return a; } @@ -205,10 +224,10 @@ module ManageForm { a.position = position; } else if (action == ManageAction.REPLACE) { var secret = Forms.rawText(store, SECRET); - if (Text.trim(secret).length() == 0) { - return "Replace: enter the new secret or contents"; + if (secret.length() == 0) { + return "Replace: enter the new secret or contents"; // only spaces: ManageRun says so, in steps } - a.material = Bytes.fromUtf8(secret); + a.text = secret; // checked against the entry by ManageRun } else if (action == ManageAction.COUNTER) { var counter = Forms.long(store, "mgCounter"); if (counter == null || counter < 0) { @@ -249,6 +268,35 @@ class ManageRun extends Jobs.Job { private var _header as Entry? = null; private var _check as CodeCheck? = null; private var _cache as ByteArray? = null; + // Buffers to wipe once the run ends; large ones take steps of their own. + private var _wiper as Jobs.Wiper = new Jobs.Wiper(); + private var _wiping as Boolean = false; + // A code REPLACE: CodeCheck is still to start, with this payload text + // (null: from the queue, still to be read from `material`). + private var _needsCheck as Boolean = false; + private var _payload as String? = null; + // How far queued contents are checked as UTF-8. + private var _utf8At as Number = 0; + // A REPLACE's raw text, prepared in steps before it is checked + // (prepareText): a 0.9.0 item's raw bytes checked as UTF-8, then the + // spaces around it found, in pieces (kilobytes of either are a byte or + // character loop). + private var _rawAt as Number = 0; + private var _chars as Array<Char>? = null; + private var _trimStart as Number = 0; + private var _trimEnd as Number = -1; + private var _trimmed as String? = null; + // A REPLACE: the entry opened in the step before it is re-sealed. + private var _entry as Entry? = null; + + // Raw OTP secrets longer than this are refused before Base32.decode's + // character loop (128 bytes are 205 Base32 characters; this leaves room + // for spaces). + static const MAX_SECRET_TEXT = 1024; + // Contents checked as UTF-8 per step (up to 30 bytecodes a byte). + static const UTF8_STEP_BYTES = 1024; + // Characters looked at per step when trimming the raw text. + static const TRIM_STEP_CHARS = 512; // confirmed: the PIN was just entered for this action (from the queue), // so it is applied, never queued again. @@ -260,6 +308,9 @@ class ManageRun extends Jobs.Job { } function step(budget as Number) as Boolean { + if (_wiping) { + return _wiper.step(Jobs.Wiper.STEP_BYTES); + } var check = _check; if (check != null) { if (!check.step(budget)) { @@ -277,12 +328,22 @@ class ManageRun extends Jobs.Job { return false; } if (_header == null) { + if (_a.action == ManageAction.REPLACE && !_a.decoded && _trimmed == null) { + return prepareText(); + } try { return resolve(); } catch (e instanceof VaultError) { return fail(problemText(e)); } } + if (_needsCheck) { + try { + return prepareCheck(); + } catch (e instanceof VaultError) { + return fail(problemText(e)); + } + } return finish(_cache); } @@ -317,7 +378,8 @@ class ManageRun extends Jobs.Job { return queue(); } if (a.action == ManageAction.REPLACE && header.kind == Entry.KIND_CODE) { - return startCodeCheck(header); + _needsCheck = true; // CodeCheck with the entry's options (prepareCheck) + return false; } return false; } @@ -344,12 +406,26 @@ class ManageRun extends Jobs.Job { if (a.action == ManageAction.UNFAVORITE && !header.favorite) { return a.id + " is not a favourite"; } - if (a.action == ManageAction.REPLACE && isOtp && !a.decoded) { - var raw = a.material as ByteArray; - var text = Bytes.toUtf8(raw); - Bytes.wipe(raw); - a.material = null; - var decoded = OtpForm.decodeSecret(text != null ? text : "?"); + if (a.action == ManageAction.REPLACE && !a.decoded) { + // Checked before queueing, so a locked entry's queue item holds + // only the decoded secret or the checked contents. + var text = a.text as String; + var trimmed = _trimmed as String; + a.text = null; + if (!isOtp) { + var contents = CodeForm.contentsOf(text, trimmed, header.type); + if (contents instanceof String) { + return contents; + } + a.material = contents; + a.decoded = true; + _payload = Codes.isLinear(header.type) ? trimmed : text; + return null; + } + if (text.length() > MAX_SECRET_TEXT) { + return "Secret: too long"; + } + var decoded = OtpForm.decodeSecret(trimmed); // trimmed: no character loop there if (decoded instanceof String) { return decoded; } @@ -379,45 +455,105 @@ class ManageRun extends Jobs.Job { private function queue() as Boolean { try { - ChangeQueue.add(_vault, _a); + ChangeQueue.addWith(_vault, _a, _wiper); } catch (e instanceof VaultError) { return fail(e.code == VaultError.LIMIT ? e.getErrorMessage() as String : "storage full"); } queued = true; message = (_a.action == ManageAction.LOCK ? "Locks when you next enter your PIN on the watch: " : "Waiting for your PIN on the watch: ") + _a.describe(); - _a.wipe(); - return true; + wipeAction(); + return ending(); } - // Replacing a code's contents: validate them with the entry's options. - private function startCodeCheck(header as Entry) as Boolean { + // One step of preparing a REPLACE's raw text: a 0.9.0 item's raw bytes + // become text (checked as UTF-8 in pieces); text longer than any valid + // input is refused; then the spaces around it are found in pieces. + private function prepareText() as Boolean { var a = _a; - var raw = a.material as ByteArray; - var text = Bytes.toUtf8(raw); - Bytes.wipe(raw); - a.material = null; - var contents = CodeForm.contents(text != null ? text : "", header.type); - if (contents instanceof String) { - return fail(contents); - } - a.material = contents; - var options = Codes.DEFAULT_OPTIONS; - try { - var entry = _vault.open(a.id); - options = entry.options; - entry.wipeSecrets(); - } catch (e instanceof VaultError) { - return fail(problemText(e)); + var text = a.text; + if (text == null) { + var raw = a.material; + if (raw == null) { + return fail("Replace: enter the new secret or contents"); + } + if (_rawAt < raw.size()) { + var end = _rawAt + UTF8_STEP_BYTES < raw.size() ? _rawAt + UTF8_STEP_BYTES : raw.size(); + _rawAt = Bytes.utf8Prefix(raw, _rawAt, end); + return _rawAt < 0 ? fail("Replace: the new secret or contents are damaged") : false; + } + a.text = Bytes.toUtf8Checked(raw); + a.material = null; + _wiper.add(raw); + return false; + } + var chars = _chars; + if (chars == null) { + if (text.length() > Entry.MAX_PAYLOAD_BYTES) { + return fail("Replace: too long (" + text.length() + " characters, at most " + Entry.MAX_PAYLOAD_BYTES + ")"); + } + chars = text.toCharArray(); + _chars = chars; + _trimEnd = chars.size(); + return false; + } + var budget = TRIM_STEP_CHARS; + while (budget > 0 && _trimStart < _trimEnd && Text.isSpace(chars[_trimStart])) { + _trimStart++; + budget--; + } + if (budget == 0) { + return false; } - var payload = Bytes.toUtf8(contents); - _check = new CodeCheck(header.type, payload != null ? payload : "", options); + while (budget > 0 && _trimEnd > _trimStart && Text.isSpace(chars[_trimEnd - 1])) { + _trimEnd--; + budget--; + } + if (budget == 0) { + return false; + } + _trimmed = text.substring(_trimStart, _trimEnd); + _chars = null; + return false; + } + + // One step towards CodeCheck: queued contents are read as text first + // (the UTF-8 check is a byte loop, done in pieces), then the entry is opened for its + // options. + private function prepareCheck() as Boolean { + var a = _a; + var payload = _payload; + if (payload == null) { + var m = a.material as ByteArray; + if (_utf8At < m.size()) { + var end = _utf8At + UTF8_STEP_BYTES < m.size() ? _utf8At + UTF8_STEP_BYTES : m.size(); + _utf8At = Bytes.utf8Prefix(m, _utf8At, end); + return _utf8At < 0 ? fail("the new contents are damaged") : false; + } + _payload = Bytes.toUtf8Checked(m); + return false; + } + var entry = _vault.openWith(a.id, _wiper); + var options = entry.options; + wipeLater(entry); + _needsCheck = false; + _payload = null; + _check = new CodeCheck((_header as Entry).type, payload, options); return false; } // Apply the action; cache: a checked code's packed matrix, or null. private function finish(cache as ByteArray?) as Boolean { var a = _a; + if (a.action == ManageAction.REPLACE && _entry == null) { + // Opening and re-sealing a large code are a step each. + try { + _entry = _vault.openWith(a.id, _wiper); + } catch (e instanceof VaultError) { + return fail(problemText(e)); + } + return false; + } var target = a.id + (a.label.length() > 0 ? " " + a.label : ""); try { if (a.action == ManageAction.MOVE) { @@ -435,15 +571,14 @@ class ManageRun extends Jobs.Job { return fail(problem); } if (cache != null) { - _vault.saveCodeCache(a.id, cache); + _vault.saveCodeCacheWith(a.id, cache, _wiper); } } } catch (e instanceof VaultError) { - a.wipe(); return fail(problemText(e)); } - a.wipe(); - return true; + wipeAction(); + return ending(); } // Edit, lock, unlock, (un)favourite or replace: open, change, re-seal. @@ -451,13 +586,15 @@ class ManageRun extends Jobs.Job { private function changeEntry() as String? { var a = _a; var target = a.id + (a.label.length() > 0 ? " " + a.label : ""); - var entry = _vault.open(a.id); + var opened = _entry; + _entry = null; + var entry = opened != null ? opened : _vault.openWith(a.id, _wiper); if (a.action == ManageAction.EDIT) { entry.name = edited(entry.name, a.name); entry.account = edited(entry.account, a.account); entry.group = edited(entry.group, a.group); if (entry.name.length() == 0 && (entry.kind == Entry.KIND_CODE || entry.account.length() == 0)) { - entry.wipeSecrets(); + wipeLater(entry); return entry.kind == Entry.KIND_CODE ? "Name: a barcode needs a name" : "An OTP entry needs an issuer or an account"; } message = "Edited " + a.id + ": " + Forms.label(entry.name, entry.account) @@ -471,22 +608,22 @@ class ManageRun extends Jobs.Job { } else { var material = a.material as ByteArray; if (entry.kind == Entry.KIND_OTP) { - Bytes.wipe(entry.secret); + _wiper.add(entry.secret); entry.secret = Bytes.copy(material); message = "Replaced the secret of " + target; } else { - Bytes.wipe(entry.payload); + _wiper.add(entry.payload); entry.payload = Bytes.copy(material); message = "Replaced the contents of " + target; } } try { - _vault.update(entry); + _vault.updateWith(entry, _wiper); } catch (e instanceof VaultError) { - entry.wipeSecrets(); + wipeLater(entry); throw e; } - entry.wipeSecrets(); + wipeLater(entry); return null; } @@ -501,8 +638,38 @@ class ManageRun extends Jobs.Job { private function fail(problem as String) as Boolean { failed = true; message = problem; - _a.wipe(); - return true; + var entry = _entry; + if (entry != null) { + wipeLater(entry); + _entry = null; + } + wipeAction(); + return ending(); + } + + // The action's material goes to the wiper. + private function wipeAction() as Void { + _wiper.add(_a.material); + _a.material = null; + _a.text = null; + } + + private function wipeLater(entry as Entry) as Void { + _wiper.add(entry.secret); + _wiper.add(entry.payload); + entry.secret = null; + entry.payload = null; + } + + // The run is over once its buffers are wiped: small ones now, large + // ones in the next steps. + private function ending() as Boolean { + if (_wiper.left() <= Jobs.Wiper.INLINE_BYTES) { + _wiper.step(Jobs.Wiper.INLINE_BYTES); + return true; + } + _wiping = true; + return false; } static function problemText(e as VaultError) as String { |
