diff options
Diffstat (limited to 'source/storage/Entry.mc')
| -rw-r--r-- | source/storage/Entry.mc | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/source/storage/Entry.mc b/source/storage/Entry.mc new file mode 100644 index 0000000..57e0309 --- /dev/null +++ b/source/storage/Entry.mc @@ -0,0 +1,149 @@ +// Author: cirakg & claude ai +// Summary: One wallet entry (an OTP account or a barcode card): its fields, limits, validation and wiping. +// +// The fields follow README 6.1. Header fields are readable without a key so +// the list draws without decrypting; the sensitive fields (algorithm, digits, +// period, secret, payload, options) only exist in memory after the vault has +// opened the record, and wipeSecrets() zeroes them again. +// +// An Entry that came from Vault.header() has secret == null and +// payload == null; one from Vault.open() has them filled in. + +import Toybox.Lang; + +class Entry { + + // kind + static const KIND_OTP = 0; + static const KIND_CODE = 1; + + // type, for OTP entries (code entries store their symbology, M6) + static const OTP_TOTP = 0; + static const OTP_HOTP = 1; + static const OTP_STEAM = 2; + + // source + static const SOURCE_MANUAL = 0; + static const SOURCE_IMPORT = 1; + + // ID alphabet: digits and capitals without 0, 1, I, L, O and U, so IDs + // are easy to read off the watch and type into the Manage form. + static const ID_ALPHABET = "23456789ABCDEFGHJKMNPQRSTVWXYZ"; // lint: allow-literal + static const ID_LENGTH = 4; + + // Limits, in UTF-8 bytes. They keep one record far below the 8 KB + // per-value limit of Application.Storage (README 6.2). + static const MAX_NAME_BYTES = 64; + static const MAX_GROUP_BYTES = 32; + static const MAX_SECRET_BYTES = 128; + static const MAX_PAYLOAD_BYTES = 4096; + static const MAX_SYMBOLOGY = 31; + + // Header fields. + var id as String = ""; + var kind as Number = KIND_OTP; + var type as Number = OTP_TOTP; + var name as String = ""; + var account as String = ""; + var group as String = ""; + var locked as Boolean = false; + var source as Number = SOURCE_MANUAL; + var favorite as Boolean = false; + var counter as Long = 0l; + var created as Long = 0l; + var namesEncrypted as Boolean = false; + + // Sensitive fields (encrypted at rest). + var algorithm as Number = Hashes.SHA1; + var digits as Number = 6; + var period as Number = 30; + var secret as ByteArray? = null; + var payload as ByteArray? = null; + var options as Number = 0; + + function initialize() { + } + + // True for a well-formed ID: ID_LENGTH characters from ID_ALPHABET. + static function isValidId(value as String) as Boolean { + if (value.length() != ID_LENGTH) { + return false; + } + var chars = value.toCharArray(); + for (var i = 0; i < chars.size(); i++) { + if (ID_ALPHABET.find(chars[i].toString()) == null) { + return false; + } + } + return true; + } + + // null when the entry can be stored, otherwise a short English reason. + // Checks the ID only when one is set (the vault assigns new IDs). + // Settings and import validate user input in more detail (M5, M7); this is + // the last line of defence before anything is written. + function validate() as String? { + if (id.length() > 0 && !isValidId(id)) { + return "bad id"; + } + if (Bytes.fromUtf8(name).size() > MAX_NAME_BYTES || Bytes.fromUtf8(account).size() > MAX_NAME_BYTES) { + return "name too long"; + } + if (Bytes.fromUtf8(group).size() > MAX_GROUP_BYTES) { + return "group too long"; + } + if (source != SOURCE_MANUAL && source != SOURCE_IMPORT) { + return "bad source"; + } + if (counter < 0) { + return "bad counter"; + } + if (kind == KIND_OTP) { + return validateOtp(); + } + if (kind == KIND_CODE) { + return validateCode(); + } + return "bad kind"; + } + + private function validateOtp() as String? { + if (type != OTP_TOTP && type != OTP_HOTP && type != OTP_STEAM) { + return "bad otp type"; + } + if (!Hashes.isValid(algorithm)) { + return "bad algorithm"; + } + if (digits < Otp.MIN_DIGITS || digits > Otp.MAX_DIGITS) { + return "bad digits"; + } + if (period < Otp.MIN_PERIOD || period > Otp.MAX_PERIOD) { + return "bad period"; + } + var s = secret; + if (s == null || s.size() == 0 || s.size() > MAX_SECRET_BYTES) { + return "bad secret"; + } + return null; + } + + private function validateCode() as String? { + if (type < 0 || type > MAX_SYMBOLOGY) { + return "bad symbology"; + } + var p = payload; + if (p == null || p.size() == 0 || p.size() > MAX_PAYLOAD_BYTES) { + return "bad payload"; + } + return null; + } + + // Zero the secret and payload in place and drop them. Call as soon as a + // code has been computed or a barcode encoded (README 7.8). + function wipeSecrets() as Void { + Bytes.wipe(secret); + Bytes.wipe(payload); + secret = null; + payload = null; + } +} |
