@toruslabs/eccrypto
Version:
JavaScript Elliptic curve cryptography library, includes fix to browser.js so that encrypt/decrypt works
215 lines (208 loc) • 8 kB
JavaScript
import { secp256k1 } from '@noble/curves/secp256k1.js';
import { concatBytes, bytesToNumberBE, equalBytes } from '@noble/curves/utils.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any, n/no-unsupported-features/node-builtins
const browserCrypto = globalThis.crypto || globalThis.msCrypto || {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any, n/no-unsupported-features/node-builtins
const subtle = browserCrypto.subtle || browserCrypto.webkitSubtle;
const SECP256K1_GROUP_ORDER = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
function assert(condition, message) {
if (!condition) {
throw new Error(message || "Assertion failed");
}
}
function isValidPrivateKey(privateKey) {
if (privateKey.length !== 32) {
return false;
}
const privateKeyBigInt = bytesToNumberBE(privateKey);
return privateKeyBigInt > 0n &&
// > 0
privateKeyBigInt < SECP256K1_GROUP_ORDER; // < G
}
/* This must check if we're in the browser or
not, since the functions are different and does
not convert using browserify */
function randomBytes(size) {
if (typeof browserCrypto.getRandomValues === "undefined") {
return browserCrypto.randomBytes(size);
}
const arr = new Uint8Array(size);
browserCrypto.getRandomValues(arr);
return arr;
}
async function sha512(msg) {
if (!browserCrypto.createHash) {
const hash = await subtle.digest("SHA-512", msg);
const result = new Uint8Array(hash);
return result;
}
const hash = browserCrypto.createHash("sha512");
const result = hash.update(msg).digest();
return new Uint8Array(result);
}
function getAes(op) {
return async function (iv, key, data) {
if (subtle && subtle[op] && subtle.importKey) {
const importAlgorithm = {
name: "AES-CBC"
};
const cryptoKey = await subtle.importKey("raw", key, importAlgorithm, false, [op]);
const encAlgorithm = {
name: "AES-CBC",
iv
};
// encrypt and decrypt ops are not implemented in react-native-quick-crypto yet.
const result = await subtle[op](encAlgorithm, cryptoKey, data);
return new Uint8Array(result);
} else if (op === "encrypt" && browserCrypto.createCipheriv) {
// This is available if crypto is polyfilled in react native environment
const cipher = browserCrypto.createCipheriv("aes-256-cbc", key, iv);
const firstChunk = cipher.update(data);
const secondChunk = cipher.final();
return concatBytes(firstChunk, secondChunk);
} else if (op === "decrypt" && browserCrypto.createDecipheriv) {
const decipher = browserCrypto.createDecipheriv("aes-256-cbc", key, iv);
const firstChunk = decipher.update(data);
const secondChunk = decipher.final();
return concatBytes(firstChunk, secondChunk);
}
throw new Error(`Unsupported operation: ${op}`);
};
}
const aesCbcEncrypt = getAes("encrypt");
const aesCbcDecrypt = getAes("decrypt");
async function hmacSha256Sign(key, msg) {
if (!browserCrypto.createHmac) {
const importAlgorithm = {
name: "HMAC",
hash: {
name: "SHA-256"
}
};
const cryptoKey = await subtle.importKey("raw", key, importAlgorithm, false, ["sign", "verify"]);
const sig = await subtle.sign("HMAC", cryptoKey, msg);
const result = new Uint8Array(sig);
return result;
}
const hmac = browserCrypto.createHmac("sha256", key);
hmac.update(msg);
const result = hmac.digest();
return result;
}
async function hmacSha256Verify(key, msg, sig) {
const expectedSig = await hmacSha256Sign(key, msg);
return equalBytes(expectedSig, sig);
}
function assertValidPrivateKey(privateKey) {
assert(isValidPrivateKey(privateKey), "Bad private key");
}
function assertValidPublicKey(publicKey) {
const isValid = secp256k1.utils.isValidPublicKey(publicKey, true) || secp256k1.utils.isValidPublicKey(publicKey, false);
assert(isValid, "Bad public key");
}
function assertValidMessage(msg) {
assert(msg.length > 0, "Message should not be empty");
assert(msg.length <= 32, "Message is too long");
}
/**
* Generate a new valid private key. Will use the window.crypto or window.msCrypto as source
* depending on your browser.
*/
const generatePrivate = function () {
let privateKey = randomBytes(32);
while (!isValidPrivateKey(privateKey)) {
privateKey = randomBytes(32);
}
return privateKey;
};
const getPublic = function (privateKey) {
assertValidPrivateKey(privateKey);
return secp256k1.getPublicKey(privateKey, false);
};
/**
* Get compressed version of public key.
*/
const getPublicCompressed = function (privateKey) {
assertValidPrivateKey(privateKey);
return secp256k1.getPublicKey(privateKey);
};
const sign = async function (privateKey, msg) {
assertValidPrivateKey(privateKey);
assertValidMessage(msg);
const sig = secp256k1.sign(msg, privateKey, {
prehash: false,
format: "der"
});
return sig;
};
const verify = async function (publicKey, msg, sig) {
assertValidPublicKey(publicKey);
assertValidMessage(msg);
if (secp256k1.verify(sig, msg, publicKey, {
prehash: false,
format: "der"
})) return null;
throw new Error("Bad signature");
};
const derive = async function (privateKeyA, publicKeyB) {
assertValidPrivateKey(privateKeyA);
assertValidPublicKey(publicKeyB);
// Strip leading zeros for backwards compatibility with older versions
// that used elliptic's BN.toArray() which didn't include leading zeros.
// Use derivePadded() if you need a fixed 32-byte output.
const sharedSecret = secp256k1.getSharedSecret(privateKeyA, publicKeyB);
const Px = sharedSecret.subarray(1);
const i = Px.findIndex(byte => byte !== 0);
return Px.subarray(i);
};
const deriveUnpadded = derive;
const derivePadded = async function (privateKeyA, publicKeyB) {
assertValidPrivateKey(privateKeyA);
assertValidPublicKey(publicKeyB);
const sharedSecret = secp256k1.getSharedSecret(privateKeyA, publicKeyB);
return sharedSecret.subarray(1);
};
const encrypt = async function (publicKeyTo, msg, opts) {
var _opts$padding;
opts = opts || {};
const padding = (_opts$padding = opts.padding) !== null && _opts$padding !== void 0 ? _opts$padding : true;
let ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32);
// There is a very unlikely possibility that it is not a valid key
while (!isValidPrivateKey(ephemPrivateKey)) {
ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32);
}
const ephemPublicKey = getPublic(ephemPrivateKey);
const deriveLocal = padding ? derivePadded : deriveUnpadded;
const Px = await deriveLocal(ephemPrivateKey, publicKeyTo);
const hash = await sha512(Px);
const iv = opts.iv || randomBytes(16);
const encryptionKey = hash.slice(0, 32);
const macKey = hash.slice(32);
const ciphertext = await aesCbcEncrypt(iv, encryptionKey, msg);
const dataToMac = concatBytes(iv, ephemPublicKey, ciphertext);
const mac = await hmacSha256Sign(macKey, dataToMac);
return {
iv,
ephemPublicKey,
ciphertext,
mac
};
};
const decrypt = async function (privateKey, opts, _padding) {
const padding = _padding !== null && _padding !== void 0 ? _padding : false;
const deriveLocal = padding ? derivePadded : deriveUnpadded;
const Px = await deriveLocal(privateKey, opts.ephemPublicKey);
const hash = await sha512(Px);
const encryptionKey = hash.slice(0, 32);
const macKey = hash.slice(32);
const dataToMac = concatBytes(opts.iv, opts.ephemPublicKey, opts.ciphertext);
const macGood = await hmacSha256Verify(macKey, dataToMac, opts.mac);
if (!macGood && padding === false) {
return decrypt(privateKey, opts, true);
} else if (!macGood && padding === true) {
throw new Error("bad MAC after trying padded");
}
const msg = await aesCbcDecrypt(opts.iv, encryptionKey, opts.ciphertext);
return msg;
};
export { decrypt, derive, derivePadded, deriveUnpadded, encrypt, generatePrivate, getPublic, getPublicCompressed, sign, verify };