prototype of getting from 4s passphrase to decrypted backup session
This commit is contained in:
parent
2f2a2ee73f
commit
8b0892974d
1 changed files with 361 additions and 0 deletions
361
prototypes/derive-keys.js
vendored
Normal file
361
prototypes/derive-keys.js
vendored
Normal file
|
@ -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++) arr[i] = 0xFF && (num >> ((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);
|
||||
}
|
Reference in a new issue