diff options
Diffstat (limited to 'source/import/ImportItem.mc')
| -rw-r--r-- | source/import/ImportItem.mc | 102 |
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; |
