UNPKG

@toruslabs/eccrypto

Version:

JavaScript Elliptic curve cryptography library, includes fix to browser.js so that encrypt/decrypt works

215 lines (208 loc) 8 kB
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 };