summaryrefslogtreecommitdiff
path: root/source/crypto/Aes.mc
blob: 9e4f32095d389fcf27f99b828c90ae03c6fdcb9f (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
// Author:  cirakg & claude ai
// Summary: AES-CBC encryption with PKCS#7 padding on top of the native Connect IQ cipher.
//
// The native Cipher offers AES-128 and AES-256 in ECB and CBC mode, without
// padding (CBC input must be a multiple of 16 bytes). This module adds PKCS#7
// padding. AES-256 is used for records (decided in M2: it works in the
// simulator on all four test devices and costs nothing extra; the PIN, not
// the cipher, limits the security of locked entries).
//
// Security note: CBC alone does not detect tampering. Records are always
// sealed with an HMAC as well (SealedBox.mc), and the MAC is checked before
// anything is decrypted, so padding errors never reach an attacker.
// Verified with NIST SP 800-38A F.2.1 and F.2.5 (test/crypto/AesTest.mc).

import Toybox.Cryptography;
import Toybox.Lang;

// (:glance): also loaded by the glance, which shows the favourite code (GlanceCode).
(:glance)
module Aes {

    const BLOCK_BYTES = 16;

    // A CBC cipher for a 16- or 32-byte key and a 16-byte IV.
    function cipher(key as ByteArray, iv as ByteArray) as Cryptography.Cipher {
        if ((key.size() != 16 && key.size() != 32) || iv.size() != BLOCK_BYTES) {
            throw new Lang.InvalidValueException("AES: key must be 16 or 32 bytes, IV 16 bytes");
        }
        return new Cryptography.Cipher({
            :algorithm => key.size() == 32 ? Cryptography.CIPHER_AES256 : Cryptography.CIPHER_AES128,
            :mode => Cryptography.MODE_CBC,
            :key => key,
            :iv => iv
        });
    }

    // CBC without padding; data must be a multiple of 16 bytes.
    function cbcRaw(key as ByteArray, iv as ByteArray, data as ByteArray, encrypt as Boolean) as ByteArray {
        var c = cipher(key, iv);
        return encrypt ? c.encrypt(data) : c.decrypt(data);
    }

    // Encrypt with PKCS#7 padding: 1 to 16 bytes, each holding the pad length.
    function cbcEncrypt(key as ByteArray, iv as ByteArray, plaintext as ByteArray) as ByteArray {
        return cbcEncryptKeep(key, iv, plaintext, null);
    }

    // As cbcEncrypt(); with `leftovers`, the padded copy is added there for
    // the caller to wipe (in steps, for kilobytes) instead of wiped here.
    function cbcEncryptKeep(key as ByteArray, iv as ByteArray, plaintext as ByteArray, leftovers as Array<ByteArray>?) as ByteArray {
        var padLength = BLOCK_BYTES - (plaintext.size() % BLOCK_BYTES);
        var padded = Bytes.copy(plaintext);
        for (var i = 0; i < padLength; i++) {
            padded.add(padLength);
        }
        var out = cbcRaw(key, iv, padded, true);
        if (leftovers != null) {
            leftovers.add(padded);
        } else {
            Bytes.wipe(padded);
        }
        return out;
    }

    // Decrypt and remove PKCS#7 padding; null when the length or the padding is
    // wrong.
    function cbcDecrypt(key as ByteArray, iv as ByteArray, ciphertext as ByteArray) as ByteArray? {
        return cbcDecryptKeep(key, iv, ciphertext, null);
    }

    // As cbcDecrypt(); with `leftovers`, the padded plaintext is added there.
    function cbcDecryptKeep(key as ByteArray, iv as ByteArray, ciphertext as ByteArray, leftovers as Array<ByteArray>?) as ByteArray? {
        var size = ciphertext.size();
        if (size == 0 || size % BLOCK_BYTES != 0) {
            return null;
        }
        var padded = cbcRaw(key, iv, ciphertext, false);
        var padLength = padded[size - 1];
        var valid = padLength >= 1 && padLength <= BLOCK_BYTES;
        if (valid) {
            for (var i = size - padLength; i < size; i++) {
                valid = valid && padded[i] == padLength;
            }
        }
        var plaintext = valid ? padded.slice(0, size - padLength) : null;
        if (leftovers != null) {
            leftovers.add(padded);
        } else {
            Bytes.wipe(padded);
        }
        return plaintext;
    }
}