@li0ard/gost3413
Version:
Cipher modes and padding's according to GOST R 34.13-2015 in pure TypeScript
412 lines (411 loc) • 16.2 kB
JavaScript
import { bytesToNumberBE, concatBytes, equalBytes, KEYSIZE, numberToBytesBE, xor } from "./utils.js";
export { KEYSIZE } from "./utils.js";
export { xor, equalBytes, concatBytes, hexToBytes, bytesToHex, numberToBytesBE, numberToBytesLE, hexToNumber, bytesToNumberBE, bytesToNumberLE } from "./utils.js";
export { MGM } from "./mgm.js";
/**
* Calculate length of padding bytes needed for specific block size.
*
* @param dataLength Length of input data
* @param blockSize Target block size
*/
export const getPadLength = (dataLength, blockSize) => {
if (dataLength < blockSize)
return blockSize - dataLength;
if (dataLength % blockSize == 0)
return 0;
return blockSize - dataLength % blockSize;
};
/**
* Procedure 1 (aka `Процедура 1`)
*
* Just fill in with zeros if necessary
* @param data Input data
* @param blockSize Target block size
*/
export const pad1 = (data, blockSize) => {
const padded = new Uint8Array(data.length + getPadLength(data.length, blockSize));
padded.set(data);
return padded;
};
/**
* Procedure 2 (aka `Процедура 2` aka `ISO/IEC 7816-4`)
* @param data Input data
* @param blockSize Target block size
*/
export const pad2 = (data, blockSize) => {
const padded = new Uint8Array(data.length + 1 + getPadLength(data.length + 1, blockSize));
padded.set(data, 0);
padded[data.length] = 0x80;
return padded;
};
/**
* Unpadding for Procedure 2
* @param data Input data
* @param blockSize Target block size
*/
export const unpad2 = (data, blockSize) => {
const lastBlock = data.slice(data.length - blockSize);
let padIndex = -1;
for (let i = lastBlock.length - 1; i >= 0; i--) {
if (lastBlock[i] === 0x80) {
padIndex = i;
break;
}
}
if (padIndex === -1)
throw new Error("Padding marker (0x80) not found");
for (let i = padIndex + 1; i < lastBlock.length; i++) {
if (lastBlock[i] !== 0)
throw new Error("Invalid padding: non-zero bytes after 0x80");
}
return data.slice(0, data.length - (blockSize - padIndex));
};
/**
* Procedure 3 (aka `Процедура 3`)
*
* If length of data matches block size, do nothing; otherwise, use Procedure 2 (`pad2`)
* @param data Input data
* @param blockSize Block size
*/
export const pad3 = (data, blockSize) => {
if (getPadLength(data.length, blockSize) == 0)
return data;
return pad2(data, blockSize);
};
/**
* Wrapper for Electronic Codebook (ECB) mode
* @param encrypter Encrypting function, that takes block as input
* @param blockSize Cipher block size
* @param data Input data
*/
export const ecb_encrypt = (encrypter, blockSize, data) => {
if (data.length == 0 || data.length % blockSize !== 0)
throw new Error("Data not aligned");
const result = new Uint8Array(data.length);
let offset = 0;
for (let i = 0; i < data.length; i += blockSize) {
const chunk = data.slice(i, i + blockSize);
const encrypted = encrypter(chunk);
result.set(encrypted, offset);
offset += encrypted.length;
}
return result.slice();
};
/**
* Wrapper for Electronic Codebook (ECB) mode
* @param decrypter Decrypting function, that takes block as input
* @param blockSize Cipher block size
* @param data Input data
*/
export const ecb_decrypt = (decrypter, blockSize, data) => {
if (data.length == 0 || data.length % blockSize !== 0)
throw new Error("Data not aligned");
const result = new Uint8Array(data.length);
let offset = 0;
for (let i = 0; i < data.length; i += blockSize) {
const chunk = data.slice(i, i + blockSize);
const encrypted = decrypter(chunk);
result.set(encrypted, offset);
offset += encrypted.length;
}
return result.slice();
};
/**
* Wrapper for Output Feedback (OFB) mode
*
* For decryption you SHOULD use this function again
* @param encrypter Encrypting function, that takes block as input
* @param blockSize Cipher block size
* @param data Input data
* @param iv Initialization vector
*/
export const ofb = (encrypter, blockSize, data, iv) => {
if (iv.length == 0 || iv.length % blockSize !== 0)
throw new Error("Invalid IV size");
let r = [];
for (let i = 0; i < iv.length; i += blockSize)
r.push(iv.slice(i, i + blockSize));
const result = [];
for (let i = 0; i < (data.length + getPadLength(data.length, blockSize)); i += blockSize) {
r = r.slice(1).concat([encrypter(r[0])]);
const keystreamBlock = r[r.length - 1];
const dataBlock = data.slice(i, i + blockSize);
result.push(xor(keystreamBlock, dataBlock));
}
return concatBytes(...result).slice();
};
/**
* Wrapper for Cipher Block Chaining (CBC) mode
* @param encrypter Encrypting function, that takes block as input
* @param blockSize Cipher block size
* @param data Input data
* @param iv Initialization vector
*/
export const cbc_encrypt = (encrypter, blockSize, data, iv) => {
if (data.length == 0 || data.length % blockSize !== 0)
throw new Error("Data not aligned");
if (iv.length == 0 || iv.length % blockSize !== 0)
throw new Error("Invalid IV size");
let r = [];
for (let i = 0; i < iv.length; i += blockSize)
r.push(iv.slice(i, i + blockSize));
const result = [];
for (let i = 0; i < data.length; i += blockSize) {
result.push(encrypter(xor(r[0], data.slice(i, i + blockSize))));
r = r.slice(1).concat([result[result.length - 1]]);
}
return concatBytes(...result).slice();
};
/**
* Wrapper for Cipher Block Chaining (CBC) mode
* @param decrypter Decrypting function, that takes block as input
* @param blockSize Cipher block size
* @param data Input data
* @param iv Initialization vector
*/
export const cbc_decrypt = (decrypter, blockSize, data, iv) => {
if (data.length == 0 || data.length % blockSize !== 0)
throw new Error("Data not aligned");
if (iv.length == 0 || iv.length % blockSize !== 0)
throw new Error("Invalid IV size");
let r = [];
for (let i = 0; i < iv.length; i += blockSize)
r.push(iv.slice(i, i + blockSize));
const result = [];
for (let i = 0; i < data.length; i += blockSize) {
let blk = data.slice(i, i + blockSize);
result.push(xor(r[0], decrypter(blk)));
r = r.slice(1).concat([blk]);
}
return concatBytes(...result).slice();
};
/**
* Wrapper for Cipher Feedback (CFB) mode
* @param encrypter Encrypting function, that takes block as input
* @param blockSize Cipher block size
* @param data Input data
* @param iv Initialization vector
*/
export const cfb_encrypt = (encrypter, blockSize, data, iv) => {
if (iv.length == 0 || iv.length % blockSize !== 0)
throw new Error("Invalid IV size");
let r = [];
for (let i = 0; i < iv.length; i += blockSize)
r.push(iv.slice(i, i + blockSize));
const result = [];
for (let i = 0; i < (data.length + getPadLength(data.length, blockSize)); i += blockSize) {
result.push(xor(encrypter(r[0]), data.slice(i, i + blockSize)));
r = r.slice(1).concat([result[result.length - 1]]);
}
return concatBytes(...result).slice();
};
/**
* Wrapper for Cipher Feedback (CFB) mode
* @param decrypter Decrypting function, that takes block as input
* @param blockSize Cipher block size
* @param data Input data
* @param iv Initialization vector
*/
export const cfb_decrypt = (decrypter, blockSize, data, iv) => {
if (iv.length == 0 || iv.length % blockSize !== 0)
throw new Error("Invalid IV size");
let r = [];
for (let i = 0; i < iv.length; i += blockSize)
r.push(iv.slice(i, i + blockSize));
const result = [];
for (let i = 0; i < (data.length + getPadLength(data.length, blockSize)); i += blockSize) {
let blk = data.slice(i, i + blockSize);
result.push(xor(decrypter(r[0]), blk));
r = r.slice(1).concat([blk]);
}
return concatBytes(...result).slice();
};
/**
* Wrapper for counter (CTR) mode
*
* For decryption you SHOULD use this function again
* @param encrypter Encrypting function, that takes block as input
* @param blockSize Cipher block size
* @param data Input data
* @param iv Initialization vector (Half of block size)
* @param acpkm Optional. Parameters for CTR-ACPKM mode
*/
export const ctr = (encrypter, blockSize, data, iv, acpkm) => {
const halfBlockSize = (blockSize / 2) | 0;
if (iv.length !== halfBlockSize)
throw new Error("Invalid IV size");
const ctrMax = 1n << (8n * BigInt(halfBlockSize));
const maxSize = ctrMax * BigInt(blockSize);
if (BigInt(data.length) > maxSize)
throw new Error("Too big data");
let acpkmSectionSize = 0;
if (acpkm)
acpkmSectionSize = (acpkm.sectionSize / blockSize) | 0;
const keystreamBlocks = [];
for (let ctr = 0; ctr < Math.ceil(data.length / blockSize); ctr++) {
if (acpkm && ctr != 0 && (ctr % acpkmSectionSize) == 0) {
let cipher = new acpkm.cipherClass(acpkmDerivation(encrypter, blockSize));
encrypter = cipher.encrypt.bind(cipher);
}
keystreamBlocks.push(encrypter(concatBytes(iv, numberToBytesBE(ctr, halfBlockSize))));
}
return xor(concatBytes(...keystreamBlocks), data);
};
const Rb64 = 0b11011;
const Rb128 = 0b10000111;
const macShift = (blockSize, data, xorLsb = 0) => {
const num = (bytesToNumberBE(data) * BigInt(2)) ^ BigInt(xorLsb);
return numberToBytesBE(num, blockSize).slice(-blockSize);
};
const macKs = (encrypter, blockSize) => {
const Rb = blockSize === 16 ? Rb128 : Rb64;
const l = encrypter(new Uint8Array(blockSize));
let k1;
if ((l[0] & 0x80) !== 0)
k1 = macShift(blockSize, l, Rb);
else
k1 = macShift(blockSize, l);
let k2;
if ((k1[0] & 0x80) !== 0)
k2 = macShift(blockSize, k1, Rb);
else
k2 = macShift(blockSize, k1);
return [k1, k2];
};
/**
* Wrapper for MAC (CMAC/OMAC1) mode
* @param encrypter Encrypting function, that takes block as input
* @param blockSize Cipher block size
* @param data Input data
*/
export const mac = (encrypter, blockSize, data) => {
const [k1, k2] = macKs(encrypter, blockSize);
let tailOffset;
if (data.length % blockSize === 0)
tailOffset = data.length - blockSize;
else
tailOffset = data.length - (data.length % blockSize);
let prev = new Uint8Array(blockSize);
for (let i = 0; i < tailOffset; i += blockSize)
prev = encrypter(xor(data.slice(i, i + blockSize), prev));
const tail = data.slice(tailOffset);
const xorWithPrev = xor(pad3(tail, blockSize), prev);
return encrypter(xor(xorWithPrev, (tail.length === blockSize ? k1 : k2)));
};
/**
* ACPKM key derivation
* @param encrypter Encrypting function, that takes block as input
* @param blockSize Cipher block size
*/
export const acpkmDerivation = (encrypter, blockSize) => {
let result = [];
for (let d = 0x80; d < (0x80 + blockSize * ((KEYSIZE / blockSize) | 0)); d += blockSize) {
const block = new Uint8Array(blockSize);
for (let i = 0; i < blockSize; i++)
block[i] = d + i;
result.push(encrypter(block));
}
return concatBytes(...result);
};
/**
* Wrapper for Counter with Advance Cryptographic Prolongation of Key Material (CTR-ACPKM) mode
*
* For decryption you SHOULD use this function again
* @param cipherClass Cipher class (see `ACPKMConstructor` and `ACPKMClass`)
* @param encrypter Encrypting function, that takes block as input
* @param sectionSize ACPKM section size (N)
* @param blockSize Cipher block size
* @param data Input data
* @param iv Initialization vector (Half of block size)
*/
export const ctr_acpkm = (cipherClass, encrypter, sectionSize, blockSize, data, iv) => {
return ctr(encrypter, blockSize, data, iv, { cipherClass, sectionSize });
};
/**
* ACPKM master key derivation
* @param cipherClass Cipher class (see `ACPKMConstructor` and `ACPKMClass`)
* @param encrypter Encrypting function, that takes block as input
* @param keySectionSize ACPKM key section size (T*)
* @param blockSize Cipher block size
* @param keyMaterialLength Length of key material
*/
export const acpkmDerivationMaster = (cipherClass, encrypter, keySectionSize, blockSize, keyMaterialLength) => {
return ctr_acpkm(cipherClass, encrypter, keySectionSize, blockSize, new Uint8Array(keyMaterialLength).fill(0), new Uint8Array((blockSize / 2) | 0).fill(0xFF));
};
/**
* Wrapper for MAC with Advance Cryptographic Prolongation of Key Material (OMAC-ACPKM) mode
* @param cipherClass Cipher class (see `ACPKMConstructor` and `ACPKMClass`)
* @param encrypter Encrypting function, that takes block as input
* @param keySectionSize ACPKM key section size (T*)
* @param sectionSize ACPKM section size (N)
* @param blockSize Cipher block size
* @param data Input data
*/
export const omac_acpkm_master = (cipherClass, encrypter, keySectionSize, sectionSize, blockSize, data) => {
let tail_offset = 0;
if (data.length % blockSize == 0)
tail_offset = data.length - blockSize;
else
tail_offset = data.length - (data.length % blockSize);
let prev = new Uint8Array(blockSize).fill(0);
let sections = data.length;
if (data.length % sectionSize != 0)
sections += 1;
let keymats = acpkmDerivationMaster(cipherClass, encrypter, keySectionSize, blockSize, (KEYSIZE + blockSize) * sections);
let k1 = new Uint8Array(sectionSize);
for (let i = 0; i < tail_offset; i += blockSize) {
if (i % sectionSize == 0) {
let keymat = keymats.slice(0, KEYSIZE + blockSize);
keymats = keymats.slice(KEYSIZE + blockSize);
let key = keymat.slice(0, KEYSIZE);
k1 = keymat.slice(KEYSIZE);
let cipher = new cipherClass(key);
encrypter = cipher.encrypt.bind(cipher);
}
prev = encrypter(xor(data.slice(i, i + blockSize), prev));
}
let tail = data.slice(tail_offset);
if (tail.length == blockSize) {
let key = keymats.slice(0, KEYSIZE);
k1 = keymats.slice(KEYSIZE);
let cipher = new cipherClass(key);
encrypter = cipher.encrypt.bind(cipher);
}
let k2 = numberToBytesBE(bytesToNumberBE(k1) << 1n, blockSize);
if ((k1.slice()[0] & 0x80) != 0)
k2 = xor(k2, numberToBytesBE(blockSize == 16 ? Rb128 : Rb64, blockSize));
return encrypter(xor(xor(pad3(tail, blockSize), prev), (tail.length == blockSize) ? k1 : k2));
};
/**
* KExp15 key exporting
* @param encrypter_key Encrypting function for key encryption, that takes block as input
* @param encrypter_mac Encrypting function for key authentication, that takes block as input
* @param blockSize Cipher block size
* @param key Key to export
* @param iv Initialization vector (Half of block size)
*/
export const kexp15 = (encrypter_key, encrypter_mac, blockSize, key, iv) => {
const halfBlockSize = (blockSize / 2) | 0;
if (iv.length !== halfBlockSize)
throw new Error("Invalid IV size");
let key_mac = mac(encrypter_mac, blockSize, concatBytes(iv, key));
return ctr(encrypter_key, blockSize, concatBytes(key, key_mac), iv);
};
/**
* KImp15 key importing
* @param encrypter_key Encrypting function for key decryption, that takes block as input
* @param encrypter_mac Encrypting function for key authentication, that takes block as input
* @param blockSize Cipher block size
* @param kexp Key to import
* @param iv Initialization vector (Half of block size)
*/
export const kimp15 = (encrypter_key, encrypter_mac, blockSize, kexp, iv) => {
const halfBlockSize = (blockSize / 2) | 0;
if (iv.length !== halfBlockSize)
throw new Error("Invalid IV size");
const key_and_key_mac = ctr(encrypter_key, blockSize, kexp, iv);
const [key, key_mac] = [key_and_key_mac.slice(0, -blockSize), key_and_key_mac.slice(-blockSize)];
if (!equalBytes(mac(encrypter_mac, blockSize, concatBytes(iv, key)), key_mac))
throw new Error("Invalid authentication tag");
return key;
};