1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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;
}
}
|