feature/type-declarations #2

Merged
jackhadrill merged 10 commits from feature/type-declarations into master 2022-02-05 22:57:49 +00:00
11 changed files with 194 additions and 79 deletions

30
package-lock.json generated
View File

@ -7,9 +7,14 @@
"": { "": {
"name": "romulus-js", "name": "romulus-js",
"version": "1.0.0", "version": "1.0.0",
"hasInstallScript": true,
"license": "ISC", "license": "ISC",
"dependencies": {
"uuid": "^8.3.2"
},
"devDependencies": { "devDependencies": {
"@types/jest": "^27.4.0", "@types/jest": "^27.4.0",
"@types/uuid": "^8.3.4",
"jest": "^27.4.7", "jest": "^27.4.7",
"ts-jest": "^27.1.3", "ts-jest": "^27.1.3",
"ts-standard": "^11.0.0", "ts-standard": "^11.0.0",
@ -1220,6 +1225,12 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true "dev": true
}, },
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "16.0.4", "version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@ -6196,6 +6207,14 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache": { "node_modules/v8-compile-cache": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
@ -7411,6 +7430,12 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true "dev": true
}, },
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"@types/yargs": { "@types/yargs": {
"version": "16.0.4", "version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@ -11064,6 +11089,11 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"v8-compile-cache": { "v8-compile-cache": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",

View File

@ -2,11 +2,13 @@
"name": "romulus-js", "name": "romulus-js",
"version": "1.0.0", "version": "1.0.0",
"description": "An implementation of the Romulus-M cryptography specification.", "description": "An implementation of the Romulus-M cryptography specification.",
"main": "index.js", "main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": { "scripts": {
"lint": "ts-standard", "lint": "ts-standard",
"test": "jest", "test": "jest",
"build": "tsc" "build": "tsc",
"postinstall": "tsc"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -15,16 +17,20 @@
"author": "Butlersaurus", "author": "Butlersaurus",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"typescript": "^4.5.5", "@types/jest": "^27.4.0",
"ts-standard": "^11.0.0", "@types/uuid": "^8.3.4",
"jest": "^27.4.7", "jest": "^27.4.7",
"ts-jest": "^27.1.3", "ts-jest": "^27.1.3",
"@types/jest": "^27.4.0" "ts-standard": "^11.0.0",
"typescript": "^4.5.5"
}, },
"jest": { "jest": {
"verbose": true, "verbose": true,
"transform": { "transform": {
"^.+\\.ts?$": "ts-jest" "^.+\\.ts?$": "ts-jest"
} }
},
"dependencies": {
"uuid": "^8.3.2"
} }
} }

View File

@ -1,6 +1,29 @@
import { cryptoAeadDecrypt } from './romulus-m' import { cryptoAeadDecrypt } from './romulus-m'
export function decrypt (ciphertext: Buffer, associatedData: Buffer, nonce: Buffer, key: Buffer): Buffer { interface DecryptResult {
const plaintext = cryptoAeadDecrypt(Array.from(ciphertext), Array.from(associatedData), Array.from(nonce), Array.from(key)) success: boolean
return Buffer.from(plaintext) 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 { 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)) * Encrypt a message using the Romulus-M encryption algorithm.
return Buffer.from(ciphertext) * 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. // Slice message into blocks.
let ret: number[][] = [] let ret: number[][] = []
while (message.length - cursor >= blockLength) { while (message.length - cursor >= blockLength) {
ret.push(...[message.slice(cursor, cursor + blockLength)]) ret.push(message.slice(cursor, cursor + blockLength))
cursor = cursor + blockLength cursor = cursor + blockLength
} }
// Append any remaining blocks regardless of block length. These will be padded later. // Append any remaining blocks regardless of block length. These will be padded later.
if (message.length - cursor > 0) { if (message.length - cursor > 0) {
ret.push(...[message.slice(cursor)]) ret.push(message.slice(cursor))
} }
// If no message, return a single block. // 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[] { function pad (message: number[], padLength: number): number[] {
// If there is no message, return a fully padded block. // If there is no message, return a fully padded block.
if (message.length === 0) { if (message.length === 0) {
return Array(16).fill(0) return Array(16)
} }
// Return a copy of the message if no padding is required. // Return a copy of the message if no padding is required.
if (message.length === padLength) { if (message.length === padLength) {
return [...message] return Array.from(message)
} }
// Pad a copy of the message to padLength. // Pad a copy of the message to padLength.
const ret = [...message] const ret = Array.from(message)
const requiredPadding = padLength - message.length - 1 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. // Set the final byte of the padded blocked to the length of the original message.
ret[padLength - 1] = message.length ret[padLength - 1] = message.length
@ -83,12 +83,12 @@ function rho (state: number[], mBlock: number[]): [number[], number[]] {
const gOfS = g(state) const gOfS = g(state)
// C = M ⊕ G(S) // 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 // 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) const gOfS = g(state)
// M = C ⊕ G(S) // 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 // 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] return [nextState, mBlock]
} }
@ -138,20 +138,11 @@ function increaseCounter (counter: number[]): number[] {
* @returns A reset counter. * @returns A reset counter.
*/ */
function resetCounter (): number[] { function resetCounter (): number[] {
const counter = Array(COUNTER_LENGTH).fill(0) const counter = Array(COUNTER_LENGTH)
counter[0] = 1 counter[0] = 1
return counter 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. * Calculate the domain separation.
* @param combinedData The parsed and concatenated message and associated data, * @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. * 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 message The message to encrypt.
* @param associatedData The associated data to encrypt. * @param associatedData The associated data to encrypt.
* @param nonce A 128 bit nonce. * @param nonce A 128 bit nonce.
@ -193,7 +185,7 @@ export function cryptoAeadEncrypt (message: number[], associatedData: number[],
const ciphertext = [] const ciphertext = []
// Reset state and counter. // Reset state and counter.
let state = zeroedBuffer(16) let state = Array(16)
let counter = resetCounter() let counter = resetCounter()
// Carve message and associated data into blocks. // 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] = pad(combinedDataBlocks[associatedDataBlockCount], 16)
combinedDataBlocks[associatedDataBlockCount + messageBlockCount] = pad(combinedDataBlocks[associatedDataBlockCount + messageBlockCount], 16) combinedDataBlocks[associatedDataBlockCount + messageBlockCount] = pad(combinedDataBlocks[associatedDataBlockCount + messageBlockCount], 16)
// Do the encryption. // Process the associated data.
// See https://romulusae.github.io/romulus/docs/Romulusv1.3.pdf for more information.
let x = 8 let x = 8
for (let i = 1; i < Math.floor((associatedDataBlockCount + messageBlockCount) / 2) + 1; i++) { for (let i = 1; i < Math.floor((associatedDataBlockCount + messageBlockCount) / 2) + 1; i++) {
[state] = rho(state, combinedDataBlocks[2 * i - 1]) [state] = rho(state, combinedDataBlocks[2 * i - 1])
counter = increaseCounter(counter) counter = increaseCounter(counter)
@ -231,21 +221,23 @@ export function cryptoAeadEncrypt (message: number[], associatedData: number[],
} }
if (associatedDataBlockCount % 2 === messageBlockCount % 2) { if (associatedDataBlockCount % 2 === messageBlockCount % 2) {
[state] = rho(state, zeroedBuffer(16)) [state] = rho(state, Array(16))
} else { } else {
[state] = rho(state, combinedDataBlocks[associatedDataBlockCount + messageBlockCount]) [state] = rho(state, combinedDataBlocks[associatedDataBlockCount + messageBlockCount])
counter = increaseCounter(counter) 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) { if (message.length === 0) {
return authenticationTag return authenticationTag
} }
state = [...authenticationTag] state = Array.from(authenticationTag)
counter = resetCounter() counter = resetCounter()
// Encrypt the message.
const originalFinalMessageBlockLength = messageBlocks[messageBlockCount].length const originalFinalMessageBlockLength = messageBlocks[messageBlockCount].length
messageBlocks[messageBlockCount] = pad(messageBlocks[messageBlockCount], 16) 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) ciphertext.push(...authenticationTag)
return ciphertext return ciphertext
} }
/**
* Return interface for decrypting a message.
*/
export interface DecryptResult {
success: boolean
plaintext: number[]
}
/** /**
* Decrypt a message using the Romulus-M cryptography specification. * 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 ciphertext The ciphertext to decrypt.
* @param associatedData The associated data. * @param associatedData The associated data.
* @param nonce The nonce. * @param nonce The nonce.
* @param key The key. * @param key The key.
* @returns The decrypted plaintext. * @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. // Buffer for decrypted message.
const message = [] const cleartext = []
// The authentication tag is represented by the final 16 bytes of the ciphertext. // The authentication tag is represented by the final 16 bytes of the ciphertext.
const authenticationTag = ciphertext.slice(-16) const authenticationTag = ciphertext.slice(-16)
ciphertext.length -= 16 ciphertext.length -= 16
// Reset state and counter. // Reset state and counter.
let state = zeroedBuffer(16) let state = Array(16)
let counter = resetCounter() let counter = resetCounter()
if (ciphertext.length !== 0) { if (ciphertext.length !== 0) {
// Combine the ciphertext // Combine the ciphertext.
state = [...authenticationTag] state = Array.from(authenticationTag)
const ciphertextBlocks = parse(ciphertext, 16) const ciphertextBlocks = parse(ciphertext, 16)
const ciphertextBlockCount = ciphertextBlocks.length - 1 const ciphertextBlockCount = ciphertextBlocks.length - 1
const finalCiphertextBlockLength = ciphertextBlocks[ciphertextBlockCount].length const finalCiphertextBlockLength = ciphertextBlocks[ciphertextBlockCount].length
@ -305,9 +306,9 @@ export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[
counter = increaseCounter(counter) counter = increaseCounter(counter)
if (i < ciphertextBlockCount) { if (i < ciphertextBlockCount) {
message.push(...mBlock) cleartext.push(...mBlock)
} else { } else {
message.push(...mBlock.slice(0, finalCiphertextBlockLength)) cleartext.push(...mBlock.slice(0, finalCiphertextBlockLength))
} }
} }
} else { } else {
@ -315,11 +316,11 @@ export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[
} }
// Reset state and counter. // Reset state and counter.
state = zeroedBuffer(16) state = Array(16)
counter = resetCounter() counter = resetCounter()
// Carve the message and associated data into blocks. // Carve the message and associated data into blocks.
const messageBlocks = parse(message, 16) const messageBlocks = parse(cleartext, 16)
const messageBlockLength = messageBlocks.length - 1 const messageBlockLength = messageBlocks.length - 1
const associatedDataBlocks = parse(associatedData, 16) const associatedDataBlocks = parse(associatedData, 16)
@ -338,6 +339,7 @@ export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[
combinedData[associatedDataBlockCount] = pad(combinedData[associatedDataBlockCount], 16) combinedData[associatedDataBlockCount] = pad(combinedData[associatedDataBlockCount], 16)
combinedData[associatedDataBlockCount + messageBlockLength] = pad(combinedData[associatedDataBlockCount + messageBlockLength], 16) combinedData[associatedDataBlockCount + messageBlockLength] = pad(combinedData[associatedDataBlockCount + messageBlockLength], 16)
// Verifiy associated data.
let x = 8 let x = 8
for (let i = 1; i < Math.floor((associatedDataBlockCount + messageBlockLength) / 2) + 1; i++) { for (let i = 1; i < Math.floor((associatedDataBlockCount + messageBlockLength) / 2) + 1; i++) {
[state] = rho(state, combinedData[2 * i - 1]) [state] = rho(state, combinedData[2 * i - 1])
@ -350,23 +352,32 @@ export function cryptoAeadDecrypt (ciphertext: number[], associatedData: number[
} }
if (associatedDataBlockCount % 2 === messageBlockLength % 2) { if (associatedDataBlockCount % 2 === messageBlockLength % 2) {
[state] = rho(state, zeroedBuffer(16)) [state] = rho(state, Array(16))
} else { } else {
[state] = rho(state, combinedData[associatedDataBlockCount + messageBlockLength]) [state] = rho(state, combinedData[associatedDataBlockCount + messageBlockLength])
counter = increaseCounter(counter) counter = increaseCounter(counter)
} }
// Calculate authentication tag. // 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 let compare = 0
for (let i = 0; i < 16; i++) { for (let i = 0; i < 16; i++) {
compare |= (authenticationTag[i] ^ computedTag[i]) compare |= (authenticationTag[i] ^ computedTag[i])
} }
if (compare !== 0) { if (compare !== 0) {
return [] // Authentication failed.
return {
success: false,
plaintext: []
}
} else { } 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. * @returns The tweakey.
*/ */
export function tweakeyEncode (counter: number[], domainSeparation: number, nonce: number[], key: number[]): number[] { 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[] { export function skinnyEncrypt (plaintext: number[], tweakey: number[]): number[] {
const tk = Array(NB_ROUNDS + 1).fill(Array(TWEAK_LENGTH).fill(0)) 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++) { 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++) { for (let j = 0; j < TWEAK_LENGTH; j++) {
tk[i + 1][j] = tk[i][j - j % 16 + PT[j % 16]] 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 i = 0; i < NB_ROUNDS; i++) {
for (let j = 0; j < 16; j++) { for (let j = 0; j < 16; j++) {
s[j] = S8[s[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]] 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++) { 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[j] = tmp[j] ^ tmp[8 + j] ^ tmp[12 + j]
s[4 + j] = tmp[j] s[4 + j] = tmp[j]
s[8 + j] = tmp[4 + j] ^ tmp[8 + 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])
} }

View File

@ -1,21 +1,44 @@
import { decrypt } from '../src/decrypt' import { decrypt } from '../src/decrypt'
test('Test buffers are supported by decrypt function.', () => { test('Test nonce parsing by public decrypt function.', () => {
// Given // Given
const ciphertext = Buffer.from([ const ciphertext = Buffer.from([
// Nonce
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
// Ciphertext
225, 53, 3, 212, 22, 112, 246, 194, 61, 171, 230, 187, 157, 102, 32, 76, 62, 65, 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, 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 167, 88, 94, 195, 174, 217, 242, 149, 224, 125, 196, 237, 172, 165, 116, 119, 128
]) ])
const associatedData = Buffer.from('Some associated data.') const associatedData = Buffer.from('Some associated data.')
const nonce = Buffer.from('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f')
const key = Buffer.from('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f') const key = Buffer.from('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f')
// When // When
const result = decrypt(ciphertext, associatedData, nonce, key) const result = decrypt(ciphertext, associatedData, key)
// Then // Then
const expectedResult = Buffer.from('Hello, World! This is a test message.') const expectedResult = Buffer.from('Hello, World! This is a test message.')
expect(result.success).toBe(true)
expect(result).toMatchObject(expectedResult) expect(result.plaintext).toMatchObject(expectedResult)
})
test('Test decryption with an invalid key.', () => {
// Given
const ciphertext = Buffer.from([
// Nonce
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
// Ciphertext
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
])
const associatedData = Buffer.from('Some associated data.')
const key = Buffer.from('\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x00')
// When
const result = decrypt(ciphertext, associatedData, key)
// Then
expect(result.success).toBe(false)
expect(result.plaintext).toMatchObject(Buffer.alloc(0))
}) })

View File

@ -1,20 +1,17 @@
import { decrypt } from '../src/decrypt'
import { encrypt } from '../src/encrypt' import { encrypt } from '../src/encrypt'
test('Test buffers are supported by encrypt function.', () => { test('Test nonce generation by public encrypt function.', () => {
// Given // Given
const message = Buffer.from('Hello, World! This is a test message.') const message = Buffer.from('Hello, World! This is a test message.')
const associatedData = Buffer.from('Some associated data.') const associatedData = Buffer.from('Some associated data.')
const nonce = Buffer.from('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f')
const key = Buffer.from('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f') const key = Buffer.from('\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f')
// When // When
const result = encrypt(message, associatedData, nonce, key) const ciphertext = encrypt(message, associatedData, key)
const plaintext = decrypt(ciphertext, associatedData, key)
// Then // Then
const expectedResult = Buffer.from([ expect(plaintext.success).toBe(true)
225, 53, 3, 212, 22, 112, 246, 194, 61, 171, 230, 187, 157, 102, 32, 76, 62, 65, expect(plaintext.plaintext).toMatchObject(message)
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)
}) })

View File

@ -1,5 +1,5 @@
import { referenceTests } from './resources/reference-tests' import { referenceTests } from './resources/reference-tests'
import { cryptoAeadDecrypt, cryptoAeadEncrypt } from '../src/romulus-m' import { cryptoAeadDecrypt, cryptoAeadEncrypt, DecryptResult } from '../src/romulus-m'
function parseHexString (string: string): number[] { function parseHexString (string: string): number[] {
const ret = [] const ret = []
@ -24,6 +24,9 @@ test.each(referenceTests)('Perform decryption using reference test %#.', (key, n
const result = cryptoAeadDecrypt(parseHexString(ciphertext), parseHexString(associatedData), parseHexString(nonce), parseHexString(key)) const result = cryptoAeadDecrypt(parseHexString(ciphertext), parseHexString(associatedData), parseHexString(nonce), parseHexString(key))
// Then // Then
const expectedResult = parseHexString(plaintext) const expectedResult: DecryptResult = {
success: true,
plaintext: parseHexString(plaintext)
}
expect(result).toMatchObject(expectedResult) expect(result).toMatchObject(expectedResult)
}) })

View File

@ -59,7 +59,8 @@ test('Decrypt a message with no associated data.', () => {
// Then // Then
const expectedResult = stringToArray('Hello, World! This is a test message.') const expectedResult = stringToArray('Hello, World! This is a test message.')
expect(result).toMatchObject(expectedResult) expect(result.success).toBe(true)
expect(result.plaintext).toMatchObject(expectedResult)
}) })
test('Decrypt a message with associated data.', () => { test('Decrypt a message with associated data.', () => {
@ -78,5 +79,6 @@ test('Decrypt a message with associated data.', () => {
// Then // Then
const expectedResult = stringToArray('Hello, World! This is a test message.') const expectedResult = stringToArray('Hello, World! This is a test message.')
expect(result).toMatchObject(expectedResult) expect(result.success).toBe(true)
expect(result.plaintext).toMatchObject(expectedResult)
}) })

View File

@ -9,7 +9,11 @@
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */ "strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true, /* Skip type checking all .d.ts files. */
"declaration": true
}, },
"exclude": ["tests"] "exclude": [
"tests",
"dist"
]
} }