UNPKG

micro-rsa-dsa-dh

Version:

Minimal implementation of older cryptography algorithms: RSA, DSA, DH, ElGamal

458 lines 19.5 kB
import { sha1 } from '@noble/hashes/legacy.js'; import { sha224, sha256, sha384, sha512, sha512_224, sha512_256 } from '@noble/hashes/sha2.js'; import { sha3_224, sha3_256, sha3_384, sha3_512, shake128, shake256 } from '@noble/hashes/sha3.js'; import { concatBytes, createView, hexToBytes, randomBytes } from '@noble/hashes/utils.js'; import { isProbablePrimeRSA } from "./primality.js"; import { I2OSP, OS2IP, ensureBytes, gcd, invert, pow, randomBits, sqrt, } from "./utils.js"; const hashOutputLen = (hash, dkLen) => { const res = (msg) => hash(msg, { dkLen }); res.outputLen = dkLen; res.blockLen = hash.blockLen; res.create = () => hash.create({ dkLen }); return res; }; /** * Generate the RSA primes p and q according to the FIPS 186-5 standard (A.1.3 Generation of Random Primes that are Probably Prime) * @param nlen - Bit length of the modulus. * @param e - Public exponent. Must be an odd positive integer * @param a - Optional parameter for p ≡ a mod 8. * @param b - Optional parameter for q ≡ b mod 8. */ export function IFCPrimes(nlen, e = 65537n, a, b, randFn = randomBytes) { if (nlen % 8 !== 0) throw new Error(`expected bit length aligned to byte boundary, got ${nlen}`); if (nlen < 2048) throw new Error(`wrong nlen=${nlen}, expected at least 2048`); // Step 1: Check nlen if (e <= 2n ** 16n || e >= 2n ** 256n || e % 2n === 0n) throw new Error(`Wrong public exponent e=${e}`); // Step 2: Check e const limit = sqrt(1n << BigInt(nlen - 1)); // Step 4: Generate p for (let i = 0; i < 5 * nlen; i++) { // Step 4.1 and Step 4.7 let p = randomBits(nlen / 2); // Step 4.2 if (a !== undefined) p += BigInt((a - Number(p % 8n)) % 8); // Step 4.3 else if (p % 2n === 0n) p += 1n; // Step 4.3 if (p < limit) continue; // Step 4.4 if (gcd(p - 1n, e) === 1n) { // Step 4.5 if (isProbablePrimeRSA(p, randFn)) { // Step 4.5.1 and Step 4.5.2 // Proceed to Step 5 if p is probably prime for (let j = 0; j < 10 * nlen; j++) { let q = randomBits(nlen / 2); // Step 5.2 if (b !== undefined) q += BigInt((b - Number(q % 8n)) % 8); // Step 5.3 else if (q % 2n === 0n) q += 1n; // Step 5.3 if (q < limit) continue; // Step 5.4 let distance = p - q; if (distance < 0n) distance = -distance; if (distance <= 2n ** ((BigInt(nlen) >> 1n) - 100n)) continue; // Step 5.5 if (gcd(q - 1n, e) === 1n && isProbablePrimeRSA(q, randFn)) return { p, q }; // Step 5.6 } throw new Error('failed to find q after max iterations'); } } } throw new Error('failed to find p after max iterations'); } // Compares 2 u8a-s in kinda constant time function equalBytes(a, b) { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; return diff === 0; } // Takes Hash, returns VarLenHash export function mgf1(hash) { // From noble-post-quantum const counterB = new Uint8Array(4); const counterV = createView(counterB); return (msg, opts) => { const { dkLen } = opts; const out = new Uint8Array(Math.ceil(dkLen / hash.outputLen) * hash.outputLen); if (dkLen > 2 ** 32) throw new Error('mask too long'); for (let counter = 0, o = out; o.length; counter++) { counterV.setUint32(0, counter, false); hash.create().update(msg).update(counterB).digestInto(o); o = o.subarray(hash.outputLen); } out.subarray(dkLen).fill(0); return out.subarray(0, dkLen); }; } const validatePublicKey = (key) => { if (key === null || typeof key !== 'object' || typeof key.n !== 'bigint' || typeof key.e !== 'bigint') throw new Error('wrong private key'); }; const validatePrivateKey = (key) => { if (key === null || typeof key !== 'object' || typeof key.n !== 'bigint' || typeof key.d !== 'bigint') throw new Error('wrong private key'); }; /** * RSA Encryption Primitive (RSAEP) * * @param publicKey - An object containing RSA public key components. * @param m - The message representative. * @returns The ciphertext representative. */ function RSAEP(publicKey, m) { const { n, e } = publicKey; if (m < 0n || m >= n) throw new Error('message representative out of range'); return pow(m, e, n); // c = m^e mod n } /** * RSA Decryption Primitive (RSADP) * * @param privateKey - An object containing RSA private key components. * @param c - The ciphertext representative. * @returns The message representative. * @throws Will throw an error if the ciphertext representative is out of range. */ function RSADP(privateKey, c) { const { n } = privateKey; if (c < 0n || c >= n) throw new Error('ciphertext representative out of range'); // Step 1 return pow(c, privateKey.d, n); // m = c^d mod n } /** * RSA Signature Primitive (RSASP1) * * @param privateKey - An object containing RSA private key components. * @param m - The message representative. * @returns The signature representative. */ function RSASP1(privateKey, m) { const { n } = privateKey; // Step 1: Check if m is between 0 and n - 1 if (m < 0n || m >= n) throw new Error('message representative out of range'); // Step 1 return pow(m, privateKey.d, n); // s = m^d mod n } /** * RSAVP1 * * RSA Verification Primitive. * * @param publicKey - RSA public key containing modulus (n) and exponent (e) * @param s - Signature representative, an integer between 0 and n - 1 * @returns Message representative, an integer between 0 and n - 1 */ function RSAVP1(publicKey, s) { const { n, e } = publicKey; if (s < 0n || s >= n) return false; // Step 1 return pow(s, e, n); // Step 2 } // Exported API /** * Generates an RSA key pair. * * This function generates an RSA key pair using the given prime numbers `p` and `q`, and the public exponent `e`. * Output: * * @param p - A prime number. * @param q - A prime number. * @param e - The public exponent. * @returns An object containing the public key and the private key. */ export function keygen(nlen, e = 0x10001n, randFn = randomBytes) { if (!Number.isSafeInteger(nlen) || nlen <= 0) throw new Error('wrong nlen'); const { p, q } = IFCPrimes(nlen, e, undefined, undefined, randFn); const n = p * q; const phi = (p - 1n) * (q - 1n); const d = invert(e, phi); return { publicKey: { e, n }, privateKey: { d, n } }; } /** * improved ES; based on the optimal asymmetric encryption padding * @param hash * @param mgfHash * @param label optional label to be associated with the message */ export const OAEP = (hash, mgfHash, label = Uint8Array.of()) => ({ encrypt(publicKey, M) { validatePublicKey(publicKey); const { n } = publicKey; const k = Math.ceil(n.toString(16).length / 2); const mLen = M.length; if (mLen > k - 2 * hash.outputLen - 2) throw new Error('message too long'); const lHash = hash(label); // Step 2a const PS = new Uint8Array(k - mLen - 2 * hash.outputLen - 2); // Step 2b const DB = concatBytes(lHash, PS, new Uint8Array([0x01]), M); // Step 2c: DB = lHash || PS || 0x01 || M const seed = randomBytes(hash.outputLen); // Step 2d const dbMask = mgfHash(seed, { dkLen: k - hash.outputLen - 1 }); // Step 2e const maskedDB = DB.map((byte, idx) => byte ^ dbMask[idx]); // Step 2f const seedMask = mgfHash(maskedDB, { dkLen: hash.outputLen }); // Step 2g const maskedSeed = seed.map((byte, idx) => byte ^ seedMask[idx]); // Step 2h const EM = concatBytes(new Uint8Array([0x00]), maskedSeed, maskedDB); // Step 2i const m = OS2IP(EM); // Step 3a const c = RSAEP(publicKey, m); // Step 3b return I2OSP(c, k); // Step 3c }, decrypt(privateKey, C) { validatePrivateKey(privateKey); const { n } = privateKey; const k = Math.ceil(n.toString(16).length / 2); // Length of the RSA modulus in bytes if (C.length !== k) throw new Error('incorrect ciphertext length'); if (k < 2 * hash.outputLen + 2) throw new Error('RSA modulus too short'); const c = OS2IP(C); // Step 2a const m = RSADP(privateKey, c); // Step 2b const EM = I2OSP(m, k); // Step 2c const lHash = hash(label); // Step 3a // Step 3b const Y = EM[0]; const maskedSeed = EM.subarray(1, 1 + hash.outputLen); const maskedDB = EM.subarray(1 + hash.outputLen); const seedMask = mgfHash(maskedDB, { dkLen: hash.outputLen }); // Step 3c const seed = maskedSeed.map((byte, idx) => byte ^ seedMask[idx]); // Step 3d const dbMask = mgfHash(seed, { dkLen: k - hash.outputLen - 1 }); // Step 3e const DB = maskedDB.map((byte, idx) => byte ^ dbMask[idx]); // Step 3f const lHashPrime = DB.subarray(0, hash.outputLen); // Step 3g const rest = DB.subarray(hash.outputLen); let idx = rest.indexOf(0x01); if (idx === -1 || !equalBytes(lHash, lHashPrime) || Y !== 0x00) throw new Error('decryption error'); // PS should be zeros for (let i = 0; i < idx; i++) if (rest[i] !== 0) throw new Error('decryption error'); return rest.subarray(idx + 1); }, }); function fixShake(hash) { // TODO: find better solution. // Problem is that spec requires different outputLen for shake, so we patch it here if (hash !== shake128 && hash !== shake256) return hash; const dkLen = hash === shake128 ? 32 : 64; return hashOutputLen(hash, dkLen); } function EMSA_PSS_ENCODE(M, emBits, opts) { let { hash, mgfHash, sLen } = opts; hash = fixShake(hash); const emLen = Math.ceil(emBits / 8); const mHash = hash(M); // Step 2 if (emLen < hash.outputLen + sLen + 2) throw new Error('encoding error'); // Step 3 const salt = sLen === 0 ? Uint8Array.of() : randomBytes(sLen); // Step 4 // Step 5: Let M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt const Mprime = concatBytes(new Uint8Array(8), mHash, salt); // Step 5 const H = hash(Mprime); // Step 6 const PS = new Uint8Array(emLen - sLen - hash.outputLen - 2); // Step 7 const DB = concatBytes(PS, new Uint8Array([0x01]), salt); // Step 8: DB = PS || 0x01 || salt const dbMask = mgfHash(H, { dkLen: emLen - hash.outputLen - 1 }); // Step 9 const maskedDB = DB.map((byte, idx) => byte ^ dbMask[idx]); // Step 10 const leftmostBits = 8 * emLen - emBits; // Step 11 maskedDB[0] &= 0xff >> leftmostBits; return concatBytes(maskedDB, H, new Uint8Array([0xbc])); // Step 12: EM = maskedDB || H || 0xbc } function EMSA_PSS_VERIFY(M, EM, emBits, opts) { let { hash, mgfHash, sLen } = opts; hash = fixShake(hash); const emLen = Math.ceil(emBits / 8); const mHash = hash(M); // Step 2 if (emLen < hash.outputLen + sLen + 2) return false; // Step 3 if (EM[EM.length - 1] !== 0xbc) return false; // Step 4 const maskedDB = EM.subarray(0, emLen - hash.outputLen - 1); // Step 5 const H = EM.subarray(emLen - hash.outputLen - 1, emLen - 1); // Step 5 // Step 6: Check the leftmost bits of maskedDB const leftmostBits = 8 * emLen - emBits; if (maskedDB[0] >> (8 - leftmostBits) !== 0) return false; const dbMask = mgfHash(H, { dkLen: emLen - hash.outputLen - 1 }); // Step 7 const DB = maskedDB.map((byte, idx) => byte ^ dbMask[idx]); // Step 8 DB[0] &= 0xff >> leftmostBits; // Step 9 // Step 10: Check the leftmost octets and the 0x01 separator const psLen = emLen - hash.outputLen - sLen - 2; for (let i = 0; i < psLen; i++) if (DB[i] !== 0x00) return false; if (DB[psLen] !== 0x01) return false; const salt = sLen > 0 ? DB.subarray(-sLen) : new Uint8Array(0); // Step 11 const Mprime = concatBytes(new Uint8Array(8), mHash, salt); // Step 12: M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt const Hprime = hash(Mprime); // Step 13 return equalBytes(H, Hprime); // Step 14: Compare H and H' } /** * EMSA-PSS: improved EMSA, based on the probabilistic signature scheme * @param opts * @returns */ export const PSS = (hash, mgfHash, sLen = 0) => ({ sign(privateKey, M) { validatePrivateKey(privateKey); M = ensureBytes('message', M); const { n, d } = privateKey; const emBits = n.toString(2).length - 1; const EM = EMSA_PSS_ENCODE(M, emBits, { hash, mgfHash, sLen }); const emLen = Math.ceil(emBits / 8); const m = OS2IP(EM); // Step 2a const s = RSASP1({ n, d }, m); // Step 2b return I2OSP(s, emLen); // Step 2c }, verify(publicKey, M, S) { validatePublicKey(publicKey); M = ensureBytes('message', M); S = ensureBytes('signature', S); const { n, e } = publicKey; const k = Math.ceil(n.toString(16).length / 2); const emBits = n.toString(2).length - 1; const emLen = Math.ceil(emBits / 8); if (S.length !== k) return false; // Step 1 const s = OS2IP(S); // Step 2a const m = RSAVP1({ n, e }, s); // Step 2b if (m === false) return false; const EM = I2OSP(m, emLen); // Step 2c if (EM.length !== emLen) return false; return EMSA_PSS_VERIFY(M, EM, emBits, { hash, mgfHash, sLen }); // Step 3 }, }); // RSAES-PKCS1-v1_5 /** * EMSA-PKCS1-v1_5-ENCODE function * * @param M - Message to be encoded. * @param emLen - Intended length in octets of the encoded message. * @param hash - Hash function to be used. * @returns Encoded message. * @throws Will throw an error if the message is too long or intended encoded message length is too short. */ function EMSA_PKCS1_V1_5_ENCODE(hash, prefix, M, emLen) { const H = hash(M); const T = concatBytes(hexToBytes(prefix), H); const tLen = T.length; if (emLen < tLen + 11) throw new Error('intended encoded message length too short'); const PS = new Uint8Array(emLen - tLen - 3).fill(0xff); // Step 4 return concatBytes(new Uint8Array([0x00, 0x01]), PS, new Uint8Array([0x00]), T); // Step 5 } /** * RSAES-PKCS1-v1_5: older Encryption/decryption Scheme (ES) as first standardized in version 1.5 of PKCS #1. Known-vulnerable. */ export const PKCS1_KEM = { encrypt(publicKey, M) { validatePublicKey(publicKey); M = ensureBytes('message', M); const { n } = publicKey; const k = Math.ceil(n.toString(16).length / 2); // Length of the RSA modulus in bytes const mLen = M.length; if (mLen > k - 11) throw new Error('message too long'); // Step 1 const psLen = k - mLen - 3; const PS = new Uint8Array(psLen); // Step 2a for (let i = 0; i < psLen; i++) { let rnd = 0; while (rnd === 0) rnd = randomBytes(1)[0]; PS[i] = rnd; } const EM = concatBytes(new Uint8Array([0x00, 0x02]), PS, new Uint8Array([0x00]), M); // Step 2b const m = OS2IP(EM); // Step 3a const c = RSAEP(publicKey, m); // Step 3b return I2OSP(c, k); // Step 3c }, decrypt(privateKey, C) { validatePrivateKey(privateKey); C = ensureBytes('ciphertext', C); const { n } = privateKey; const k = Math.ceil(n.toString(16).length / 2); if (C.length !== k || k < 11) throw new Error('decryption error'); // Step 1 const c = OS2IP(C); // Step 2a const m = RSADP(privateKey, c); // Step 2b if (m >= n) throw new Error('decryption error'); const EM = I2OSP(m, k); // Step 2c // Step 3: EME-PKCS1-v1_5 decoding if (EM[0] !== 0x00 || EM[1] !== 0x02) throw new Error('decryption error'); // Find the position of the 0x00 byte that separates PS from M let sepIdx = -1; for (let i = 2; i < EM.length; i++) { if (EM[i] === 0x00) { sepIdx = i; break; } } // PS length must be at least 8 octets if (sepIdx === -1 || sepIdx < 10) throw new Error('decryption error'); return EM.subarray(sepIdx + 1); // Step 4 }, }; /** * RSASSA-PKCS1-v1_5: old Signature Scheme with Appendix (SSA) as first standardized in version 1.5 of PKCS #1. */ const PKCS1 = (hash, prefix) => ({ verify(publicKey, M, S) { validatePublicKey(publicKey); M = ensureBytes('message', M); S = ensureBytes('signature', S); const { n, e } = publicKey; const k = Math.ceil(n.toString(16).length / 2); if (S.length !== k) return false; // Step 1 const s = OS2IP(S); // Step 2a const m = RSAVP1({ n, e }, s); // Step 2b if (m === false) return false; const EM = I2OSP(m, k); // Step 2c if (EM.length !== k) return false; const EMprime = EMSA_PKCS1_V1_5_ENCODE(hash, prefix, M, k); // Step 3 return equalBytes(EM, EMprime); // Step 4 }, sign(privateKey, M) { validatePrivateKey(privateKey); M = ensureBytes('message', M); const { n, d } = privateKey; const k = Math.ceil(n.toString(16).length / 2); const EM = EMSA_PKCS1_V1_5_ENCODE(hash, prefix, M, k); // Step 1 const m = OS2IP(EM); // Step 2a const s = RSASP1({ n, d }, m); // Step 2b return I2OSP(s, k); // Step 2c }, }); // Encoded OIDs export const PKCS1_SHA1 = /* @__PURE__ */ PKCS1(sha1, '3021300906052b0e03021a05000414'); export const PKCS1_SHA224 = /* @__PURE__ */ PKCS1(sha224, '302d300d06096086480165030402040500041c'); export const PKCS1_SHA256 = /* @__PURE__ */ PKCS1(sha256, '3031300d060960864801650304020105000420'); export const PKCS1_SHA384 = /* @__PURE__ */ PKCS1(sha384, '3041300d060960864801650304020205000430'); export const PKCS1_SHA512 = /* @__PURE__ */ PKCS1(sha512, '3051300d060960864801650304020305000440'); export const PKCS1_SHA512_224 = /* @__PURE__ */ PKCS1(sha512_224, '302d300d06096086480165030402050500041c'); export const PKCS1_SHA512_256 = /* @__PURE__ */ PKCS1(sha512_256, '3031300d060960864801650304020605000420'); // https://github.com/usnistgov/ACVP-Server/issues/257#issuecomment-1502669140 export const PKCS1_SHA3_224 = /* @__PURE__ */ PKCS1(sha3_224, '302d300d06096086480165030402070500041c'); export const PKCS1_SHA3_256 = /* @__PURE__ */ PKCS1(sha3_256, '3031300d060960864801650304020805000420'); export const PKCS1_SHA3_384 = /* @__PURE__ */ PKCS1(sha3_384, '3041300d060960864801650304020905000430'); export const PKCS1_SHA3_512 = /* @__PURE__ */ PKCS1(sha3_512, '3051300d060960864801650304020a05000440'); export const _TEST = { RSAEP, RSADP, RSASP1 }; //# sourceMappingURL=rsa.js.map