aboutsummaryrefslogtreecommitdiff
path: root/source/import/ImportItem.mc
diff options
context:
space:
mode:
Diffstat (limited to 'source/import/ImportItem.mc')
-rw-r--r--source/import/ImportItem.mc148
1 files changed, 148 insertions, 0 deletions
diff --git a/source/import/ImportItem.mc b/source/import/ImportItem.mc
new file mode 100644
index 0000000..a242aa0
--- /dev/null
+++ b/source/import/ImportItem.mc
@@ -0,0 +1,148 @@
+// Author: cirakg & claude ai
+// Summary: One parsed import candidate (an entry, an error or a skip) 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.
+
+import Toybox.Lang;
+
+class ImportItem {
+
+ // 1-based position in the import (URI number or Aegis entry number).
+ var index as Number;
+ // A short name for messages: the issuer, else the account, else "".
+ var label as String = "";
+ var entry as Entry? = null;
+ // Why the item can't be imported (shown as an error), or null.
+ var problem as String? = null;
+ // Why the item was deliberately skipped (unsupported type), or null.
+ var skipped as String? = null;
+ // A remark that doesn't stop the import (issuer mismatch).
+ var note as String? = null;
+ // Aegis groups: a group UUID (database version 3) or a name, resolved by
+ // the reader once the whole document is read.
+ var groupRef as String? = null;
+
+ function initialize(position as Number) {
+ index = position;
+ }
+
+ // "entry 7 'GitHub'" or "URI 3", for messages.
+ function describe(kind as String) as String {
+ return label.length() > 0 ? kind + " " + index + " '" + label + "'" : kind + " " + index;
+ }
+}
+
+// The raw fields one source item provides, as text, and their conversion.
+class ImportFields {
+
+ var type as String = "totp";
+ var issuer as String = "";
+ var account as String = "";
+ var secret as String? = null;
+ var algorithm as String = "SHA1";
+ var digits as Long = 6l;
+ var period as Long = 30l;
+ var counter as Long? = null;
+ var favorite as Boolean = false;
+
+ function initialize() {
+ }
+
+ // Fill item.entry, or set item.problem / item.skipped.
+ function toEntry(item as ImportItem) as Void {
+ item.label = issuer.length() > 0 ? issuer : account;
+ var t = type.toLower();
+ var otpType = -1;
+ if (t.equals("totp")) {
+ otpType = Entry.OTP_TOTP;
+ } else if (t.equals("hotp")) {
+ otpType = Entry.OTP_HOTP;
+ } else if (t.equals("steam")) {
+ otpType = Entry.OTP_STEAM;
+ } else if (t.equals("motp")) {
+ item.skipped = "mOTP is not supported";
+ return;
+ } else if (t.equals("yandex")) {
+ item.skipped = "Yandex is not supported";
+ return;
+ } else {
+ item.skipped = "type '" + Text.clip(t, 12) + "' is not supported";
+ return;
+ }
+ if (issuer.length() == 0 && account.length() == 0) {
+ item.problem = "needs an issuer or an account name";
+ return;
+ }
+ if (Bytes.fromUtf8(issuer).size() > Entry.MAX_NAME_BYTES || Bytes.fromUtf8(account).size() > Entry.MAX_NAME_BYTES) {
+ item.problem = "issuer or account longer than " + Entry.MAX_NAME_BYTES + " bytes";
+ return;
+ }
+ var e = new Entry();
+ e.kind = Entry.KIND_OTP;
+ e.type = otpType;
+ e.name = issuer;
+ e.account = account;
+ e.source = Entry.SOURCE_IMPORT;
+ e.favorite = favorite;
+ if (otpType == Entry.OTP_STEAM) {
+ // Steam always uses SHA-1, 30 s and 5 characters.
+ e.algorithm = Hashes.SHA1;
+ e.digits = Otp.MIN_DIGITS;
+ e.period = Otp.STEAM_PERIOD;
+ } else {
+ var a = algorithm.toUpper();
+ if (a.equals("SHA1")) {
+ e.algorithm = Hashes.SHA1;
+ } else if (a.equals("SHA256")) {
+ e.algorithm = Hashes.SHA256;
+ } else if (a.equals("SHA512")) {
+ e.algorithm = Hashes.SHA512;
+ } else {
+ item.problem = "algorithm '" + Text.clip(algorithm, 10) + "' is not supported (SHA1, SHA256, SHA512)";
+ return;
+ }
+ if (digits < Otp.MIN_DIGITS || digits > Otp.MAX_DIGITS) {
+ item.problem = digits + " digits not supported (6-8)";
+ return;
+ }
+ e.digits = digits.toNumber();
+ if (period < Otp.MIN_PERIOD || period > Otp.MAX_PERIOD) {
+ item.problem = "period " + period + " s not supported (1-300)";
+ return;
+ }
+ e.period = period.toNumber();
+ }
+ if (otpType == Entry.OTP_HOTP) {
+ var c = counter;
+ if (c == null) {
+ item.problem = "HOTP needs a counter";
+ return;
+ }
+ if (c < 0) {
+ item.problem = "counter can't be negative";
+ return;
+ }
+ e.counter = c;
+ }
+ var s = secret;
+ if (s == null || s.length() == 0) {
+ item.problem = "secret missing";
+ return;
+ }
+ var decoded = Base32.decode(s);
+ if (decoded instanceof Base32.DecodeError) {
+ item.problem = "secret: " + decoded.message + (decoded.position > 0 ? " (position " + decoded.position + ")" : "");
+ return;
+ }
+ if (decoded.size() == 0 || decoded.size() > Entry.MAX_SECRET_BYTES) {
+ item.problem = "secret must be 1 to " + Entry.MAX_SECRET_BYTES + " bytes";
+ Bytes.wipe(decoded);
+ return;
+ }
+ e.secret = decoded;
+ item.entry = e;
+ }
+}