|
@@ -1,21 +1,153 @@
|
|
|
-import jose from 'node-jose';
|
|
|
import { arrayToB64, b64ToArray } from './utils';
|
|
|
|
|
|
const encoder = new TextEncoder();
|
|
|
+const decoder = new TextDecoder();
|
|
|
|
|
|
-export async function prepareWrapKey(storage) {
|
|
|
- const keystore = jose.JWK.createKeyStore();
|
|
|
- const keypair = await keystore.generate('EC', 'P-256');
|
|
|
- storage.set('fxaWrapKey', JSON.stringify(keystore.toJSON(true)));
|
|
|
- return jose.util.base64url.encode(JSON.stringify(keypair.toJSON()));
|
|
|
+function getOtherInfo(enc) {
|
|
|
+ const name = encoder.encode(enc);
|
|
|
+ const length = 256;
|
|
|
+ const buffer = new ArrayBuffer(name.length + 16);
|
|
|
+ const dv = new DataView(buffer);
|
|
|
+ const result = new Uint8Array(buffer);
|
|
|
+ let i = 0;
|
|
|
+ dv.setUint32(i, name.length);
|
|
|
+ i += 4;
|
|
|
+ result.set(name, i);
|
|
|
+ i += name.length;
|
|
|
+ dv.setUint32(i, 0);
|
|
|
+ i += 4;
|
|
|
+ dv.setUint32(i, 0);
|
|
|
+ i += 4;
|
|
|
+ dv.setUint32(i, length);
|
|
|
+ return result;
|
|
|
}
|
|
|
|
|
|
-export async function getFileListKey(storage, bundle) {
|
|
|
- const keystore = await jose.JWK.asKeyStore(
|
|
|
- JSON.parse(storage.get('fxaWrapKey'))
|
|
|
+function concat(b1, b2) {
|
|
|
+ const result = new Uint8Array(b1.length + b2.length);
|
|
|
+ result.set(b1, 0);
|
|
|
+ result.set(b2, b1.length);
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+async function concatKdf(key, enc) {
|
|
|
+ if (key.length !== 32) {
|
|
|
+ throw new Error('unsupported key length');
|
|
|
+ }
|
|
|
+ const otherInfo = getOtherInfo(enc);
|
|
|
+ const buffer = new ArrayBuffer(4 + key.length + otherInfo.length);
|
|
|
+ const dv = new DataView(buffer);
|
|
|
+ const concat = new Uint8Array(buffer);
|
|
|
+ dv.setUint32(0, 1);
|
|
|
+ concat.set(key, 4);
|
|
|
+ concat.set(otherInfo, key.length + 4);
|
|
|
+ const result = await crypto.subtle.digest('SHA-256', concat);
|
|
|
+ return new Uint8Array(result);
|
|
|
+}
|
|
|
+
|
|
|
+export async function prepareScopedBundleKey(storage) {
|
|
|
+ const keys = await crypto.subtle.generateKey(
|
|
|
+ {
|
|
|
+ name: 'ECDH',
|
|
|
+ namedCurve: 'P-256'
|
|
|
+ },
|
|
|
+ true,
|
|
|
+ ['deriveBits']
|
|
|
+ );
|
|
|
+ const privateJwk = await crypto.subtle.exportKey('jwk', keys.privateKey);
|
|
|
+ const publicJwk = await crypto.subtle.exportKey('jwk', keys.publicKey);
|
|
|
+ const kid = await crypto.subtle.digest(
|
|
|
+ 'SHA-256',
|
|
|
+ encoder.encode(JSON.stringify(publicJwk))
|
|
|
+ );
|
|
|
+ privateJwk.kid = kid;
|
|
|
+ publicJwk.kid = kid;
|
|
|
+ storage.set('scopedBundlePrivateKey', JSON.stringify(privateJwk));
|
|
|
+ return arrayToB64(encoder.encode(JSON.stringify(publicJwk)));
|
|
|
+}
|
|
|
+
|
|
|
+export async function decryptBundle(storage, bundle) {
|
|
|
+ const privateJwk = JSON.parse(storage.get('scopedBundlePrivateKey'));
|
|
|
+ storage.remove('scopedBundlePrivateKey');
|
|
|
+ const privateKey = await crypto.subtle.importKey(
|
|
|
+ 'jwk',
|
|
|
+ privateJwk,
|
|
|
+ {
|
|
|
+ name: 'ECDH',
|
|
|
+ namedCurve: 'P-256'
|
|
|
+ },
|
|
|
+ false,
|
|
|
+ ['deriveBits']
|
|
|
+ );
|
|
|
+ const jweParts = bundle.split('.');
|
|
|
+ if (jweParts.length !== 5) {
|
|
|
+ throw new Error('invalid jwe');
|
|
|
+ }
|
|
|
+ const header = JSON.parse(decoder.decode(b64ToArray(jweParts[0])));
|
|
|
+ const additionalData = encoder.encode(jweParts[0]);
|
|
|
+ const iv = b64ToArray(jweParts[2]);
|
|
|
+ const ciphertext = b64ToArray(jweParts[3]);
|
|
|
+ const tag = b64ToArray(jweParts[4]);
|
|
|
+
|
|
|
+ if (header.alg !== 'ECDH-ES' || header.enc !== 'A256GCM') {
|
|
|
+ throw new Error('unsupported jwe type');
|
|
|
+ }
|
|
|
+
|
|
|
+ const publicKey = await crypto.subtle.importKey(
|
|
|
+ 'jwk',
|
|
|
+ header.epk,
|
|
|
+ {
|
|
|
+ name: 'ECDH',
|
|
|
+ namedCurve: 'P-256'
|
|
|
+ },
|
|
|
+ false,
|
|
|
+ []
|
|
|
);
|
|
|
- const result = await jose.JWE.createDecrypt(keystore).decrypt(bundle);
|
|
|
- const jwks = JSON.parse(jose.util.utf8.encode(result.plaintext));
|
|
|
+ const sharedBits = await crypto.subtle.deriveBits(
|
|
|
+ {
|
|
|
+ name: 'ECDH',
|
|
|
+ public: publicKey
|
|
|
+ },
|
|
|
+ privateKey,
|
|
|
+ 256
|
|
|
+ );
|
|
|
+
|
|
|
+ const rawSharedKey = await concatKdf(new Uint8Array(sharedBits), header.enc);
|
|
|
+ const sharedKey = await crypto.subtle.importKey(
|
|
|
+ 'raw',
|
|
|
+ rawSharedKey,
|
|
|
+ {
|
|
|
+ name: 'AES-GCM'
|
|
|
+ },
|
|
|
+ false,
|
|
|
+ ['decrypt']
|
|
|
+ );
|
|
|
+
|
|
|
+ const plaintext = await crypto.subtle.decrypt(
|
|
|
+ {
|
|
|
+ name: 'AES-GCM',
|
|
|
+ iv: iv,
|
|
|
+ additionalData: additionalData,
|
|
|
+ tagLength: tag.length * 8
|
|
|
+ },
|
|
|
+ sharedKey,
|
|
|
+ concat(ciphertext, tag)
|
|
|
+ );
|
|
|
+
|
|
|
+ return JSON.parse(decoder.decode(plaintext));
|
|
|
+}
|
|
|
+
|
|
|
+export async function preparePkce(storage) {
|
|
|
+ const verifier = arrayToB64(crypto.getRandomValues(new Uint8Array(64)));
|
|
|
+ storage.set('pkceVerifier', verifier);
|
|
|
+ const challenge = await crypto.subtle.digest(
|
|
|
+ 'SHA-256',
|
|
|
+ encoder.encode(verifier)
|
|
|
+ );
|
|
|
+ return arrayToB64(new Uint8Array(challenge));
|
|
|
+}
|
|
|
+
|
|
|
+export async function getFileListKey(storage, bundle) {
|
|
|
+ const jwks = await decryptBundle(storage, bundle);
|
|
|
const jwk = jwks['https://identity.mozilla.com/apps/send'];
|
|
|
const baseKey = await crypto.subtle.importKey(
|
|
|
'raw',
|