blockstack
Version:
The Blockstack Javascript library for authentication, identity, and storage.
347 lines • 12.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const elliptic_1 = require("elliptic");
const cryptoRandom_1 = require("./cryptoRandom");
const errors_1 = require("../errors");
const keys_1 = require("../keys");
const sha2Hash_1 = require("./sha2Hash");
const hmacSha256_1 = require("./hmacSha256");
const aesCipher_1 = require("./aesCipher");
const utils_1 = require("../utils");
const ecurve = new elliptic_1.ec('secp256k1');
/**
* @ignore
*/
function aes256CbcEncrypt(iv, key, plaintext) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const cipher = yield aesCipher_1.createCipher();
const result = yield cipher.encrypt('aes-256-cbc', key, iv, plaintext);
return result;
});
}
exports.aes256CbcEncrypt = aes256CbcEncrypt;
/**
* @ignore
*/
function aes256CbcDecrypt(iv, key, ciphertext) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const cipher = yield aesCipher_1.createCipher();
const result = yield cipher.decrypt('aes-256-cbc', key, iv, ciphertext);
return result;
});
}
/**
* @ignore
*/
function hmacSha256(key, content) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const hmacSha256 = yield hmacSha256_1.createHmacSha256();
return hmacSha256.digest(key, content);
});
}
/**
* @ignore
*/
function equalConstTime(b1, b2) {
if (b1.length !== b2.length) {
return false;
}
let res = 0;
for (let i = 0; i < b1.length; i++) {
res |= b1[i] ^ b2[i]; // jshint ignore:line
}
return res === 0;
}
/**
* @ignore
*/
function sharedSecretToKeys(sharedSecret) {
// generate mac and encryption key from shared secret
const hashedSecret = sha2Hash_1.hashSha512Sync(sharedSecret);
return {
encryptionKey: hashedSecret.slice(0, 32),
hmacKey: hashedSecret.slice(32)
};
}
/**
* Hex encodes a 32-byte BN.js instance.
* The result string is zero padded and always 64 characters in length.
* @ignore
*/
function getHexFromBN(bnInput) {
const hexOut = bnInput.toString('hex', 64);
if (hexOut.length === 64) {
return hexOut;
}
else if (hexOut.length < 64) {
// pad with leading zeros
// the padStart function would require node 9
const padding = '0'.repeat(64 - hexOut.length);
return `${padding}${hexOut}`;
}
else {
throw new Error('Generated a > 32-byte BN for encryption. Failing.');
}
}
exports.getHexFromBN = getHexFromBN;
/**
* Returns a big-endian encoded 32-byte BN.js instance.
* The result Buffer is zero padded and always 32 bytes in length.
* @ignore
*/
function getBufferFromBN(bnInput) {
const result = bnInput.toArrayLike(Buffer, 'be', 32);
if (result.byteLength !== 32) {
throw new Error('Generated a 32-byte BN for encryption. Failing.');
}
return result;
}
exports.getBufferFromBN = getBufferFromBN;
/**
* Get details about the JSON envelope size overhead for ciphertext payloads.
* @ignore
*/
function getCipherObjectWrapper(opts) {
// Placeholder structure of the ciphertext payload, used to determine the
// stringified JSON overhead length.
const shell = {
iv: '',
ephemeralPK: '',
mac: '',
cipherText: '',
wasString: !!opts.wasString,
};
if (opts.cipherTextEncoding === 'base64') {
shell.cipherTextEncoding = 'base64';
}
// Hex encoded 16 byte buffer.
const ivLength = 32;
// Hex encoded, compressed EC pubkey of 33 bytes.
const ephemeralPKLength = 66;
// Hex encoded 32 byte hmac-sha256.
const macLength = 64;
return {
payloadValuesLength: ivLength + ephemeralPKLength + macLength,
payloadShell: JSON.stringify(shell)
};
}
exports.getCipherObjectWrapper = getCipherObjectWrapper;
/**
* Get details about the JSON envelope size overhead for signed ciphertext payloads.
* @param payloadShell - The JSON stringified empty `CipherObject`
* @ignore
*/
function getSignedCipherObjectWrapper(payloadShell) {
// Placeholder structure of the signed ciphertext payload, used to determine the
// stringified JSON overhead length.
const shell = {
signature: '',
publicKey: '',
cipherText: payloadShell
};
// Hex encoded DER signature, up to 72 byte length.
const signatureLength = 144;
// Hex encoded 33 byte public key.
const publicKeyLength = 66;
return {
signedPayloadValuesLength: signatureLength + publicKeyLength,
signedPayloadShell: JSON.stringify(shell)
};
}
exports.getSignedCipherObjectWrapper = getSignedCipherObjectWrapper;
/**
* Fast function that determines the final ASCII string byte length of the
* JSON stringified ECIES encrypted payload.
* @ignore
*/
function eciesGetJsonStringLength(opts) {
const { payloadShell, payloadValuesLength } = getCipherObjectWrapper(opts);
// Calculate the AES output length given the input length.
const cipherTextLength = utils_1.getAesCbcOutputLength(opts.contentLength);
// Get the encoded string length of the cipherText.
let encodedCipherTextLength;
if (!opts.cipherTextEncoding || opts.cipherTextEncoding === 'hex') {
encodedCipherTextLength = (cipherTextLength * 2);
}
else if (opts.cipherTextEncoding === 'base64') {
encodedCipherTextLength = utils_1.getBase64OutputLength(cipherTextLength);
}
else {
throw new Error(`Unexpected cipherTextEncoding "${opts.cipherTextEncoding}"`);
}
if (!opts.sign) {
// Add the length of the JSON envelope, ciphertext length, and length of const values.
return payloadShell.length
+ payloadValuesLength
+ encodedCipherTextLength;
}
else {
// Get the signed version of the JSON envelope
const { signedPayloadShell, signedPayloadValuesLength } = getSignedCipherObjectWrapper(payloadShell);
// Add length of the JSON envelope, ciphertext length, and length of the const values.
return signedPayloadShell.length
+ signedPayloadValuesLength
+ payloadValuesLength
+ encodedCipherTextLength;
}
}
exports.eciesGetJsonStringLength = eciesGetJsonStringLength;
/**
* Encrypt content to elliptic curve publicKey using ECIES
* @param publicKey - secp256k1 public key hex string
* @param content - content to encrypt
* @return Object containing:
* iv (initialization vector, hex encoding),
* cipherText (cipher text either hex or base64 encoded),
* mac (message authentication code, hex encoded),
* ephemeral public key (hex encoded),
* wasString (boolean indicating with or not to return a buffer or string on decrypt)
* @private
* @ignore
*/
function encryptECIES(publicKey, content, wasString, cipherTextEncoding) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const ecPK = ecurve.keyFromPublic(publicKey, 'hex').getPublic();
const ephemeralSK = ecurve.genKeyPair();
const ephemeralPK = Buffer.from(ephemeralSK.getPublic().encodeCompressed());
const sharedSecret = ephemeralSK.derive(ecPK);
const sharedSecretBuffer = getBufferFromBN(sharedSecret);
const sharedKeys = sharedSecretToKeys(sharedSecretBuffer);
const initializationVector = cryptoRandom_1.randomBytes(16);
const cipherText = yield aes256CbcEncrypt(initializationVector, sharedKeys.encryptionKey, content);
const macData = Buffer.concat([initializationVector,
ephemeralPK,
cipherText]);
const mac = yield hmacSha256(sharedKeys.hmacKey, macData);
let cipherTextString;
if (!cipherTextEncoding || cipherTextEncoding === 'hex') {
cipherTextString = cipherText.toString('hex');
}
else if (cipherTextEncoding === 'base64') {
cipherTextString = cipherText.toString('base64');
}
else {
throw new Error(`Unexpected cipherTextEncoding "${cipherTextEncoding}"`);
}
const result = {
iv: initializationVector.toString('hex'),
ephemeralPK: ephemeralPK.toString('hex'),
cipherText: cipherTextString,
mac: mac.toString('hex'),
wasString: !!wasString
};
if (cipherTextEncoding && cipherTextEncoding !== 'hex') {
result.cipherTextEncoding = cipherTextEncoding;
}
return result;
});
}
exports.encryptECIES = encryptECIES;
/**
* Decrypt content encrypted using ECIES
* @param {String} privateKey - secp256k1 private key hex string
* @param {Object} cipherObject - object to decrypt, should contain:
* iv (initialization vector), cipherText (cipher text),
* mac (message authentication code), ephemeralPublicKey
* wasString (boolean indicating with or not to return a buffer or string on decrypt)
* @return {Buffer} plaintext
* @throws {FailedDecryptionError} if unable to decrypt
* @private
* @ignore
*/
function decryptECIES(privateKey, cipherObject) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const ecSK = ecurve.keyFromPrivate(privateKey, 'hex');
let ephemeralPK = null;
try {
ephemeralPK = ecurve.keyFromPublic(cipherObject.ephemeralPK, 'hex').getPublic();
}
catch (error) {
throw new errors_1.FailedDecryptionError('Unable to get public key from cipher object. '
+ 'You might be trying to decrypt an unencrypted object.');
}
const sharedSecret = ecSK.derive(ephemeralPK);
const sharedSecretBuffer = getBufferFromBN(sharedSecret);
const sharedKeys = sharedSecretToKeys(sharedSecretBuffer);
const ivBuffer = Buffer.from(cipherObject.iv, 'hex');
let cipherTextBuffer;
if (!cipherObject.cipherTextEncoding || cipherObject.cipherTextEncoding === 'hex') {
cipherTextBuffer = Buffer.from(cipherObject.cipherText, 'hex');
}
else if (cipherObject.cipherTextEncoding === 'base64') {
cipherTextBuffer = Buffer.from(cipherObject.cipherText, 'base64');
}
else {
throw new Error(`Unexpected cipherTextEncoding "${cipherObject.cipherText}"`);
}
const macData = Buffer.concat([ivBuffer,
Buffer.from(ephemeralPK.encodeCompressed()),
cipherTextBuffer]);
const actualMac = yield hmacSha256(sharedKeys.hmacKey, macData);
const expectedMac = Buffer.from(cipherObject.mac, 'hex');
if (!equalConstTime(expectedMac, actualMac)) {
throw new errors_1.FailedDecryptionError('Decryption failed: failure in MAC check');
}
const plainText = yield aes256CbcDecrypt(ivBuffer, sharedKeys.encryptionKey, cipherTextBuffer);
if (cipherObject.wasString) {
return plainText.toString();
}
else {
return plainText;
}
});
}
exports.decryptECIES = decryptECIES;
/**
* Sign content using ECDSA
*
* @param {String} privateKey - secp256k1 private key hex string
* @param {Object} content - content to sign
* @return {Object} contains:
* signature - Hex encoded DER signature
* public key - Hex encoded private string taken from privateKey
* @private
* @ignore
*/
function signECDSA(privateKey, content) {
const contentBuffer = content instanceof Buffer ? content : Buffer.from(content);
const ecPrivate = ecurve.keyFromPrivate(privateKey, 'hex');
const publicKey = keys_1.getPublicKeyFromPrivate(privateKey);
const contentHash = sha2Hash_1.hashSha256Sync(contentBuffer);
const signature = ecPrivate.sign(contentHash);
const signatureString = signature.toDER('hex');
return {
signature: signatureString,
publicKey
};
}
exports.signECDSA = signECDSA;
/**
* @ignore
*/
function getBuffer(content) {
if (content instanceof Buffer)
return content;
else if (content instanceof ArrayBuffer)
return Buffer.from(content);
else
return Buffer.from(content);
}
/**
* Verify content using ECDSA
* @param {String | Buffer} content - Content to verify was signed
* @param {String} publicKey - secp256k1 private key hex string
* @param {String} signature - Hex encoded DER signature
* @return {Boolean} returns true when signature matches publickey + content, false if not
* @private
* @ignore
*/
function verifyECDSA(content, publicKey, signature) {
const contentBuffer = getBuffer(content);
const ecPublic = ecurve.keyFromPublic(publicKey, 'hex');
const contentHash = sha2Hash_1.hashSha256Sync(contentBuffer);
return ecPublic.verify(contentHash, signature);
}
exports.verifyECDSA = verifyECDSA;
//# sourceMappingURL=ec.js.map