316 lines
13 KiB
JavaScript
316 lines
13 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.cryptoAeadDecrypt = exports.cryptoAeadEncrypt = void 0;
|
|
const constants_1 = require("./constants");
|
|
const skinny_128_384_plus_1 = require("./skinny-128-384-plus");
|
|
/**
|
|
* Parse message into blocks.
|
|
* @param message The message to parse.
|
|
* @param blockLength The block length.
|
|
* @returns An array of blocks.
|
|
*/
|
|
function parse(message, blockLength) {
|
|
// Keep track of position in message currently parsed into blocks.
|
|
let cursor = 0;
|
|
// Slice message into blocks.
|
|
let ret = [];
|
|
while (message.length - cursor >= blockLength) {
|
|
ret.push(...[message.slice(cursor, cursor + blockLength)]);
|
|
cursor = cursor + blockLength;
|
|
}
|
|
// Append any remaining blocks regardless of block length. These will be padded later.
|
|
if (message.length - cursor > 0) {
|
|
ret.push(...[message.slice(cursor)]);
|
|
}
|
|
// If no message, return a single block.
|
|
if (message.length === 0) {
|
|
ret = [[]];
|
|
}
|
|
// Insert empty array at position 0.
|
|
ret.splice(0, 0, []);
|
|
return ret;
|
|
}
|
|
/**
|
|
* Pads the byte length of message to padLength. The final byte (when padded) contains the original message length.
|
|
* @param message The message to pad.
|
|
* @param padLength The length to pad the message to.
|
|
* @returns A padded block.
|
|
*/
|
|
function pad(message, padLength) {
|
|
// If there is no message, return a fully padded block.
|
|
if (message.length === 0) {
|
|
return Array(16).fill(0);
|
|
}
|
|
// Return a copy of the message if no padding is required.
|
|
if (message.length === padLength) {
|
|
return [...message];
|
|
}
|
|
// Pad a copy of the message to padLength.
|
|
const ret = [...message];
|
|
const requiredPadding = padLength - message.length - 1;
|
|
ret.push(...Array(requiredPadding).fill(0));
|
|
// Set the final byte of the padded blocked to the length of the original message.
|
|
ret[padLength - 1] = message.length;
|
|
return ret;
|
|
}
|
|
/**
|
|
* Generate the key stream from the internal state by multiplying the state S and the constant matrix G.
|
|
* @param state The state from which the key stream will be generated.
|
|
* @returns The key stream.
|
|
*/
|
|
function g(state) {
|
|
return state.map(x => {
|
|
return (x >> 1) ^ (x & 0x80) ^ ((x & 0x01) << 7);
|
|
});
|
|
}
|
|
/**
|
|
* The state update function. Pads an M block.
|
|
* @param state The internal state, S.
|
|
* @param mBlock An M block.
|
|
* @returns [S', C] where S' = M ⊕ S and C = M ⊕ G(S)
|
|
*/
|
|
function rho(state, mBlock) {
|
|
// G(S)
|
|
const gOfS = g(state);
|
|
// C = M ⊕ G(S)
|
|
const c = [...Array(16).keys()].map(i => mBlock[i] ^ gOfS[i]);
|
|
// S' = M ⊕ S
|
|
const nextState = [...Array(16).keys()].map(i => state[i] ^ mBlock[i]);
|
|
return [nextState, c];
|
|
}
|
|
/**
|
|
* The state update function. Pads a C block.
|
|
* @param state The internal state, S.
|
|
* @param cBlock A C block.
|
|
* @returns [S', M] where M = C ⊕ G(S) and S' = C ⊕ M.
|
|
*/
|
|
function inverseRoh(state, cBlock) {
|
|
// G(S)
|
|
const gOfS = g(state);
|
|
// M = C ⊕ G(S)
|
|
const mBlock = [...Array(16).keys()].map(i => cBlock[i] ^ gOfS[i]);
|
|
// S' = S ⊕ M
|
|
const nextState = [...Array(16).keys()].map(i => state[i] ^ mBlock[i]);
|
|
return [nextState, mBlock];
|
|
}
|
|
/**
|
|
* Increments the 56 bit LFSR-based counter.
|
|
* @param counter The old counter.
|
|
* @returns An incremented counter.
|
|
*/
|
|
function increaseCounter(counter) {
|
|
const fb0 = counter[6] >> 7;
|
|
counter[6] = (counter[6] << 1) | (counter[5] >> 7);
|
|
counter[5] = (counter[5] << 1) | (counter[4] >> 7);
|
|
counter[4] = (counter[4] << 1) | (counter[3] >> 7);
|
|
counter[3] = (counter[3] << 1) | (counter[2] >> 7);
|
|
counter[2] = (counter[2] << 1) | (counter[1] >> 7);
|
|
counter[1] = (counter[1] << 1) | (counter[0] >> 7);
|
|
if (fb0 === 1) {
|
|
counter[0] = (counter[0] << 1) ^ 0x95;
|
|
}
|
|
else {
|
|
counter[0] = (counter[0] << 1);
|
|
}
|
|
return counter;
|
|
}
|
|
/**
|
|
* Returns a reset counter.
|
|
* @returns A reset counter.
|
|
*/
|
|
function resetCounter() {
|
|
const counter = Array(constants_1.COUNTER_LENGTH).fill(0);
|
|
counter[0] = 1;
|
|
return counter;
|
|
}
|
|
/**
|
|
* Returns a zeroed buffer.
|
|
* @param bufferLength The length of the buffer to return.
|
|
* @returns A zeroed buffer.
|
|
*/
|
|
function zeroedBuffer(bufferLength) {
|
|
return Array(bufferLength).fill(0);
|
|
}
|
|
/**
|
|
* Calculate the domain separation.
|
|
* @param combinedData The parsed and concatenated message and associated data,
|
|
* @param parsedMessageLength The length of the parsed message.
|
|
* @param parsedAssociatedDataLength The length of the parsed associated data.
|
|
*/
|
|
function calculateDomainSeparation(combinedData, parsedMessageLength, parsedAssociatedDataLength) {
|
|
let domainSeparation = 16;
|
|
if (combinedData[parsedAssociatedDataLength].length < 16) {
|
|
domainSeparation = domainSeparation ^ 2;
|
|
}
|
|
if (combinedData[parsedAssociatedDataLength + parsedMessageLength].length < 16) {
|
|
domainSeparation = domainSeparation ^ 1;
|
|
}
|
|
if (parsedAssociatedDataLength % 2 === 0) {
|
|
domainSeparation = domainSeparation ^ 8;
|
|
}
|
|
if (parsedMessageLength % 2 === 0) {
|
|
domainSeparation = domainSeparation ^ 4;
|
|
}
|
|
return domainSeparation;
|
|
}
|
|
/**
|
|
* Encrypt a message using the Romulus-M cryptography specification.
|
|
* @param message The message to encrypt.
|
|
* @param associatedData The associated data to encrypt.
|
|
* @param nonce A 128 bit nonce.
|
|
* @param key A 128 bit encryption key.
|
|
* @returns The encrypted ciphertext.
|
|
*/
|
|
function cryptoAeadEncrypt(message, associatedData, nonce, key) {
|
|
// Buffer for ciphertext.
|
|
const ciphertext = [];
|
|
// Reset state and counter.
|
|
let state = zeroedBuffer(16);
|
|
let counter = resetCounter();
|
|
// Carve message and associated data into blocks.
|
|
const messageBlocks = parse(message, 16);
|
|
const messageBlockCount = messageBlocks.length - 1;
|
|
const associatedDataBlocks = parse(associatedData, 16);
|
|
const associatedDataBlockCount = associatedDataBlocks.length - 1;
|
|
// Concatenate the message and associated data blocks, excluding each array's first element.
|
|
const combinedDataBlocks = associatedDataBlocks.slice(1).concat(messageBlocks.slice(1));
|
|
// Insert empty array at position 0.
|
|
combinedDataBlocks.splice(0, 0, []);
|
|
// Calculate domain separation for final encryption stage.
|
|
const domainSeparation = calculateDomainSeparation(combinedDataBlocks, messageBlockCount, associatedDataBlockCount);
|
|
// Pad combined data.
|
|
combinedDataBlocks[associatedDataBlockCount] = pad(combinedDataBlocks[associatedDataBlockCount], 16);
|
|
combinedDataBlocks[associatedDataBlockCount + messageBlockCount] = pad(combinedDataBlocks[associatedDataBlockCount + messageBlockCount], 16);
|
|
// Do the encryption.
|
|
// See https://romulusae.github.io/romulus/docs/Romulusv1.3.pdf for more information.
|
|
let x = 8;
|
|
for (let i = 1; i < Math.floor((associatedDataBlockCount + messageBlockCount) / 2) + 1; i++) {
|
|
[state] = rho(state, combinedDataBlocks[2 * i - 1]);
|
|
counter = increaseCounter(counter);
|
|
if (i === Math.floor(associatedDataBlockCount / 2) + 1) {
|
|
x = x ^ 4;
|
|
}
|
|
state = (0, skinny_128_384_plus_1.skinnyEncrypt)(state, (0, skinny_128_384_plus_1.tweakeyEncode)(counter, x, combinedDataBlocks[2 * i], key));
|
|
counter = increaseCounter(counter);
|
|
}
|
|
if (associatedDataBlockCount % 2 === messageBlockCount % 2) {
|
|
[state] = rho(state, zeroedBuffer(16));
|
|
}
|
|
else {
|
|
[state] = rho(state, combinedDataBlocks[associatedDataBlockCount + messageBlockCount]);
|
|
counter = increaseCounter(counter);
|
|
}
|
|
const [, authenticationTag] = rho((0, skinny_128_384_plus_1.skinnyEncrypt)(state, (0, skinny_128_384_plus_1.tweakeyEncode)(counter, domainSeparation, nonce, key)), zeroedBuffer(16));
|
|
if (message.length === 0) {
|
|
return authenticationTag;
|
|
}
|
|
state = [...authenticationTag];
|
|
counter = resetCounter();
|
|
const originalFinalMessageBlockLength = messageBlocks[messageBlockCount].length;
|
|
messageBlocks[messageBlockCount] = pad(messageBlocks[messageBlockCount], 16);
|
|
for (let i = 1; i < messageBlockCount + 1; i++) {
|
|
state = (0, skinny_128_384_plus_1.skinnyEncrypt)(state, (0, skinny_128_384_plus_1.tweakeyEncode)(counter, 4, nonce, key));
|
|
let cBlock;
|
|
[state, cBlock] = rho(state, messageBlocks[i]);
|
|
counter = increaseCounter(counter);
|
|
if (i < messageBlockCount) {
|
|
ciphertext.push(...cBlock);
|
|
}
|
|
else {
|
|
ciphertext.push(...cBlock.slice(0, originalFinalMessageBlockLength));
|
|
}
|
|
}
|
|
// The authentication tag is stored in the final 16 bytes of the ciphertext.
|
|
ciphertext.push(...authenticationTag);
|
|
return ciphertext;
|
|
}
|
|
exports.cryptoAeadEncrypt = cryptoAeadEncrypt;
|
|
/**
|
|
* Decrypt a message using the Romulus-M cryptography specification.
|
|
* @param ciphertext The ciphertext to decrypt.
|
|
* @param associatedData The associated data.
|
|
* @param nonce The nonce.
|
|
* @param key The key.
|
|
* @returns The decrypted plaintext.
|
|
*/
|
|
function cryptoAeadDecrypt(ciphertext, associatedData, nonce, key) {
|
|
// Buffer for decrypted message.
|
|
const message = [];
|
|
// The authentication tag is represented by the final 16 bytes of the ciphertext.
|
|
const authenticationTag = ciphertext.slice(-16);
|
|
ciphertext.length -= 16;
|
|
// Reset state and counter.
|
|
let state = zeroedBuffer(16);
|
|
let counter = resetCounter();
|
|
if (ciphertext.length !== 0) {
|
|
// Combine the ciphertext
|
|
state = [...authenticationTag];
|
|
const ciphertextBlocks = parse(ciphertext, 16);
|
|
const ciphertextBlockCount = ciphertextBlocks.length - 1;
|
|
const finalCiphertextBlockLength = ciphertextBlocks[ciphertextBlockCount].length;
|
|
ciphertextBlocks[ciphertextBlockCount] = pad(ciphertextBlocks[ciphertextBlockCount], 16);
|
|
for (let i = 1; i < ciphertextBlockCount + 1; i++) {
|
|
state = (0, skinny_128_384_plus_1.skinnyEncrypt)(state, (0, skinny_128_384_plus_1.tweakeyEncode)(counter, 4, nonce, key));
|
|
let mBlock;
|
|
[state, mBlock] = inverseRoh(state, ciphertextBlocks[i]);
|
|
counter = increaseCounter(counter);
|
|
if (i < ciphertextBlockCount) {
|
|
message.push(...mBlock);
|
|
}
|
|
else {
|
|
message.push(...mBlock.slice(0, finalCiphertextBlockLength));
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
state = [];
|
|
}
|
|
// Reset state and counter.
|
|
state = zeroedBuffer(16);
|
|
counter = resetCounter();
|
|
// Carve the message and associated data into blocks.
|
|
const messageBlocks = parse(message, 16);
|
|
const messageBlockLength = messageBlocks.length - 1;
|
|
const associatedDataBlocks = parse(associatedData, 16);
|
|
const associatedDataBlockCount = associatedDataBlocks.length - 1;
|
|
// Concatenate the message and associated data blocks, excluding each array's first element.
|
|
const combinedData = associatedDataBlocks.slice(1).concat(messageBlocks.slice(1));
|
|
// Insert empty array at position 0.
|
|
combinedData.splice(0, 0, []);
|
|
// Calculate domain separation for final decryption stage.
|
|
const domainSeparation = calculateDomainSeparation(combinedData, messageBlockLength, associatedDataBlockCount);
|
|
// Pad combined data.
|
|
combinedData[associatedDataBlockCount] = pad(combinedData[associatedDataBlockCount], 16);
|
|
combinedData[associatedDataBlockCount + messageBlockLength] = pad(combinedData[associatedDataBlockCount + messageBlockLength], 16);
|
|
let x = 8;
|
|
for (let i = 1; i < Math.floor((associatedDataBlockCount + messageBlockLength) / 2) + 1; i++) {
|
|
[state] = rho(state, combinedData[2 * i - 1]);
|
|
counter = increaseCounter(counter);
|
|
if (i === Math.floor(associatedDataBlockCount / 2) + 1) {
|
|
x = x ^ 4;
|
|
}
|
|
state = (0, skinny_128_384_plus_1.skinnyEncrypt)(state, (0, skinny_128_384_plus_1.tweakeyEncode)(counter, x, combinedData[2 * i], key));
|
|
counter = increaseCounter(counter);
|
|
}
|
|
if (associatedDataBlockCount % 2 === messageBlockLength % 2) {
|
|
[state] = rho(state, zeroedBuffer(16));
|
|
}
|
|
else {
|
|
[state] = rho(state, combinedData[associatedDataBlockCount + messageBlockLength]);
|
|
counter = increaseCounter(counter);
|
|
}
|
|
// Calculate authentication tag.
|
|
const [, computedTag] = rho((0, skinny_128_384_plus_1.skinnyEncrypt)(state, (0, skinny_128_384_plus_1.tweakeyEncode)(counter, domainSeparation, nonce, key)), zeroedBuffer(16));
|
|
let compare = 0;
|
|
for (let i = 0; i < 16; i++) {
|
|
compare |= (authenticationTag[i] ^ computedTag[i]);
|
|
}
|
|
if (compare !== 0) {
|
|
return [];
|
|
}
|
|
else {
|
|
return message;
|
|
}
|
|
}
|
|
exports.cryptoAeadDecrypt = cryptoAeadDecrypt;
|
|
//# sourceMappingURL=romulus-m.js.map
|