From 1998293b65552e1248c28ec11a82afad86f29620 Mon Sep 17 00:00:00 2001 From: Jack Hadrill Date: Sat, 29 Jan 2022 22:25:00 +0000 Subject: [PATCH] Add encrypt method --- src/constants.ts | 59 +++++++++ src/romulus-m.ts | 246 ++++++++++++++++++++++++++++++++++++- src/skinny-128-384-plus.ts | 65 ++++++++++ tests/romulus-m.test.ts | 45 ++++++- 4 files changed, 410 insertions(+), 5 deletions(-) create mode 100644 src/constants.ts create mode 100644 src/skinny-128-384-plus.ts diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..f0ec10a --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,59 @@ +// SKINNY-128/384+ block cipher constants. +export const MEMBER_MASK = 32 +export const NB_ROUNDS = 40 +export const TWEAK_LENGTH = 48 +export const PT = [9, 15, 8, 13, 10, 14, 12, 11, 0, 1, 2, 3, 4, 5, 6, 7] +export const LFSR_8_TK2 = [ + 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, + 58, 60, 62, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99, 101, 103, 105, 107, 109, + 111, 113, 115, 117, 119, 121, 123, 125, 127, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, + 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 182, 184, 186, 188, 190, 193, 195, 197, + 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221, 223, 225, 227, 229, 231, 233, 235, 237, 239, 241, + 243, 245, 247, 249, 251, 253, 255, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, + 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, + 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 129, 131, 133, 135, 137, + 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, + 183, 185, 187, 189, 191, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, + 226, 228, 230, 232, 234, 236, 238, 240, 242, 244, 246, 248, 250, 252, 254 +] +export const LFSR_8_TK3 = [ + 0, 128, 1, 129, 2, 130, 3, 131, 4, 132, 5, 133, 6, 134, 7, 135, 8, 136, 9, 137, 10, 138, 11, 139, 12, 140, + 13, 141, 14, 142, 15, 143, 16, 144, 17, 145, 18, 146, 19, 147, 20, 148, 21, 149, 22, 150, 23, 151, 24, 152, + 25, 153, 26, 154, 27, 155, 28, 156, 29, 157, 30, 158, 31, 159, 160, 32, 161, 33, 162, 34, 163, 35, 164, 36, + 165, 37, 166, 38, 167, 39, 168, 40, 169, 41, 170, 42, 171, 43, 172, 44, 173, 45, 174, 46, 175, 47, 176, 48, + 177, 49, 178, 50, 179, 51, 180, 52, 181, 53, 182, 54, 183, 55, 184, 56, 185, 57, 186, 58, 187, 59, 188, 60, + 189, 61, 190, 62, 191, 63, 64, 192, 65, 193, 66, 194, 67, 195, 68, 196, 69, 197, 70, 198, 71, 199, 72, 200, + 73, 201, 74, 202, 75, 203, 76, 204, 77, 205, 78, 206, 79, 207, 80, 208, 81, 209, 82, 210, 83, 211, 84, 212, + 85, 213, 86, 214, 87, 215, 88, 216, 89, 217, 90, 218, 91, 219, 92, 220, 93, 221, 94, 222, 95, 223, 224, 96, + 225, 97, 226, 98, 227, 99, 228, 100, 229, 101, 230, 102, 231, 103, 232, 104, 233, 105, 234, 106, 235, 107, + 236, 108, 237, 109, 238, 110, 239, 111, 240, 112, 241, 113, 242, 114, 243, 115, 244, 116, 245, 117, 246, + 118, 247, 119, 248, 120, 249, 121, 250, 122, 251, 123, 252, 124, 253, 125, 254, 126, 255, 127 +] +export const S8 = [ + 0x65, 0x4c, 0x6a, 0x42, 0x4b, 0x63, 0x43, 0x6b, 0x55, 0x75, 0x5a, 0x7a, 0x53, 0x73, 0x5b, 0x7b, + 0x35, 0x8c, 0x3a, 0x81, 0x89, 0x33, 0x80, 0x3b, 0x95, 0x25, 0x98, 0x2a, 0x90, 0x23, 0x99, 0x2b, + 0xe5, 0xcc, 0xe8, 0xc1, 0xc9, 0xe0, 0xc0, 0xe9, 0xd5, 0xf5, 0xd8, 0xf8, 0xd0, 0xf0, 0xd9, 0xf9, + 0xa5, 0x1c, 0xa8, 0x12, 0x1b, 0xa0, 0x13, 0xa9, 0x05, 0xb5, 0x0a, 0xb8, 0x03, 0xb0, 0x0b, 0xb9, + 0x32, 0x88, 0x3c, 0x85, 0x8d, 0x34, 0x84, 0x3d, 0x91, 0x22, 0x9c, 0x2c, 0x94, 0x24, 0x9d, 0x2d, + 0x62, 0x4a, 0x6c, 0x45, 0x4d, 0x64, 0x44, 0x6d, 0x52, 0x72, 0x5c, 0x7c, 0x54, 0x74, 0x5d, 0x7d, + 0xa1, 0x1a, 0xac, 0x15, 0x1d, 0xa4, 0x14, 0xad, 0x02, 0xb1, 0x0c, 0xbc, 0x04, 0xb4, 0x0d, 0xbd, + 0xe1, 0xc8, 0xec, 0xc5, 0xcd, 0xe4, 0xc4, 0xed, 0xd1, 0xf1, 0xdc, 0xfc, 0xd4, 0xf4, 0xdd, 0xfd, + 0x36, 0x8e, 0x38, 0x82, 0x8b, 0x30, 0x83, 0x39, 0x96, 0x26, 0x9a, 0x28, 0x93, 0x20, 0x9b, 0x29, + 0x66, 0x4e, 0x68, 0x41, 0x49, 0x60, 0x40, 0x69, 0x56, 0x76, 0x58, 0x78, 0x50, 0x70, 0x59, 0x79, + 0xa6, 0x1e, 0xaa, 0x11, 0x19, 0xa3, 0x10, 0xab, 0x06, 0xb6, 0x08, 0xba, 0x00, 0xb3, 0x09, 0xbb, + 0xe6, 0xce, 0xea, 0xc2, 0xcb, 0xe3, 0xc3, 0xeb, 0xd6, 0xf6, 0xda, 0xfa, 0xd3, 0xf3, 0xdb, 0xfb, + 0x31, 0x8a, 0x3e, 0x86, 0x8f, 0x37, 0x87, 0x3f, 0x92, 0x21, 0x9e, 0x2e, 0x97, 0x27, 0x9f, 0x2f, + 0x61, 0x48, 0x6e, 0x46, 0x4f, 0x67, 0x47, 0x6f, 0x51, 0x71, 0x5e, 0x7e, 0x57, 0x77, 0x5f, 0x7f, + 0xa2, 0x18, 0xae, 0x16, 0x1f, 0xa7, 0x17, 0xaf, 0x01, 0xb2, 0x0e, 0xbe, 0x07, 0xb7, 0x0f, 0xbf, + 0xe2, 0xca, 0xee, 0xc6, 0xcf, 0xe7, 0xc7, 0xef, 0xd2, 0xf2, 0xde, 0xfe, 0xd7, 0xf7, 0xdf, 0xff +] +export const C = [ + 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3E, 0x3D, 0x3B, 0x37, 0x2F, + 0x1E, 0x3C, 0x39, 0x33, 0x27, 0x0E, 0x1D, 0x3A, 0x35, 0x2B, + 0x16, 0x2C, 0x18, 0x30, 0x21, 0x02, 0x05, 0x0B, 0x17, 0x2E, + 0x1C, 0x38, 0x31, 0x23, 0x06, 0x0D, 0x1B, 0x36, 0x2D, 0x1A +] + +// Romulus-M cryptography specification constants. +export const T_LENGTH = 16 +export const COUNTER_LENGTH = 7 diff --git a/src/romulus-m.ts b/src/romulus-m.ts index 01e5c14..9695303 100644 --- a/src/romulus-m.ts +++ b/src/romulus-m.ts @@ -1,3 +1,245 @@ -export function addNunbers (a: number, b: number): number { - return a + b +import { COUNTER_LENGTH } from './constants' +import { tweakeyEncode, skinnyEncrypt } from './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: number[], blockLength: number): number[][] { + // Keep track of position in message currently parsed into blocks. + let cursor = 0 + + // Slice message into blocks. + let ret: number[][] = [] + 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 + * @param padLength + * @returns A complete 16 byte block. + */ +function pad (message: number[], padLength: number): number[] { + // 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 + * @returns The key stream. + */ +function g (state: number[]): number[] { + 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: number[], mBlock: number[]): [number[], number[]] { + // 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] +} + +/** + * Increments the 56 bit LFSR-based counter. + * @param counter The old counter. + * @returns An incremented counter. + */ +function increaseCounter (counter: number[]): number[] { + 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 (): number[] { + const counter = Array(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: number): number[] { + 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: number[][], parsedMessageLength: number, parsedAssociatedDataLength: number): number { + 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 ciphertext. + */ +export function cryptoAeadEncrypt (message: number[], associatedData: number[], nonce: number[], key: number[]): number[] { + let counter = resetCounter() + + // Carve message and associated data into blocks. + const parsedMessage = parse(message, 16) + const parsedMessageLength = parsedMessage.length - 1 + + const parsedAssociatedData = parse(associatedData, 16) + const parsedAssociatedDataLength = parsedAssociatedData.length - 1 + + // Concatenate the parsed message and the associated data, excluding each array's first element. + const combinedData = parsedAssociatedData.slice(1).concat(parsedMessage.slice(1)) + + // Insert empty array at position 0. + combinedData.splice(0, 0, []) + + // Calculate domain separation for final encryption stage. + const domainSeparation = calculateDomainSeparation(combinedData, parsedMessageLength, parsedAssociatedDataLength) + + // Pad combined data. + combinedData[parsedAssociatedDataLength] = pad(combinedData[parsedAssociatedDataLength], 16) + combinedData[parsedAssociatedDataLength + parsedMessageLength] = pad(combinedData[parsedAssociatedDataLength + parsedMessageLength], 16) + + // Do the encryption. + // See https://romulusae.github.io/romulus/docs/Romulusv1.3.pdf for more information. + let state = zeroedBuffer(16) + let c = zeroedBuffer(16) + let x = 8 + for (let i = 1; i < Math.floor((parsedAssociatedDataLength + parsedMessageLength) / 2) + 1; i++) { + [state, c] = rho(state, combinedData[2 * i - 1]) + counter = increaseCounter(counter) + if (i === Math.floor(parsedAssociatedDataLength / 2) + 1) { + x = x ^ 4 + } + state = skinnyEncrypt(state, tweakeyEncode(counter, x, combinedData[2 * i], key)) + counter = increaseCounter(counter) + } + + if (parsedAssociatedDataLength % 2 === parsedMessageLength % 2) { + [state, c] = rho(state, zeroedBuffer(16)) + } else { + [state, c] = rho(state, combinedData[parsedAssociatedDataLength + parsedMessageLength]) + counter = increaseCounter(counter) + } + + [state, c] = rho(skinnyEncrypt(state, tweakeyEncode(counter, domainSeparation, nonce, key)), zeroedBuffer(16)) + if (message.length === 0) { + return c + } + + state = [...c] + + const ciphertext = [] + const originalLastBlockLength = parsedMessage[parsedMessageLength].length + parsedMessage[parsedMessageLength] = pad(parsedMessage[parsedMessageLength], 16) + + counter = resetCounter() + for (let i = 1; i < parsedMessageLength + 1; i++) { + state = skinnyEncrypt(state, tweakeyEncode(counter, 4, nonce, key)) + let x + [state, x] = rho(state, parsedMessage[i]) + counter = increaseCounter(counter) + if (i < parsedMessageLength) { + ciphertext.push(...x) + } else { + ciphertext.push(...x.slice(0, originalLastBlockLength)) + } + } + + ciphertext.push(...c) + + return ciphertext } diff --git a/src/skinny-128-384-plus.ts b/src/skinny-128-384-plus.ts new file mode 100644 index 0000000..916321c --- /dev/null +++ b/src/skinny-128-384-plus.ts @@ -0,0 +1,65 @@ +import { MEMBER_MASK, NB_ROUNDS, TWEAK_LENGTH, PT, LFSR_8_TK2, LFSR_8_TK3, S8, C } from './constants' + +/** + * Create a tweakey based on the specified domain separation, nonce, key and current counter state. + * @param counter The counter. + * @param domainSeparation The domain separation. + * @param nonce The nonce. + * @param key The encryption key. + * @returns The tweakey. + */ +export function tweakeyEncode (counter: number[], domainSeparation: number, nonce: number[], key: number[]): number[] { + return counter.concat([domainSeparation ^ MEMBER_MASK], Array(8).fill(0), nonce, key) +} + +/** + * Perform a round of SKINNY-188/384+ encryption. + * @param plaintext The plaintext to encrypt. + * @param tweakey The tweakey to use for encryption. + * @returns The ciphertext. + */ +export function skinnyEncrypt (plaintext: number[], tweakey: number[]): number[] { + const tk = Array(NB_ROUNDS + 1).fill(Array(TWEAK_LENGTH).fill(0)) + + tk[0] = [...Array(TWEAK_LENGTH).keys()].map(i => tweakey[i]) + + for (let i = 0; i < NB_ROUNDS - 1; i++) { + tk[i + 1] = [...tk[i]] + + for (let j = 0; j < TWEAK_LENGTH; j++) { + tk[i + 1][j] = tk[i][j - j % 16 + PT[j % 16]] + } + + for (let j = 0; j < 8; j++) { + tk[i + 1][j + 16] = LFSR_8_TK2[tk[i + 1][j + 16]] + tk[i + 1][j + 32] = LFSR_8_TK3[tk[i + 1][j + 32]] + } + } + + let s = [...Array(16).keys()].map(i => plaintext[i]) + for (let i = 0; i < NB_ROUNDS; i++) { + for (let j = 0; j < 16; j++) { + s[j] = S8[s[j]] + } + + s[0] ^= (C[i] & 0xf) + s[4] ^= (C[i] >> 4) & 0xf + s[8] ^= 0x2 + + for (let j = 0; j < 8; j++) { + s[j] ^= tk[i][j] ^ tk[i][j + 16] ^ tk[i][j + 32] + } + + s = [s[0], s[1], s[2], s[3], s[7], s[4], s[5], s[6], s[10], s[11], s[8], s[9], s[13], s[14], s[15], s[12]] + + for (let j = 0; j < 4; j++) { + const tmp = [...s] + s[j] = tmp[j] ^ tmp[8 + j] ^ tmp[12 + j] + s[4 + j] = tmp[j] + s[8 + j] = tmp[4 + j] ^ tmp[8 + j] + s[12 + j] = tmp[0 + j] ^ tmp[8 + j] + } + } + + return [...Array(16).keys()].map(i => s[i]) +} diff --git a/tests/romulus-m.test.ts b/tests/romulus-m.test.ts index 02e3ba5..dc32dd9 100644 --- a/tests/romulus-m.test.ts +++ b/tests/romulus-m.test.ts @@ -1,5 +1,44 @@ -import { addNunbers } from '../src/romulus-m' +import { cryptoAeadEncrypt } from '../src/romulus-m' -test('Adds two numbers together', () => { - expect(addNunbers(1, 3)).toBe(4) +function stringToArray (string: string): number[] { + const encoder = new TextEncoder() + return Array.from(encoder.encode(string)) +} + +test('Encrypt a message with no associated data.', () => { + // Given + const message = stringToArray('Hello, World! This is a test message.') + const associatedData = stringToArray('') + const nonce = stringToArray('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f') + const key = stringToArray('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f') + + // When + const result = cryptoAeadEncrypt(message, associatedData, nonce, key) + + // Then + const expectedResult = [ + 85, 125, 23, 244, 73, 241, 140, 72, 166, 113, 114, 78, 239, 211, 84, 113, 222, + 153, 207, 183, 69, 142, 174, 15, 38, 46, 112, 162, 229, 27, 136, 184, 163, 78, + 132, 42, 107, 160, 74, 115, 28, 251, 209, 37, 48, 57, 184, 204, 199, 247, 93, 5, 208 + ] + expect(result).toMatchObject(expectedResult) +}) + +test('Encrypt a message with associated data.', () => { + // Given + const message = stringToArray('Hello, World! This is a test message.') + const associatedData = stringToArray('Some associated data.') + const nonce = stringToArray('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f') + const key = stringToArray('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f') + + // When + const result = cryptoAeadEncrypt(message, associatedData, nonce, key) + + // Then + const expectedResult = [ + 225, 53, 3, 212, 22, 112, 246, 194, 61, 171, 230, 187, 157, 102, 32, 76, 62, 65, + 25, 202, 255, 201, 206, 49, 60, 58, 82, 216, 72, 116, 106, 129, 162, 142, 69, 40, + 167, 88, 94, 195, 174, 217, 242, 149, 224, 125, 196, 237, 172, 165, 116, 119, 128 + ] + expect(result).toMatchObject(expectedResult) })