165 lines
6.3 KiB
JavaScript
165 lines
6.3 KiB
JavaScript
|
"use strict";
|
|||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|||
|
exports.FF1 = FF1;
|
|||
|
exports.BinaryFF1 = BinaryFF1;
|
|||
|
/**
|
|||
|
* FPE-FF1 (Format-preserving encryption algorithm) specified in
|
|||
|
* [NIST 800-38G](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf).
|
|||
|
* @module
|
|||
|
*/
|
|||
|
const _assert_js_1 = require("./_assert.js");
|
|||
|
const aes_js_1 = require("./aes.js");
|
|||
|
const utils_js_1 = require("./utils.js");
|
|||
|
// NOTE: no point in inlining encrypt instead of encryptBlock, since BigInt stuff will be slow
|
|||
|
const { expandKeyLE, encryptBlock } = aes_js_1.unsafe;
|
|||
|
// Format-preserving encryption algorithm (FPE-FF1) specified in NIST Special Publication 800-38G.
|
|||
|
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf
|
|||
|
const BLOCK_LEN = 16;
|
|||
|
function mod(a, b) {
|
|||
|
const result = a % b;
|
|||
|
return result >= 0 ? result : b + result;
|
|||
|
}
|
|||
|
function NUMradix(radix, data) {
|
|||
|
let res = BigInt(0);
|
|||
|
for (let i of data)
|
|||
|
res = res * BigInt(radix) + BigInt(i);
|
|||
|
return res;
|
|||
|
}
|
|||
|
function getRound(radix, key, tweak, x) {
|
|||
|
if (radix > 2 ** 16 - 1)
|
|||
|
throw new Error('invalid radix ' + radix);
|
|||
|
// radix**minlen ≥ 100
|
|||
|
const minLen = Math.ceil(Math.log(100) / Math.log(radix));
|
|||
|
const maxLen = 2 ** 32 - 1;
|
|||
|
// 2 ≤ minlen ≤ maxlen < 2**32
|
|||
|
if (2 > minLen || minLen > maxLen || maxLen >= 2 ** 32)
|
|||
|
throw new Error('Invalid radix: 2 ≤ minlen ≤ maxlen < 2**32');
|
|||
|
if (!Array.isArray(x))
|
|||
|
throw new Error('invalid X');
|
|||
|
if (x.length < minLen || x.length > maxLen)
|
|||
|
throw new Error('X is outside minLen..maxLen bounds');
|
|||
|
const u = Math.floor(x.length / 2);
|
|||
|
const v = x.length - u;
|
|||
|
const b = Math.ceil(Math.ceil(v * Math.log2(radix)) / 8);
|
|||
|
const d = 4 * Math.ceil(b / 4) + 4;
|
|||
|
const padding = mod(-tweak.length - b - 1, 16);
|
|||
|
// P = [1]1 || [2]1 || [1]1 || [radix]3 || [10]1 || [u mod 256]1 || [n]4 || [t]4.
|
|||
|
const P = new Uint8Array([1, 2, 1, 0, 0, 0, 10, u, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|||
|
const view = new DataView(P.buffer);
|
|||
|
view.setUint16(4, radix, false);
|
|||
|
view.setUint32(8, x.length, false);
|
|||
|
view.setUint32(12, tweak.length, false);
|
|||
|
// Q = T || [0](−t−b−1) mod 16 || [i]1 || [NUMradix(B)]b.
|
|||
|
const PQ = new Uint8Array(P.length + tweak.length + padding + 1 + b);
|
|||
|
PQ.set(P);
|
|||
|
(0, utils_js_1.clean)(P);
|
|||
|
PQ.set(tweak, P.length);
|
|||
|
const xk = expandKeyLE(key);
|
|||
|
const round = (A, B, i, decrypt = false) => {
|
|||
|
// Q = ... || [i]1 || [NUMradix(B)]b.
|
|||
|
PQ[PQ.length - b - 1] = i;
|
|||
|
if (b)
|
|||
|
PQ.set((0, utils_js_1.numberToBytesBE)(NUMradix(radix, B), b), PQ.length - b);
|
|||
|
// PRF
|
|||
|
let r = new Uint8Array(16);
|
|||
|
for (let j = 0; j < PQ.length / BLOCK_LEN; j++) {
|
|||
|
for (let i = 0; i < BLOCK_LEN; i++)
|
|||
|
r[i] ^= PQ[j * BLOCK_LEN + i];
|
|||
|
encryptBlock(xk, r);
|
|||
|
}
|
|||
|
// Let S be the first d bytes of the following string of ⎡d/16⎤ blocks:
|
|||
|
// R || CIPHK(R ⊕[1]16) || CIPHK(R ⊕[2]16) ...CIPHK(R ⊕[⎡d / 16⎤ – 1]16).
|
|||
|
let s = Array.from(r);
|
|||
|
for (let j = 1; s.length < d; j++) {
|
|||
|
const block = (0, utils_js_1.numberToBytesBE)(BigInt(j), 16);
|
|||
|
for (let k = 0; k < BLOCK_LEN; k++)
|
|||
|
block[k] ^= r[k];
|
|||
|
s.push(...Array.from(encryptBlock(xk, block)));
|
|||
|
}
|
|||
|
let y = (0, utils_js_1.bytesToNumberBE)(Uint8Array.from(s.slice(0, d)));
|
|||
|
s.fill(0);
|
|||
|
if (decrypt)
|
|||
|
y = -y;
|
|||
|
const m = i % 2 === 0 ? u : v;
|
|||
|
let c = mod(NUMradix(radix, A) + y, BigInt(radix) ** BigInt(m));
|
|||
|
// STR(radix, m, c)
|
|||
|
const C = Array(m).fill(0);
|
|||
|
for (let i = 0; i < m; i++, c /= BigInt(radix))
|
|||
|
C[m - 1 - i] = Number(c % BigInt(radix));
|
|||
|
A.fill(0);
|
|||
|
A = B;
|
|||
|
B = C;
|
|||
|
return [A, B];
|
|||
|
};
|
|||
|
const destroy = () => {
|
|||
|
(0, utils_js_1.clean)(xk, PQ);
|
|||
|
};
|
|||
|
return { u, round, destroy };
|
|||
|
}
|
|||
|
const EMPTY_BUF = new Uint8Array([]);
|
|||
|
/** FPE-FF1 format-preserving encryption */
|
|||
|
function FF1(radix, key, tweak = EMPTY_BUF) {
|
|||
|
(0, _assert_js_1.anumber)(radix);
|
|||
|
(0, _assert_js_1.abytes)(key);
|
|||
|
(0, _assert_js_1.abytes)(tweak);
|
|||
|
const PQ = getRound.bind(null, radix, key, tweak);
|
|||
|
return {
|
|||
|
encrypt(x) {
|
|||
|
const { u, round, destroy } = PQ(x);
|
|||
|
let [A, B] = [x.slice(0, u), x.slice(u)];
|
|||
|
for (let i = 0; i < 10; i++)
|
|||
|
[A, B] = round(A, B, i);
|
|||
|
destroy();
|
|||
|
const res = A.concat(B);
|
|||
|
A.fill(0);
|
|||
|
B.fill(0);
|
|||
|
return res;
|
|||
|
},
|
|||
|
decrypt(x) {
|
|||
|
const { u, round, destroy } = PQ(x);
|
|||
|
// The FF1.Decrypt algorithm is similar to the FF1.Encrypt algorithm;
|
|||
|
// the differences are in Step 6, where:
|
|||
|
// 1) the order of the indices is reversed,
|
|||
|
// 2) the roles of A and B are swapped
|
|||
|
// 3) modular addition is replaced by modular subtraction, in Step 6vi.
|
|||
|
let [B, A] = [x.slice(0, u), x.slice(u)];
|
|||
|
for (let i = 9; i >= 0; i--)
|
|||
|
[A, B] = round(A, B, i, true);
|
|||
|
destroy();
|
|||
|
const res = B.concat(A);
|
|||
|
A.fill(0);
|
|||
|
B.fill(0);
|
|||
|
return res;
|
|||
|
},
|
|||
|
};
|
|||
|
}
|
|||
|
// Binary string which encodes each byte in little-endian byte order
|
|||
|
const binLE = {
|
|||
|
encode(bytes) {
|
|||
|
const x = [];
|
|||
|
for (let i = 0; i < bytes.length; i++) {
|
|||
|
for (let j = 0, tmp = bytes[i]; j < 8; j++, tmp >>= 1)
|
|||
|
x.push(tmp & 1);
|
|||
|
}
|
|||
|
return x;
|
|||
|
},
|
|||
|
decode(b) {
|
|||
|
if (!Array.isArray(b) || b.length % 8)
|
|||
|
throw new Error('Invalid binary string');
|
|||
|
const res = new Uint8Array(b.length / 8);
|
|||
|
for (let i = 0, j = 0; i < res.length; i++) {
|
|||
|
res[i] = b[j++] | (b[j++] << 1) | (b[j++] << 2) | (b[j++] << 3);
|
|||
|
res[i] |= (b[j++] << 4) | (b[j++] << 5) | (b[j++] << 6) | (b[j++] << 7);
|
|||
|
}
|
|||
|
return res;
|
|||
|
},
|
|||
|
};
|
|||
|
/** Binary version of FPE-FF1 format-preserving encryption. */
|
|||
|
function BinaryFF1(key, tweak = EMPTY_BUF) {
|
|||
|
const ff1 = FF1(2, key, tweak);
|
|||
|
return {
|
|||
|
encrypt: (x) => binLE.decode(ff1.encrypt(binLE.encode(x))),
|
|||
|
decrypt: (x) => binLE.decode(ff1.decrypt(binLE.encode(x))),
|
|||
|
};
|
|||
|
}
|
|||
|
//# sourceMappingURL=ff1.js.map
|