summaryrefslogtreecommitdiff
path: root/source/storage/Entry.mc
blob: 57e0309cd87ab61b0f57da1660b8711c9f8d5b95 (plain) (blame)
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
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;
    }
}