aladinnetwork-blockstack
Version:
The Aladin Javascript library for authentication, identity, and storage.
144 lines (123 loc) • 4.46 kB
text/typescript
import crypto from 'crypto'
import * as bip39 from 'bip39'
import triplesec from 'triplesec'
/**
* 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 function encryptMnemonic(phrase: string, password: string) {
return Promise.resolve().then(() => {
// must be bip39 mnemonic
if (!bip39.validateMnemonic(phrase)) {
throw new Error('Not a valid bip39 nmemonic')
}
// normalize plaintext to fixed length byte string
const plaintextNormalized = Buffer.from(
bip39.mnemonicToEntropy(phrase), 'hex'
)
// AES-128-CBC with SHA256 HMAC
const salt = crypto.randomBytes(16)
const keysAndIV = crypto.pbkdf2Sync(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 = crypto.createCipheriv('aes-128-cbc', encKey, iv)
let cipherText = cipher.update(plaintextNormalized).toString('hex')
cipherText += cipher.final().toString('hex')
const hmacPayload = Buffer.concat([salt, Buffer.from(cipherText, 'hex')])
const hmac = crypto.createHmac('sha256', macKey)
hmac.write(hmacPayload)
const hmacDigest = hmac.digest()
const payload = Buffer.concat([salt, hmacDigest, Buffer.from(cipherText, 'hex')])
return payload
})
}
// Used to distinguish bad password during decrypt vs invalid format
class PasswordError extends Error { }
/**
* @ignore
*/
function decryptMnemonicBuffer(dataBuffer: Buffer, password: string) {
return Promise.resolve().then(() => {
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 keysAndIV = crypto.pbkdf2Sync(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 = crypto.createDecipheriv('aes-128-cbc', encKey, iv)
let plaintext = decipher.update(cipherText).toString('hex')
plaintext += decipher.final().toString('hex')
const hmac = crypto.createHmac('sha256', macKey)
hmac.write(hmacPayload)
const hmacDigest = hmac.digest()
// hash both hmacSig and hmacDigest so string comparison time
// is uncorrelated to the ciphertext
const hmacSigHash = crypto.createHash('sha256')
.update(hmacSig)
.digest()
.toString('hex')
const hmacDigestHash = crypto.createHash('sha256')
.update(hmacDigest)
.digest()
.toString('hex')
if (hmacSigHash !== hmacDigestHash) {
// not authentic
throw new PasswordError('Wrong password (HMAC mismatch)')
}
const mnemonic = bip39.entropyToMnemonic(plaintext)
if (!bip39.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) {
return new Promise<Buffer>((resolve, reject) => {
triplesec.decrypt(
{
key: Buffer.from(password),
data: dataBuffer
},
(err, plaintextBuffer) => {
if (!err) {
resolve(plaintextBuffer)
} else {
reject(err)
}
}
)
})
}
/**
* Encrypt a raw mnemonic phrase with a password
* @param {string | Buffer} data - Buffer or hex-encoded string of the encrypted mnemonic
* @param {string} password - Password for data
* @return {Promise<string>} the raw mnemonic phrase
* @private
* @ignore
*/
export function decryptMnemonic(data: (string | Buffer), password: string): Promise<string> {
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'hex')
return decryptMnemonicBuffer(dataBuffer, password).catch((err) => {
// If it was a password error, don't even bother with legacy
if (err instanceof PasswordError) {
throw err
}
return decryptLegacy(dataBuffer, password).then(data => data.toString())
})
}