feature/type-declarations #2
|
@ -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",
|
||||||
|
|
16
package.json
16
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue