blockstack
Version:
The Blockstack Javascript library for authentication, identity, and storage.
183 lines (165 loc) • 5.28 kB
text/typescript
import { validateMnemonic, mnemonicToEntropy, entropyToMnemonic } from 'bip39';
import { randomBytes, GetRandomBytes } from './cryptoRandom';
import { createSha2Hash } from './sha2Hash';
import { createHmacSha256 } from './hmacSha256';
import { createCipher } from './aesCipher';
import { createPbkdf2 } from './pbkdf2';
import { TriplesecDecryptSignature } from './cryptoUtils';
/**
* Encrypt a raw mnemonic phrase to be password protected
* @param {string} phrase - Raw mnemonic phrase
* @param {string} password - Password to encrypt mnemonic with
* @return {Promise<Buffer>} The encrypted phrase
* @private
* @ignore
* */
export async function encryptMnemonic(
phrase: string,
password: string,
opts?: {
getRandomBytes?: GetRandomBytes;
}
): Promise<Buffer> {
// hex encoded mnemonic string
let mnemonicEntropy: string;
try {
// must be bip39 mnemonic
mnemonicEntropy = mnemonicToEntropy(phrase);
} catch (error) {
console.error('Invalid mnemonic phrase provided');
console.error(error);
throw new Error('Not a valid bip39 mnemonic');
}
// normalize plaintext to fixed length byte string
const plaintextNormalized = Buffer.from(mnemonicEntropy, 'hex');
// AES-128-CBC with SHA256 HMAC
const pbkdf2 = await createPbkdf2();
let salt: Buffer;
if (opts && opts.getRandomBytes) {
salt = opts.getRandomBytes(16);
} else {
salt = randomBytes(16);
}
const keysAndIV = await pbkdf2.derive(password, salt, 100000, 48, 'sha512');
const encKey = keysAndIV.slice(0, 16);
const macKey = keysAndIV.slice(16, 32);
const iv = keysAndIV.slice(32, 48);
const cipher = await createCipher();
const cipherText = await cipher.encrypt(
'aes-128-cbc',
encKey,
iv,
plaintextNormalized
);
const hmacPayload = Buffer.concat([salt, cipherText]);
const hmacSha256 = await createHmacSha256();
const hmacDigest = await hmacSha256.digest(macKey, hmacPayload);
const payload = Buffer.concat([salt, hmacDigest, cipherText]);
return payload;
}
// Used to distinguish bad password during decrypt vs invalid format
class PasswordError extends Error {}
/**
* @ignore
*/
async function decryptMnemonicBuffer(
dataBuffer: Buffer,
password: string
): Promise<string> {
const salt = dataBuffer.slice(0, 16);
const hmacSig = dataBuffer.slice(16, 48); // 32 bytes
const cipherText = dataBuffer.slice(48);
const hmacPayload = Buffer.concat([salt, cipherText]);
const pbkdf2 = await createPbkdf2();
const keysAndIV = await pbkdf2.derive(password, salt, 100000, 48, 'sha512');
const encKey = keysAndIV.slice(0, 16);
const macKey = keysAndIV.slice(16, 32);
const iv = keysAndIV.slice(32, 48);
const decipher = await createCipher();
const decryptedResult = await decipher.decrypt(
'aes-128-cbc',
encKey,
iv,
cipherText
);
const hmacSha256 = await createHmacSha256();
const hmacDigest = await hmacSha256.digest(macKey, hmacPayload);
// hash both hmacSig and hmacDigest so string comparison time
// is uncorrelated to the ciphertext
const sha2Hash = await createSha2Hash();
const hmacSigHash = await sha2Hash.digest(hmacSig);
const hmacDigestHash = await sha2Hash.digest(hmacDigest);
if (!hmacSigHash.equals(hmacDigestHash)) {
// not authentic
throw new PasswordError('Wrong password (HMAC mismatch)');
}
let mnemonic: string;
try {
mnemonic = entropyToMnemonic(decryptedResult);
} catch (error) {
console.error('Error thrown by `entropyToMnemonic`');
console.error(error);
throw new PasswordError('Wrong password (invalid plaintext)');
}
if (!validateMnemonic(mnemonic)) {
throw new PasswordError('Wrong password (invalid plaintext)');
}
return mnemonic;
}
/**
* Decrypt legacy triplesec keys
* @param {Buffer} dataBuffer - The encrypted key
* @param {String} password - Password for data
* @return {Promise<Buffer>} Decrypted seed
* @private
* @ignore
*/
function decryptLegacy(
dataBuffer: Buffer,
password: string,
triplesecDecrypt: TriplesecDecryptSignature
): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
if (!triplesecDecrypt) {
reject(new Error('The `triplesec.decrypt` function must be provided'));
}
triplesecDecrypt(
{
key: Buffer.from(password),
data: dataBuffer
},
(err, plaintextBuffer) => {
if (!err) {
resolve(plaintextBuffer);
} else {
reject(err);
}
}
);
});
}
/**
* Decrypt an encrypted mnemonic phrase with a password.
* Legacy triplesec encrypted payloads are also supported.
* @param data - Buffer or hex-encoded string of the encrypted mnemonic
* @param password - Password for data
* @return the raw mnemonic phrase
* @private
* @ignore
*/
export async function decryptMnemonic(
data: string | Buffer,
password: string,
triplesecDecrypt?: TriplesecDecryptSignature
) {
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'hex');
try {
return await decryptMnemonicBuffer(dataBuffer, password);
} catch (err) {
if (err instanceof PasswordError) {
throw err;
}
const data = await decryptLegacy(dataBuffer, password, triplesecDecrypt);
return data.toString();
}
}