From 8b0892974d91aecfda007ef550dada59bbdca974 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 15 Sep 2020 16:47:20 +0200 Subject: [PATCH] prototype of getting from 4s passphrase to decrypted backup session --- prototypes/derive-keys.js | 361 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 prototypes/derive-keys.js diff --git a/prototypes/derive-keys.js b/prototypes/derive-keys.js new file mode 100644 index 00000000..358285fd --- /dev/null +++ b/prototypes/derive-keys.js @@ -0,0 +1,361 @@ +function subtleCryptoResult(promiseOrOp) { + if (promiseOrOp instanceof Promise) { + return promiseOrOp; + } else { + return new Promise((resolve, reject) => { + promiseOrOp.oncomplete = e => resolve(e.target.result); + promiseOrOp.onerror = e => reject(e); + }); + } +} + +class CryptoHMACDriver { + constructor(subtleCrypto) { + this._subtleCrypto = subtleCrypto; + } + /** + * [hmac description] + * @param {BufferSource} key + * @param {BufferSource} mac + * @param {BufferSource} data + * @param {HashName} hash + * @return {boolean} + */ + async verify(key, mac, data, hash) { + const hmacKey = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + key, + { + name: 'HMAC', + hash: {name: hashName(hash)}, + }, + false, + ['verify'], + )); + const isVerified = await subtleCryptoResult(this._subtleCrypto.verify( + {name: "HMAC"}, + hmacKey, + mac, + data, + )); + return isVerified; + } + + async compute(key, data, hash) { + const hmacKey = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + key, + { + name: 'HMAC', + hash: {name: hashName(hash)}, + }, + false, + ['sign'], + )); + const buffer = await subtleCryptoResult(this._subtleCrypto.sign( + {name: "HMAC"}, + hmacKey, + data, + )); + return new Uint8Array(buffer); + } +} + +const nwbo = (num, len) => { + const arr = new Uint8Array(len); + for(let i=0; i> ((len - i - 1)*8)); + return arr; +}; + +class CryptoLegacyDeriveDriver { + constructor(cryptoDriver) { + this._cryptoDriver = cryptoDriver; + } + + async pbkdf2(password, iterations, salt, hash, length) { + const dkLen = length / 8; + if (iterations <= 0) { + throw new Error('InvalidIterationCount'); + } + if (dkLen <= 0) { + throw new Error('InvalidDerivedKeyLength'); + } + const hLen = this._cryptoDriver.digestSize(hash); + if(dkLen > (Math.pow(2, 32) - 1) * hLen) throw new Error('DerivedKeyTooLong'); + + const l = Math.ceil(dkLen/hLen); + const r = dkLen - (l-1)*hLen; + + const funcF = async (i) => { + const seed = new Uint8Array(salt.length + 4); + seed.set(salt); + seed.set(nwbo(i+1, 4), salt.length); + let u = await this._cryptoDriver.hmac.compute(password, seed, hash); + let outputF = new Uint8Array(u); + for(let j = 1; j < iterations; j++){ + if ((j % 1000) === 0) { + console.log(j, j/iterations); + } + u = await this._cryptoDriver.hmac.compute(password, u, hash); + outputF = u.map( (elem, idx) => elem ^ outputF[idx]); + } + return {index: i, value: outputF}; + }; + + const Tis = []; + const DK = new Uint8Array(dkLen); + for(let i = 0; i < l; i++) { + Tis.push(funcF(i)); + } + const TisResolved = await Promise.all(Tis); + TisResolved.forEach(elem => { + if (elem.index !== l - 1) { + DK.set(elem.value, elem.index*hLen); + } + else { + DK.set(elem.value.slice(0, r), elem.index*hLen); + } + }); + + return DK; + } + + // based on https://github.com/junkurihara/jscu/blob/develop/packages/js-crypto-hkdf/src/hkdf.ts + async hkdf(key, salt, info, hash, length) { + length = length / 8; + const len = this._cryptoDriver.digestSize(hash); + + // RFC5869 Step 1 (Extract) + const prk = await this._cryptoDriver.hmac.compute(salt, key, hash); + + // RFC5869 Step 2 (Expand) + let t = new Uint8Array([]); + const okm = new Uint8Array(Math.ceil(length / len) * len); + for(let i = 0; i < Math.ceil(length / len); i++){ + const concat = new Uint8Array(t.length + info.length + 1); + concat.set(t); + concat.set(info, t.length); + concat.set(new Uint8Array([i+1]), t.length + info.length); + t = await this._cryptoDriver.hmac.compute(prk, concat, hash); + okm.set(t, len * i); + } + return okm.slice(0, length); + } +} + +class CryptoDeriveDriver { + constructor(subtleCrypto) { + this._subtleCrypto = subtleCrypto; + } + /** + * [pbkdf2 description] + * @param {BufferSource} password + * @param {Number} iterations + * @param {BufferSource} salt + * @param {HashName} hash + * @param {Number} length the desired length of the generated key, in bits (not bytes!) + * @return {BufferSource} + */ + async pbkdf2(password, iterations, salt, hash, length) { + const key = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + password, + {name: 'PBKDF2'}, + false, + ['deriveBits'], + )); + const keybits = await subtleCryptoResult(this._subtleCrypto.deriveBits( + { + name: 'PBKDF2', + salt, + iterations, + hash: hashName(hash), + }, + key, + length, + )); + return new Uint8Array(keybits); + } + + /** + * [hkdf description] + * @param {BufferSource} key [description] + * @param {BufferSource} salt [description] + * @param {BufferSource} info [description] + * @param {HashName} hash the hash to use + * @param {Number} length desired length of the generated key in bits (not bytes!) + * @return {[type]} [description] + */ + async hkdf(key, salt, info, hash, length) { + const hkdfkey = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + key, + {name: "HKDF"}, + false, + ["deriveBits"], + )); + const keybits = await subtleCryptoResult(this._subtleCrypto.deriveBits({ + name: "HKDF", + salt, + info, + hash: hashName(hash), + }, + hkdfkey, + length, + )); + return new Uint8Array(keybits); + } +} + +class CryptoAESDriver { + constructor(subtleCrypto) { + this._subtleCrypto = subtleCrypto; + } + /** + * [decrypt description] + * @param {BufferSource} key [description] + * @param {BufferSource} iv [description] + * @param {BufferSource} ciphertext [description] + * @return {BufferSource} [description] + */ + async decrypt(key, iv, ciphertext) { + const aesKey = await subtleCryptoResult(this._subtleCrypto.importKey( + 'raw', + key, + {name: 'AES-CTR'}, + false, + ['decrypt'], + )); + const plaintext = await subtleCryptoResult(this._subtleCrypto.decrypt( + // see https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams + { + name: "AES-CTR", + counter: iv, + length: 64, + }, + aesKey, + ciphertext, + )); + return new Uint8Array(plaintext); + } +} + +function hashName(name) { + if (name !== "SHA-256" && name !== "SHA-512") { + throw new Error(`Invalid hash name: ${name}`); + } + return name; +} + +export class CryptoDriver { + constructor(subtleCrypto) { + this.aes = new CryptoAESDriver(subtleCrypto); + //this.derive = new CryptoDeriveDriver(subtleCrypto); + this.derive = new CryptoLegacyDeriveDriver(this); + // subtleCrypto.deriveBits ? + // new CryptoDeriveDriver(subtleCrypto) : + // new CryptoLegacyDeriveDriver(this); + this.hmac = new CryptoHMACDriver(subtleCrypto); + this._subtleCrypto = subtleCrypto; + } + + /** + * [digest description] + * @param {HashName} hash + * @param {BufferSource} data + * @return {BufferSource} + */ + async digest(hash, data) { + return await subtleCryptoResult(this._subtleCrypto.digest(hashName(hash), data)); + } + + digestSize(hash) { + switch (hashName(hash)) { + case "SHA-512": return 64; + case "SHA-256": return 32; + default: throw new Error(`Not implemented for ${hashName(hash)}`); + } + } +} + +function decodeBase64(base64) { + const binStr = window.atob(base64); + const len = binStr.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binStr.charCodeAt(i); + } + return bytes; +} + +const DEFAULT_ITERATIONS = 500000; + +const DEFAULT_BITSIZE = 256; + +export async function deriveSSSSKey(cryptoDriver, passphrase, ssssKey) { + const textEncoder = new TextEncoder(); + return await cryptoDriver.derive.pbkdf2( + textEncoder.encode(passphrase), + ssssKey.content.passphrase.iterations || DEFAULT_ITERATIONS, + textEncoder.encode(ssssKey.content.passphrase.salt), + "SHA-512", + ssssKey.content.passphrase.bits || DEFAULT_BITSIZE); +} + +export async function decryptSecret(cryptoDriver, keyId, ssssKey, event) { + const textEncoder = new TextEncoder(); + const textDecoder = new TextDecoder(); + // now derive the aes and mac key from the 4s key + const hkdfKey = await cryptoDriver.derive.hkdf( + ssssKey, + new Uint8Array(8).buffer, //salt + textEncoder.encode(event.type), // info + "SHA-256", + 512 // 512 bits or 64 bytes + ); + const aesKey = hkdfKey.slice(0, 32); + const hmacKey = hkdfKey.slice(32); + + const data = event.content.encrypted[keyId]; + + const ciphertextBytes = decodeBase64(data.ciphertext); + const isVerified = await cryptoDriver.hmac.verify( + hmacKey, decodeBase64(data.mac), + ciphertextBytes, "SHA-256"); + + if (!isVerified) { + throw new Error("Bad MAC"); + } + + const plaintext = await cryptoDriver.aes.decrypt(aesKey, decodeBase64(data.iv), ciphertextBytes); + return textDecoder.decode(new Uint8Array(plaintext)); +} + + +export async function decryptSession(backupKeyBase64, backupInfo, sessionResponse) { + const privKey = decodeBase64(backupKeyBase64); + + const decryption = new window.Olm.PkDecryption(); + let backupPubKey; + try { + backupPubKey = decryption.init_with_private_key(privKey); + } catch (e) { + decryption.free(); + throw e; + } + + // If the pubkey computed from the private data we've been given + // doesn't match the one in the auth_data, the user has enetered + // a different recovery key / the wrong passphrase. + if (backupPubKey !== backupInfo.auth_data.public_key) { + console.log({backupPubKey}) + throw new Error("bad backup key"); + } + + const sessionInfo = decryption.decrypt( + sessionResponse.session_data.ephemeral, + sessionResponse.session_data.mac, + sessionResponse.session_data.ciphertext, + ); + return JSON.parse(sessionInfo); +}