aboutsummaryrefslogtreecommitdiff
path: root/source/otp/Base32.mc
blob: 9df2e7ca53b846ff3d8d1b58aae73987daa255d2 (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
// Author:  cirakg & claude ai
// Summary: Base32 (RFC 4648) decoding of OTP secrets with precise error positions, and encoding.
//
// Secrets arrive typed by hand or pasted, so decode() is lenient where it is
// safe: any letter case, spaces, tabs and hyphens are ignored, and "="
// padding is optional. Everything else fails with the 1-based position of
// the offending character in the original text, for the status messages in
// README 10.4. Leftover bits after the last full byte are ignored (many
// services emit secrets whose length isn't a multiple of 8 characters).
//
// Verified with RFC 4648 section 10 and seeded round trips
// (test/otp/Base32Test.mc).

import Toybox.Lang;

module Base32 {

    const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; // lint: allow-literal
    const PAD = 61; // '='

    // Why a decode failed: a message, and the 1-based character position (0
    // when the problem is the length rather than one character).
    class DecodeError {
        var position as Number;
        var message as String;

        function initialize(pos as Number, msg as String) {
            position = pos;
            message = msg;
        }
    }

    // Value 0-31 of one Base32 character code, -1 if it isn't one.
    function valueOf(code as Number) as Number {
        if (code >= 65 && code <= 90) { // A-Z
            return code - 65;
        }
        if (code >= 97 && code <= 122) { // a-z
            return code - 97;
        }
        if (code >= 50 && code <= 55) { // 2-7
            return code - 24;
        }
        return -1;
    }

    // True for the separators people type into secrets: space, tab, hyphen.
    function isIgnorable(code as Number) as Boolean {
        return code == 32 || code == 9 || code == 45;
    }

    // Bytes of a Base32 text, or a DecodeError.
    function decode(text as String) as ByteArray or DecodeError {
        var chars = text.toCharArray();
        var out = new [0]b;
        var buffer = 0;
        var bits = 0;
        var symbols = 0;
        var pads = 0;
        for (var i = 0; i < chars.size(); i++) {
            var code = chars[i].toNumber();
            if (isIgnorable(code)) {
                continue;
            }
            if (code == PAD) {
                pads++;
                continue;
            }
            var value = valueOf(code);
            if (value < 0) {
                return new DecodeError(i + 1, "'" + chars[i].toString() + "' is not a Base32 character");
            }
            if (pads > 0) {
                return new DecodeError(i + 1, "characters after '=' padding");
            }
            buffer = ((buffer << 5) | value) & 0xFFF;
            bits += 5;
            symbols++;
            if (bits >= 8) {
                bits -= 8;
                out.add((buffer >> bits) & 0xFF);
            }
        }
        var remainder = symbols % 8;
        if (remainder == 1 || remainder == 3 || remainder == 6) {
            return new DecodeError(0, "incomplete Base32 group: " + symbols + " characters can't be a whole number of bytes");
        }
        if (pads > 0 && (symbols + pads) % 8 != 0) {
            return new DecodeError(0, "wrong amount of '=' padding");
        }
        return out;
    }

    // RFC 4648 Base32 text with "=" padding.
    function encode(data as ByteArray) as String {
        var result = "";
        var buffer = 0;
        var bits = 0;
        for (var i = 0; i < data.size(); i++) {
            buffer = ((buffer << 8) | data[i]) & 0xFFFF;
            bits += 8;
            while (bits >= 5) {
                bits -= 5;
                result += ALPHABET.substring((buffer >> bits) & 31, ((buffer >> bits) & 31) + 1);
            }
        }
        if (bits > 0) {
            var index = (buffer << (5 - bits)) & 31;
            result += ALPHABET.substring(index, index + 1);
        }
        while (result.length() % 8 != 0) {
            result += "=";
        }
        return result;
    }
}