summaryrefslogtreecommitdiff
path: root/web/js/crypto.js
blob: 5c12a2cb04dd033791aaaf02ed4950a72b859481 (plain)
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
'use strict';

// taken from fxa
function hexToUint8(str) {
    const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/;
    if (!HEX_STRING.test(str)) {
        throw new Error(`invalid hex string: ${str}`);
    }
    const bytes = str.match(/[a-fA-F0-9]{2}/g);
    return new Uint8Array(bytes.map((byte) => parseInt(byte, 16)));
}
function uint8ToHex(array) {
    return array.reduce((str, byte) => str + ('00' + byte.toString(16)).slice(-2), '');
}
function uint8ToBase64(array) {
    return btoa(String.fromCharCode(...array));
}
function uint8ToBase64Url(array) {
    return uint8ToBase64(array).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}

export function urlatob(s) {
    return atob(s.replace(/-/g, '+').replace(/_/g, "/"));
}

export async function deriveScopedKey(kB, uid, identifier, key_rotation_secret, key_rotation_timestamp) {
    async function importKey(k) {
        return await crypto.subtle.importKey('raw', hexToUint8(k), 'HKDF', false, ['deriveBits']);
    }

    async function derive(salt, k, info, len) {
        let params = {
            name: 'HKDF',
            salt,
            info: new TextEncoder().encode(info),
            hash: 'SHA-256',
        };
        return new Uint8Array(await crypto.subtle.deriveBits(params, await importKey(k), len * 8));
    }

    if (identifier == "https://identity.mozilla.com/apps/oldsync") {
        const bits = await derive(
            new Uint8Array(),
            kB,
            "identity.mozilla.com/picl/v1/oldsync",
            64);
        const kHash = await crypto.subtle.digest("SHA-256", hexToUint8(kB));
        const kid = key_rotation_timestamp.toString() + "-" +
              uint8ToBase64Url(new Uint8Array(kHash.slice(0, 16)));
        return {
            k: uint8ToBase64Url(bits),
            kty: "oct",
            kid,
            scope: identifier,
        };
    } else {
        const bits = await derive(
            hexToUint8(uid),
            kB + key_rotation_secret,
            "identity.mozilla.com/picl/v1/scoped_key\n" + identifier,
            16 + 32);
        const fp = new Uint8Array(bits.slice(0, 16));
        const key = new Uint8Array(bits.slice(16));
        const kid = key_rotation_timestamp.toString() + "-" + uint8ToBase64Url(fp);
        return {
            k: uint8ToBase64Url(key),
            kty: "oct",
            kid,
            scope: identifier,
        };
    }
}

export async function encryptScopedKeys(bundle, to, local_key, iv) {
    let encode = s => new TextEncoder().encode(s);

    let peer_key = await crypto.subtle.importKey(
        "jwk",
        JSON.parse(urlatob(to)),
        { name: "ECDH", namedCurve: "P-256" },
        false,
        ['deriveKey']);
    local_key = local_key || await crypto.subtle.generateKey(
        { name: "ECDH", namedCurve: "P-256" },
        true,
        ['deriveKey']);
    iv = iv || new Uint8Array(await crypto.subtle.exportKey(
        "raw",
        await crypto.subtle.generateKey({ name: "AES-CBC", length: 128 }, true, ['encrypt'])
    )).slice(0, 12);

    let key =  await crypto.subtle.deriveKey(
        { name: "ECDH", public: peer_key },
        local_key.privateKey,
        { name: "AES-GCM", length: 256 },
        true,
        ['encrypt']);
    key = new Uint8Array(await crypto.subtle.exportKey("raw", key));
    key = await crypto.subtle.digest(
        "SHA-256",
        new Uint8Array([
            0, 0, 0, 1, // rounds
            ...key,
            0, 0, 0, 7, ...encode("A256GCM"),
            0, 0, 0, 0, // apu
            0, 0, 0, 0, // apv
            0, 0, 1, 0, // key size
        ]));
    key = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ['encrypt']);

    let exported_key = await crypto.subtle.exportKey("jwk", local_key.publicKey);
    let header = {
        "alg": "ECDH-ES",
        "enc": "A256GCM",
        "epk": {
            crv: "P-256",
            kty: "EC",
            x: exported_key.x,
            y: exported_key.y
        }
    };

    let ciphered = await crypto.subtle.encrypt(
        { name: "AES-GCM", iv, additionalData: encode(uint8ToBase64Url(encode(JSON.stringify(header)))) },
        key,
        encode(JSON.stringify(bundle)));
    let tag = ciphered.slice(-16);
    ciphered = ciphered.slice(0, -16);

    return (uint8ToBase64Url(encode(JSON.stringify(header))) +
            ".." +
            uint8ToBase64Url(new Uint8Array(iv)) +
            "." +
            uint8ToBase64Url(new Uint8Array(ciphered)) +
            "." +
            uint8ToBase64Url(new Uint8Array(tag)));
}