UNPKG

ff1-js

Version:

FF1 (Format-Preserving Encryption) implementation in JavaScript/TypeScript

352 lines (348 loc) 13.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('crypto')) : typeof define === 'function' && define.amd ? define(['exports', 'crypto'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.FF1 = {}, global.crypto)); })(this, (function (exports, crypto) { 'use strict'; /** * FF1 (Format-Preserving Encryption) implementation * Based on NIST SP 800-38G specification */ class FF1 { /** * Creates a new FF1 instance * @param key - The encryption key (must be 16, 24, or 32 bytes) * @param tweak - The tweak value (must be between 0 and 2^104 bytes) * @param radix - The radix (base) of the alphabet (2-256) * @param alphabet - The alphabet string (must have length equal to radix) * @param minLength - Minimum input length (default: 2) * @param maxLength - Maximum input length (default: 100) */ constructor(key, tweak, radix, alphabet, minLength = 2, maxLength = 100) { // Validate and convert key if (typeof key === 'string') { this.key = Buffer.from(key, 'utf8'); } else { this.key = key; } // Validate and convert tweak if (typeof tweak === 'string') { this.tweak = Buffer.from(tweak, 'utf8'); } else { this.tweak = tweak; } this.radix = radix; this.alphabet = alphabet; this.minLength = minLength; this.maxLength = maxLength; this.validateParameters(); } /** * Validates the constructor parameters */ validateParameters() { // Validate key length if (this.key.length !== 16 && this.key.length !== 24 && this.key.length !== 32) { throw new Error('Key must be 16, 24, or 32 bytes long'); } // Validate radix if (this.radix < 2 || this.radix > 256) { throw new Error('Radix must be between 2 and 256'); } // Validate alphabet if (this.alphabet.length !== this.radix) { throw new Error(`Alphabet length (${this.alphabet.length}) must equal radix (${this.radix})`); } // Validate tweak length const maxTweakLength = Math.pow(2, 104); if (this.tweak.length > maxTweakLength) { throw new Error(`Tweak length (${this.tweak.length}) exceeds maximum (${maxTweakLength})`); } // Validate length constraints if (this.minLength < 2) { throw new Error('Minimum length must be at least 2'); } if (this.maxLength < this.minLength) { throw new Error('Maximum length must be greater than or equal to minimum length'); } } /** * Converts a string to a bigint using the specified radix and alphabet */ number(str) { let result = 0n; for (let i = 0; i < str.length; i++) { const char = str[i]; const index = this.alphabet.indexOf(char); if (index === -1) { throw new Error(`Invalid character in input: ${char}`); } result = result * BigInt(this.radix) + BigInt(index); } return result; } /** * Converts a bigint to a string using the specified radix and alphabet */ str(length, num) { let result = ''; for (let i = 0; i < length; i++) { const remainder = Number(num % BigInt(this.radix)); result = this.alphabet[remainder] + result; num = num / BigInt(this.radix); } return result; } /** * Pseudo-random function (PRF) using HMAC-SHA256 */ prf(output, outputOffset, input, inputOffset, length) { const hmac = crypto.createHmac('sha256', this.key); hmac.update(input.subarray(inputOffset, inputOffset + length)); const digest = hmac.digest(); digest.copy(output, outputOffset); } /** * AES encryption in ECB mode */ ciph(output, outputOffset, input, inputOffset) { // Ensure we have a 32-byte key for AES-256 let key = this.key; if (key.length === 16) { // For AES-128, we need to pad or use a different approach // For now, let's use AES-128 if key is 16 bytes const cipher = crypto.createCipheriv('aes-128-ecb', key, ''); cipher.setAutoPadding(false); const encrypted = cipher.update(input.subarray(inputOffset, inputOffset + 16)); encrypted.copy(output, outputOffset); } else if (key.length === 24) { // For AES-192 const cipher = crypto.createCipheriv('aes-192-ecb', key, ''); cipher.setAutoPadding(false); const encrypted = cipher.update(input.subarray(inputOffset, inputOffset + 16)); encrypted.copy(output, outputOffset); } else { // For AES-256 (32 bytes) const cipher = crypto.createCipheriv('aes-256-ecb', key, ''); cipher.setAutoPadding(false); const encrypted = cipher.update(input.subarray(inputOffset, inputOffset + 16)); encrypted.copy(output, outputOffset); } } /** * XOR operation between two buffers */ xor(result, resultOffset, a, aOffset, b, bOffset, length) { for (let i = 0; i < length; i++) { result[resultOffset + i] = a[aOffset + i] ^ b[bOffset + i]; } } /** * Converts bigint to bytes */ bigIntToBytes(num) { const hex = num.toString(16); const paddedHex = hex.length % 2 === 0 ? hex : '0' + hex; return Buffer.from(paddedHex, 'hex'); } /** * Converts bytes to bigint */ bytesToBigInt(bytes) { return BigInt('0x' + bytes.toString('hex')); } /** * Main FF1 cipher function */ cipher(X, twk, encrypt) { const n = X.length; const u = Math.floor(n / 2); const v = n - u; // Validate input length if (n < this.minLength || n > this.maxLength) { throw new Error(`Input length ${n} is outside allowed range [${this.minLength}, ${this.maxLength}]`); } const b = Math.ceil((Math.log2(this.radix) * v + 7) / 8); const d = 4 * Math.ceil((b + 3) / 4) + 4; const p = 16; const r = Math.ceil((d + 15) / 16) * 16; // The number of bytes in Q const q = Math.ceil((twk.length + b + 1 + 15) / 16) * 16; let A, B; const PQ = Buffer.alloc(p + q); const R = Buffer.alloc(r); // Use default tweak if none is supplied if (!twk) { twk = this.tweak; } // Step 2: Split input if (encrypt) { A = X.substring(0, u); B = X.substring(u); } else { B = X.substring(0, u); A = X.substring(u); } // Step 5: Construct P PQ[0] = 1; PQ[1] = 2; PQ[2] = 1; PQ[3] = (this.radix >> 16) & 0xff; PQ[4] = (this.radix >> 8) & 0xff; PQ[5] = this.radix & 0xff; PQ[6] = 10; PQ[7] = u; PQ[8] = (n >> 24) & 0xff; PQ[9] = (n >> 16) & 0xff; PQ[10] = (n >> 8) & 0xff; PQ[11] = n & 0xff; PQ[12] = (twk.length >> 24) & 0xff; PQ[13] = (twk.length >> 16) & 0xff; PQ[14] = (twk.length >> 8) & 0xff; PQ[15] = twk.length & 0xff; // Step 6i: Copy tweak to Q twk.copy(PQ, p); // Main FF1 loop (10 rounds) for (let i = 0; i < 10; i++) { // Step 6v: Determine m const m = ((i + (encrypt ? 1 : 0)) % 2 === 1) ? u : v; // Step 6i: Set round number PQ[PQ.length - b - 1] = encrypt ? i : (9 - i); // Convert B to integer and store in Q const c = this.number(B); const numb = this.bigIntToBytes(c); if (b <= numb.length) { numb.copy(PQ, PQ.length - b, 0, b); } else { // Pad on the left with zeros PQ.fill(0, PQ.length - b, PQ.length - numb.length); numb.copy(PQ, PQ.length - numb.length); } // Step 6ii: PRF this.prf(R, 0, PQ, 0, PQ.length); // Step 6iii: Generate additional blocks if needed for (let j = 1; j < r / 16; j++) { const l = j * 16; R.fill(0, l, l + 12); R[l + 12] = (j >> 24) & 0xff; R[l + 13] = (j >> 16) & 0xff; R[l + 14] = (j >> 8) & 0xff; R[l + 15] = j & 0xff; this.xor(R, l, R, 0, R, l, 16); this.ciph(R, l, R, l); } // Step 6vi: Calculate A +/- y mod radix^m const y = this.bytesToBigInt(R.subarray(0, d)); const yMod = y % (1n << BigInt(8 * d)); const cA = this.number(A); let result; if (encrypt) { result = (cA + yMod) % BigInt(this.radix) ** BigInt(m); } else { result = (cA - yMod) % BigInt(this.radix) ** BigInt(m); if (result < 0n) { result += BigInt(this.radix) ** BigInt(m); } } // Step 6viii: Swap A and B A = B; // Step 6vii, 6ix: Convert result back to string B = this.str(m, result); } // Step 7: Return final result return encrypt ? (A + B) : (B + A); } /** * Encrypts a string using FF1 * @param input - The string to encrypt * @param tweak - Optional tweak value (uses default if not provided) * @returns The encrypted string */ encrypt(input, tweak) { let twk = tweak; if (typeof twk === 'string') { twk = Buffer.from(twk, 'utf8'); } else if (!twk) { twk = this.tweak; } return this.cipher(input, twk, true); } /** * Decrypts a string using FF1 * @param input - The string to decrypt * @param tweak - Optional tweak value (uses default if not provided) * @returns The decrypted string */ decrypt(input, tweak) { let twk = tweak; if (typeof twk === 'string') { twk = Buffer.from(twk, 'utf8'); } else if (!twk) { twk = this.tweak; } return this.cipher(input, twk, false); } /** * Gets the current configuration */ getConfig() { return { radix: this.radix, alphabet: this.alphabet, minLength: this.minLength, maxLength: this.maxLength, keyLength: this.key.length, tweakLength: this.tweak.length }; } } /** * Predefined alphabets for common use cases */ const Alphabets = { /** Numeric alphabet (0-9) */ NUMERIC: '0123456789', /** Lowercase alphabet (a-z) */ LOWERCASE: 'abcdefghijklmnopqrstuvwxyz', /** Uppercase alphabet (A-Z) */ UPPERCASE: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', /** Alphanumeric alphabet (0-9, a-z, A-Z) */ ALPHANUMERIC: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', /** Hexadecimal alphabet (0-9, a-f) */ HEXADECIMAL: '0123456789abcdef', /** Base64 alphabet */ BASE64: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' }; /** * Creates an FF1 instance with common configurations */ function createFF1(key, tweak, alphabet = Alphabets.ALPHANUMERIC, minLength = 2, maxLength = 100) { return new FF1(key, tweak, alphabet.length, alphabet, minLength, maxLength); } /** * Creates an FF1 instance for numeric encryption */ function createNumericFF1(key, tweak, minLength = 2, maxLength = 100) { return new FF1(key, tweak, 10, Alphabets.NUMERIC, minLength, maxLength); } /** * Creates an FF1 instance for alphanumeric encryption */ function createAlphanumericFF1(key, tweak, minLength = 2, maxLength = 100) { return new FF1(key, tweak, 62, Alphabets.ALPHANUMERIC, minLength, maxLength); } exports.Alphabets = Alphabets; exports.FF1 = FF1; exports.createAlphanumericFF1 = createAlphanumericFF1; exports.createFF1 = createFF1; exports.createNumericFF1 = createNumericFF1; })); //# sourceMappingURL=ff1.js.map