Crypto & password generation refactor. Breaks restore compat to fix ugly math bug.
This commit is contained in:
159
passmate.js
159
passmate.js
@@ -18,9 +18,6 @@ class PassMate {
|
|||||||
this.PASSWORDS_PER_PAGE = 5;
|
this.PASSWORDS_PER_PAGE = 5;
|
||||||
this.VERSION = 0;
|
this.VERSION = 0;
|
||||||
|
|
||||||
// how much extra random data to generate, to handle set misses.
|
|
||||||
this.OVERSAMPLE = 4;
|
|
||||||
|
|
||||||
this.pages = [];
|
this.pages = [];
|
||||||
this.passwords = new Map();
|
this.passwords = new Map();
|
||||||
|
|
||||||
@@ -35,7 +32,7 @@ class PassMate {
|
|||||||
this.PAGES_PER_LETTER,
|
this.PAGES_PER_LETTER,
|
||||||
this.PASSWORDS_PER_PAGE);
|
this.PASSWORDS_PER_PAGE);
|
||||||
|
|
||||||
this.generateMasterPassword();
|
this.recoveryIn.innerText = this.SAFE_ALPHANUM.charAt(this.VERSION) + this.generateMasterPassword().join('');
|
||||||
this.onRecoveryChange();
|
this.onRecoveryChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,15 +179,6 @@ class PassMate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generatePassword(numChars, src) {
|
|
||||||
let ret = [];
|
|
||||||
let srcIndex = 0;
|
|
||||||
while (ret.length < numChars && srcIndex < src.length) {
|
|
||||||
ret.push.apply(ret, this.intToSafeChar(src[srcIndex++]));
|
|
||||||
}
|
|
||||||
return ret.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
validatePassword(password) {
|
validatePassword(password) {
|
||||||
if (password.length < this.PASSWORD_LENGTH) {
|
if (password.length < this.PASSWORD_LENGTH) {
|
||||||
return false;
|
return false;
|
||||||
@@ -212,26 +200,35 @@ class PassMate {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateMasterPassword() {
|
generatePassword(choices, oversample) {
|
||||||
|
oversample = oversample || 2;
|
||||||
if (this.recoveryIn.innerText != '') {
|
if (this.recoveryIn.innerText != '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let masterPassword = this.generatePassword(
|
let rand = Array.from(crypto.getRandomValues(new Uint8Array(choices.length * oversample)));
|
||||||
this.MASTER_PASSWORD_LENGTH,
|
let ret = [];
|
||||||
crypto.getRandomValues(new Uint8Array(this.MASTER_PASSWORD_LENGTH * this.OVERSAMPLE)));
|
for (let choice of choices) {
|
||||||
this.recoveryIn.innerText = this.SAFE_ALPHANUM.charAt(this.VERSION) + masterPassword;
|
let val = this.choose(choice, rand);
|
||||||
|
if (val === null) {
|
||||||
|
// Ran out of randomness. Try again.
|
||||||
|
return this.generatePassword(choices, oversample * 2);
|
||||||
|
}
|
||||||
|
ret.push(val);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateMasterPassword() {
|
||||||
|
let choices = new Array(this.MASTER_PASSWORD_LENGTH);
|
||||||
|
choices.fill(this.SAFE_ALPHANUM);
|
||||||
|
return this.generatePassword(choices);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecoveryChange() {
|
onRecoveryChange() {
|
||||||
let recovery = this.recoveryIn.innerText;
|
let recovery = this.recoveryIn.innerText;
|
||||||
if (recovery.charAt(0) == 'A') {
|
if (recovery.charAt(0) == 'A') {
|
||||||
this.recoveryOut.innerText = recovery;
|
this.recoveryOut.innerText = recovery;
|
||||||
crypto.subtle.importKey(
|
this.importKey(recovery.slice(1))
|
||||||
'raw',
|
|
||||||
this.stringToArray(recovery.slice(1)),
|
|
||||||
{name: 'HKDF'},
|
|
||||||
false,
|
|
||||||
['deriveBits'])
|
|
||||||
.then((key) => {
|
.then((key) => {
|
||||||
this.addDerivedPasswords(key);
|
this.addDerivedPasswords(key);
|
||||||
});
|
});
|
||||||
@@ -240,12 +237,75 @@ class PassMate {
|
|||||||
|
|
||||||
addDerivedPasswords(key) {
|
addDerivedPasswords(key) {
|
||||||
for (let [info, container] of this.passwords) {
|
for (let [info, container] of this.passwords) {
|
||||||
this.addDerivedPassword(key, info, container);
|
let choices = new Array(this.PASSWORD_LENGTH);
|
||||||
|
choices.fill(this.SAFE_ALPHANUM);
|
||||||
|
this.deriveValidArray(key, info, choices, this.validatePassword.bind(this))
|
||||||
|
.then((arr) => {
|
||||||
|
container.innerText = arr.join('');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addDerivedPassword(key, info, container) {
|
/**
|
||||||
crypto.subtle.deriveBits(
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
deriveValidArray(key, info, choices, validator) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.deriveArray(key, info, choices)
|
||||||
|
.then((arr) => {
|
||||||
|
if (validator(arr)) {
|
||||||
|
resolve(arr);
|
||||||
|
} else {
|
||||||
|
// Try again
|
||||||
|
resolve(this.deriveValidArray(key, info + 'x', choices, validator));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {CryptoKey} key Master key
|
||||||
|
* @param {string} info Seed for this generation. The same {key, info} input will generate the same output.
|
||||||
|
* @param {Array.<string[]>} choices Possible choices for each position in the output string
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
deriveArray(key, info, choices, oversample) {
|
||||||
|
oversample = oversample || 2;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.deriveUint8Array(key, info, choices.length * oversample)
|
||||||
|
.then((rand) => {
|
||||||
|
let ret = [];
|
||||||
|
for (let choice of choices) {
|
||||||
|
let val = this.choose(choice, rand);
|
||||||
|
if (val === null) {
|
||||||
|
// Ran out of randomness. Try again.
|
||||||
|
resolve(this.deriveArray(key, info, choices, oversample * 2));
|
||||||
|
}
|
||||||
|
ret.push(val);
|
||||||
|
}
|
||||||
|
resolve(ret);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This yields an Array instead of a Uint8Array because the latter lacks shift()
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
deriveUint8Array(key, info, numBytes) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.deriveBits(key, info, numBytes * 8)
|
||||||
|
.then((bits) => {
|
||||||
|
resolve(Array.from(new Uint8Array(bits)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
deriveBits(key, info, numBits) {
|
||||||
|
return crypto.subtle.deriveBits(
|
||||||
{
|
{
|
||||||
name: 'HKDF',
|
name: 'HKDF',
|
||||||
salt: new ArrayBuffer(),
|
salt: new ArrayBuffer(),
|
||||||
@@ -253,25 +313,38 @@ class PassMate {
|
|||||||
hash: {name: 'SHA-256'},
|
hash: {name: 'SHA-256'},
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
this.PASSWORD_LENGTH * this.OVERSAMPLE * 8 /* bits per byte */)
|
numBits);
|
||||||
.then((bits) => {
|
|
||||||
let password = this.generatePassword(this.PASSWORD_LENGTH, new Uint8Array(bits));
|
|
||||||
if (this.validatePassword(password)) {
|
|
||||||
container.innerText = password;
|
|
||||||
} else {
|
|
||||||
// Keep trying until we get a valid password.
|
|
||||||
this.addDerivedPassword(key, info + 'x', container);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
intToSafeChar(i) {
|
/**
|
||||||
i %= 0x3f;
|
* @returns {Promise}
|
||||||
if (i < this.SAFE_ALPHANUM.length) {
|
*/
|
||||||
return [this.SAFE_ALPHANUM[i]];
|
importKey(str) {
|
||||||
} else {
|
return crypto.subtle.importKey(
|
||||||
return [];
|
'raw',
|
||||||
|
this.stringToArray(str),
|
||||||
|
{name: 'HKDF'},
|
||||||
|
false,
|
||||||
|
['deriveBits']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uniform choice between options
|
||||||
|
* @param {Array} choice
|
||||||
|
* @param {Array} arr Randomness, consumed by this function
|
||||||
|
*/
|
||||||
|
choose(choice, arr) {
|
||||||
|
let mask = 1;
|
||||||
|
while (mask < choice.length) {
|
||||||
|
mask = (mask << 1) | 1;
|
||||||
}
|
}
|
||||||
|
while (arr.length) {
|
||||||
|
let rand = arr.shift() & mask;
|
||||||
|
if (rand < choice.length) {
|
||||||
|
return choice[rand];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
stringToArray(str) {
|
stringToArray(str) {
|
||||||
|
|||||||
5
test/canonical_values.txt
Normal file
5
test/canonical_values.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
A6Fe2bnneuTMUNEAVKQpjNQq74BQVG5JB
|
||||||
|
A-0-0: 7kt7QwYL
|
||||||
|
A-0-1: qhqv9Myy
|
||||||
|
A-1-0: MzyhW9Np
|
||||||
|
Z-1-4: 5EQaDfNS
|
||||||
Reference in New Issue
Block a user