summaryrefslogtreecommitdiff
path: root/source/import/ImportItem.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/import/ImportItem.mc')
-rw-r--r--source/import/ImportItem.mc102
1 files changed, 101 insertions, 1 deletions
diff --git a/source/import/ImportItem.mc b/source/import/ImportItem.mc
index a242aa0..cc772af 100644
--- a/source/import/ImportItem.mc
+++ b/source/import/ImportItem.mc
@@ -1,10 +1,17 @@
// Author: cirakg & claude ai
-// Summary: One parsed import candidate (an entry, an error or a skip) and the shared field validation of both formats.
+// Summary: One parsed import candidate (an entry, an error or a skip), its packed form, and the shared field validation of both formats.
//
// Both parsers (otpauth URIs and Aegis JSON) turn text fields into an Entry
// through ImportFields.toEntry, so the same rules (README 10.4) and the same
// messages apply to both. Messages name the field and the problem, never the
// secret itself.
+//
+// Memory: an item with its Entry takes about 760 bytes as objects (measured
+// on the Forerunner 255S in M8), so 150 parsed items would take 110 KB. The
+// parsers keep each finished item packed into one ByteArray (Codec TLV,
+// about 110 bytes) instead, and the importer unpacks one item per step.
+// The packed form holds the secret in clear, like the Entry it replaces;
+// the importer wipes it as soon as the item is unpacked.
import Toybox.Lang;
@@ -25,10 +32,103 @@ class ImportItem {
// the reader once the whole document is read.
var groupRef as String? = null;
+ // Packed field tags.
+ private static const P_INDEX = 1;
+ private static const P_LABEL = 2;
+ private static const P_PROBLEM = 3;
+ private static const P_SKIPPED = 4;
+ private static const P_NOTE = 5;
+ private static const P_GROUP_REF = 6;
+ private static const P_TYPE = 10;
+ private static const P_NAME = 11;
+ private static const P_ACCOUNT = 12;
+ private static const P_FAVORITE = 13;
+ private static const P_ALGORITHM = 14;
+ private static const P_DIGITS = 15;
+ private static const P_PERIOD = 16;
+ private static const P_COUNTER = 17;
+ private static const P_SECRET = 18;
+
function initialize(position as Number) {
index = position;
}
+ // This item as one ByteArray. Wipes the entry's secret: the packed copy
+ // is the only one left.
+ function pack() as ByteArray {
+ var w = new Codec.Writer();
+ w.addInt(P_INDEX, index);
+ w.addText(P_LABEL, label);
+ addText(w, P_PROBLEM, problem);
+ addText(w, P_SKIPPED, skipped);
+ addText(w, P_NOTE, note);
+ addText(w, P_GROUP_REF, groupRef);
+ var e = entry;
+ if (e != null && e.secret != null) {
+ w.addInt(P_TYPE, e.type);
+ w.addText(P_NAME, e.name);
+ w.addText(P_ACCOUNT, e.account);
+ w.addBool(P_FAVORITE, e.favorite);
+ w.addInt(P_ALGORITHM, e.algorithm);
+ w.addInt(P_DIGITS, e.digits);
+ w.addInt(P_PERIOD, e.period);
+ w.addInt(P_COUNTER, e.counter);
+ w.addBytes(P_SECRET, e.secret as ByteArray);
+ e.wipeSecrets();
+ }
+ return w.bytes;
+ }
+
+ private static function addText(w as Codec.Writer, tag as Number, value as String?) as Void {
+ if (value != null) {
+ w.addText(tag, value);
+ }
+ }
+
+ // The item packed by pack(), with its group resolved: groupRef is
+ // "name:<group>" or "uuid:<id>" looked up in groups (Aegis db.groups).
+ // The caller wipes `packed`.
+ static function unpack(packed as ByteArray, groups as Dictionary<String, String>) as ImportItem {
+ var map = Codec.decode(packed);
+ var item = new ImportItem(0);
+ if (map == null) {
+ item.problem = "invalid"; // only if memory was corrupted
+ return item;
+ }
+ item.index = (map[P_INDEX] as Long).toNumber();
+ item.label = map[P_LABEL] as String;
+ item.problem = map[P_PROBLEM] as String?;
+ item.skipped = map[P_SKIPPED] as String?;
+ item.note = map[P_NOTE] as String?;
+ var secret = map[P_SECRET];
+ if (secret instanceof ByteArray) {
+ var e = new Entry();
+ e.kind = Entry.KIND_OTP;
+ e.source = Entry.SOURCE_IMPORT;
+ e.type = (map[P_TYPE] as Long).toNumber();
+ e.name = map[P_NAME] as String;
+ e.account = map[P_ACCOUNT] as String;
+ e.favorite = map[P_FAVORITE] as Boolean;
+ e.algorithm = (map[P_ALGORITHM] as Long).toNumber() as Hashes.Algorithm;
+ e.digits = (map[P_DIGITS] as Long).toNumber();
+ e.period = (map[P_PERIOD] as Long).toNumber();
+ e.counter = map[P_COUNTER] as Long;
+ e.secret = secret;
+ e.group = groupName(map[P_GROUP_REF] as String?, groups);
+ item.entry = e;
+ }
+ return item;
+ }
+
+ private static function groupName(ref as String?, groups as Dictionary<String, String>) as String {
+ if (ref == null) {
+ return "";
+ }
+ var key = ref.substring(5, ref.length()) as String;
+ var name = ref.find("uuid:") == 0 ? groups[key] : key;
+ return name != null ? AegisReader.fitBytes(Text.trim(name), Entry.MAX_GROUP_BYTES) : "";
+ }
+
// "entry 7 'GitHub'" or "URI 3", for messages.
function describe(kind as String) as String {
return label.length() > 0 ? kind + " " + index + " '" + label + "'" : kind + " " + index;