Improve code quality and add decrypt status return value

This commit is contained in:
Jack Hadrill
2022-02-05 22:55:31 +00:00
parent 338cb017f2
commit 5a042757dd
10 changed files with 184 additions and 75 deletions

View File

@@ -1,6 +1,29 @@
import { cryptoAeadDecrypt } from './romulus-m'
export function decrypt (ciphertext: Buffer, associatedData: Buffer, nonce: Buffer, key: Buffer): Buffer {
const plaintext = cryptoAeadDecrypt(Array.from(ciphertext), Array.from(associatedData), Array.from(nonce), Array.from(key))
return Buffer.from(plaintext)
interface DecryptResult {
success: boolean
plaintext: Buffer
}
/**
* Decrypt a Romulus-M encrypted message.
* N.B. Nonces are handled automatically by this function.
* @param buffer The nonce-prepended data to be decrypted.
* @param associatedData The associated data.
* @param key The encryption key.
* @returns A decrypted DecryptResult object.
*/
export function decrypt (buffer: Buffer, associatedData: Buffer, key: Buffer): DecryptResult {
// Split nonce from ciphertext.
const nonce = Array.from(buffer.slice(0, 16))
const ciphertext = Array.from(buffer.slice(16))
// Decrypt ciphertext using the associated data, nonce and encryption key.
const result = cryptoAeadDecrypt(ciphertext, Array.from(associatedData), nonce, Array.from(key))
// Return the ciphertext and decryption status.
return {
success: result.success,
plaintext: Buffer.from(result.plaintext)
}
}

View File

@@ -1,6 +1,22 @@
import { cryptoAeadEncrypt } from './romulus-m'
import { v4 as uuidv4 } from 'uuid'
export function encrypt (message: Buffer, associatedData: Buffer, nonce: Buffer, key: Buffer): Buffer {
const ciphertext = cryptoAeadEncrypt(Array.from(message), Array.from(associatedData), Array.from(nonce), Array.from(key))
return Buffer.from(ciphertext)
/**
* Encrypt a message using the Romulus-M encryption algorithm.
* N.B. A nonce is automatically prepended to the ciphertext using this function.
* @param message The plaintext message to encrypt.
* @param associatedData The associated data.
* @param key The encryption key.
* @returns The nonce-prepended ciphertext.
*/
export function encrypt (message: Buffer, associatedData: Buffer, key: Buffer): Buffer {
// Generate a nonce.
const nonce = Buffer.alloc(16)
uuidv4({}, nonce)
// Encrypt the data using the associated data, newly generated nonce and encryption key.
const ciphertext = Buffer.from(cryptoAeadEncrypt(Array.from(message), Array.from(associatedData), Array.from(nonce), Array.from(key)))
// Return the nonce-prepended ciphertext.
return Buffer.concat([nonce, ciphertext])
}

View File

@@ -14,13 +14,13 @@ function parse (message: number[], blockLength: number): number[][] {
// Slice message into blocks.
let ret: number[][] = []
while (message.length - cursor >= blockLength) {
ret.push(...[message.slice(cursor, 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)])
ret.push(message.slice(cursor))
}
// If no message, return a single block.
@@ -42,18 +42,18 @@ function parse (message: number[], blockLength: number): number[][] {
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 Array(16)
}
// Return a copy of the message if no padding is required.
if (message.length === padLength) {
return [...message]
return Array.from(message)
}
// Pad a copy of the message to padLength.
const ret = [...message]
const ret = Array.from(message)
const requiredPadding = padLength - message.length - 1
ret.push(...Array(requiredPadding).fill(0))
ret.push(...Array(requiredPadding))
// Set the final byte of the padded blocked to the length of the original message.
ret[padLength - 1] = message.length
@@ -83,12 +83,12 @@ function rho (state: number[], mBlock: number[]): [number[], number[]] {
const gOfS = g(state)
// C = M ⊕ G(S)
const c = [...Array(16).keys()].map(i => mBlock[i] ^ gOfS[i])
const cBlock = Array.from(Array(16).keys()).map(i => mBlock[i] ^ gOfS[i])
// S' = M ⊕ S
const nextState = [...Array(16).keys()].map(i => state[i] ^ mBlock[i])
const nextState = Array.from(Array(16).keys()).map(i => state[i] ^ mBlock[i])
return [nextState, c]
return [nextState, cBlock]
}
/**
@@ -102,10 +102,10 @@ function inverseRoh (state: number[], cBlock: number[]): [number[], number[]] {
const gOfS = g(state)
// M = C ⊕ G(S)
const mBlock = [...Array(16).keys()].map(i => cBlock[i] ^ gOfS[i])
const mBlock = Array.from(Array(16).keys()).map(i => cBlock[i] ^ gOfS[i])
// S' = S ⊕ M
const nextState = [...Array(16).keys()].map(i => state[i] ^ mBlock[i])
const nextState = Array.from(Array(16).keys()).map(i => state[i] ^ mBlock[i])
return [nextState, mBlock]
}
@@ -138,20 +138,11 @@ function increaseCounter (counter: number[]): number[] {
* @returns A reset counter.
*/
function resetCounter (): number[] {
const counter = Array(COUNTER_LENGTH).fill(0)
const counter = Array(COUNTER_LENGTH)
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,
@@ -182,6 +173,7 @@ function calculateDomainSeparation (combinedData: number[][], parsedMessageLengt
/**
* Encrypt a message using the Romulus-M cryptography specification.
* See https://romulusae.github.io/romulus/docs/Romulusv1.3.pdf for more information.
* @param message The message to encrypt.
* @param associatedData The associated data to encrypt.
* @param nonce A 128 bit nonce.
@@ -193,7 +185,7 @@ export function cryptoAeadEncrypt (message: number[], associatedData: number[],
const ciphertext = []
// Reset state and counter.
let state = zeroedBuffer(16)
let state = Array(16)
let counter = resetCounter()
// Carve message and associated data into blocks.
@@ -216,10 +208,8 @@ export function cryptoAeadEncrypt (message: number[], associatedData: number[],
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.
// Process the associated data.
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)
@@ -231,21 +221,23 @@ export function cryptoAeadEncrypt (message: number[], associatedData: number[],
}
if (associatedDataBlockCount % 2 === messageBlockCount % 2) {
[state] = rho(state, zeroedBuffer(16))
[state] = rho(state, Array(16))
} else {
[state] = rho(state, combinedDataBlocks[associatedDataBlockCount + messageBlockCount])
counter = increaseCounter(counter)
}
const [,authenticationTag] = rho(skinnyEncrypt(state, tweakeyEncode(counter, domainSeparation, nonce, key)), zeroedBuffer(16))
// Generate authentication tag.
const [,authenticationTag] = rho(skinnyEncrypt(state, tweakeyEncode(counter, domainSeparation, nonce, key)), Array(16))
if (message.length === 0) {
return authenticationTag
}
state = [...authenticationTag]
state = Array.from(authenticationTag)
counter = resetCounter()
// Encrypt the message.
const originalFinalMessageBlockLength = messageBlocks[messageBlockCount].length
messageBlocks[messageBlockCount] = pad(messageBlocks[messageBlockCount], 16)
@@ -263,35 +255,44 @@ export function cryptoAeadEncrypt (message: number[], associatedData: number[],
}
}
// The authentication tag is stored in the final 16 bytes of the ciphertext.
// Store the authentication tag in the final 16 bytes of the ciphertext.
ciphertext.push(...authenticationTag)
return ciphertext
}
/**
* Return interface for decrypting a message.
*/
export interface DecryptResult {
success: boolean
plaintext: number[]
}
/**
* Decrypt a message using the Romulus-M cryptography specification.
* See https://romulusae.github.io/romulus/docs/Romulusv1.3.pdf for more information.
* @param ciphertext The ciphertext to decrypt.
* @param associatedData The associated data.
* @param nonce The nonce.
* @param key The key.
* @returns The decrypted plaintext.
*/
export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[], nonce: number[], key: number[]): number[] {
export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[], nonce: number[], key: number[]): DecryptResult {
// Buffer for decrypted message.
const message = []
const cleartext = []
// 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 state = Array(16)
let counter = resetCounter()
if (ciphertext.length !== 0) {
// Combine the ciphertext
state = [...authenticationTag]
// Combine the ciphertext.
state = Array.from(authenticationTag)
const ciphertextBlocks = parse(ciphertext, 16)
const ciphertextBlockCount = ciphertextBlocks.length - 1
const finalCiphertextBlockLength = ciphertextBlocks[ciphertextBlockCount].length
@@ -305,9 +306,9 @@ export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[
counter = increaseCounter(counter)
if (i < ciphertextBlockCount) {
message.push(...mBlock)
cleartext.push(...mBlock)
} else {
message.push(...mBlock.slice(0, finalCiphertextBlockLength))
cleartext.push(...mBlock.slice(0, finalCiphertextBlockLength))
}
}
} else {
@@ -315,11 +316,11 @@ export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[
}
// Reset state and counter.
state = zeroedBuffer(16)
state = Array(16)
counter = resetCounter()
// Carve the message and associated data into blocks.
const messageBlocks = parse(message, 16)
const messageBlocks = parse(cleartext, 16)
const messageBlockLength = messageBlocks.length - 1
const associatedDataBlocks = parse(associatedData, 16)
@@ -338,6 +339,7 @@ export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[
combinedData[associatedDataBlockCount] = pad(combinedData[associatedDataBlockCount], 16)
combinedData[associatedDataBlockCount + messageBlockLength] = pad(combinedData[associatedDataBlockCount + messageBlockLength], 16)
// Verifiy associated data.
let x = 8
for (let i = 1; i < Math.floor((associatedDataBlockCount + messageBlockLength) / 2) + 1; i++) {
[state] = rho(state, combinedData[2 * i - 1])
@@ -350,23 +352,32 @@ export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[
}
if (associatedDataBlockCount % 2 === messageBlockLength % 2) {
[state] = rho(state, zeroedBuffer(16))
[state] = rho(state, Array(16))
} else {
[state] = rho(state, combinedData[associatedDataBlockCount + messageBlockLength])
counter = increaseCounter(counter)
}
// Calculate authentication tag.
const [,computedTag] = rho(skinnyEncrypt(state, tweakeyEncode(counter, domainSeparation, nonce, key)), zeroedBuffer(16))
const [,computedTag] = rho(skinnyEncrypt(state, tweakeyEncode(counter, domainSeparation, nonce, key)), Array(16))
// Validate authentication tag.
let compare = 0
for (let i = 0; i < 16; i++) {
compare |= (authenticationTag[i] ^ computedTag[i])
}
if (compare !== 0) {
return []
// Authentication failed.
return {
success: false,
plaintext: []
}
} else {
return message
// Decrypted successfully.
return {
success: true,
plaintext: cleartext
}
}
}

View File

@@ -9,7 +9,7 @@ import { MEMBER_MASK, NB_ROUNDS, TWEAK_LENGTH, PT, LFSR_8_TK2, LFSR_8_TK3, S8, C
* @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)
return counter.concat([domainSeparation ^ MEMBER_MASK], Array(8), nonce, key)
}
/**
@@ -21,10 +21,10 @@ export function tweakeyEncode (counter: number[], domainSeparation: number, nonc
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])
tk[0] = Array.from(Array(TWEAK_LENGTH).keys()).map(i => tweakey[i])
for (let i = 0; i < NB_ROUNDS - 1; i++) {
tk[i + 1] = [...tk[i]]
tk[i + 1] = Array.from(tk[i])
for (let j = 0; j < TWEAK_LENGTH; j++) {
tk[i + 1][j] = tk[i][j - j % 16 + PT[j % 16]]
@@ -36,7 +36,7 @@ export function skinnyEncrypt (plaintext: number[], tweakey: number[]): number[]
}
}
let s = [...Array(16).keys()].map(i => plaintext[i])
let s = Array.from(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]]
@@ -53,7 +53,7 @@ export function skinnyEncrypt (plaintext: number[], tweakey: number[]): number[]
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]
const tmp = Array.from(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]
@@ -61,5 +61,5 @@ export function skinnyEncrypt (plaintext: number[], tweakey: number[]): number[]
}
}
return [...Array(16).keys()].map(i => s[i])
return Array.from(Array(16).keys()).map(i => s[i])
}