Modernise Romulus-M implementation and improve error handling
This commit is contained in:
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
*.min.js
|
||||
package-lock.json
|
||||
49
CLAUDE.md
Normal file
49
CLAUDE.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is romulus-js, a TypeScript implementation of the Romulus-M cryptography specification. It provides authenticated encryption with associated data (AEAD) functionality.
|
||||
|
||||
## Development Commands
|
||||
|
||||
- `npm run build` - Compile TypeScript to JavaScript (outputs to `dist/`)
|
||||
- `npm run test` - Run Jest test suite
|
||||
- `npm run lint` - Run ts-standard linter
|
||||
- `npm install` - Install dependencies (automatically runs `tsc` via postinstall)
|
||||
|
||||
## Architecture
|
||||
|
||||
The codebase implements the Romulus-M AEAD cipher with the following structure:
|
||||
|
||||
### Core Files
|
||||
|
||||
- `src/romulus-m.ts` - Main cryptographic implementation containing `cryptoAeadEncrypt` and `cryptoAeadDecrypt` functions
|
||||
- `src/skinny-128-384-plus.ts` - SKINNY-128-384+ block cipher implementation used by Romulus-M
|
||||
- `src/constants.ts` - Cryptographic constants
|
||||
|
||||
### Public API
|
||||
|
||||
- `src/encrypt.ts` - High-level encrypt function that auto-generates nonces using UUID v4
|
||||
- `src/decrypt.ts` - High-level decrypt function that handles nonce extraction
|
||||
- `src/index.ts` - Main entry point exporting encrypt/decrypt functions
|
||||
|
||||
### Key Implementation Details
|
||||
|
||||
- Uses 128-bit keys, nonces, and block sizes
|
||||
- Implements LFSR-based counter with 56-bit precision
|
||||
- Uses domain separation for different encryption phases
|
||||
- Nonces are automatically prepended to ciphertext in the high-level API
|
||||
- Low-level API (`romulus-m.ts`) works with `number[]` arrays
|
||||
- High-level API works with `Uint8Array` for better developer experience
|
||||
|
||||
### Testing
|
||||
|
||||
Tests are located in `tests/` directory using Jest framework. Test files include:
|
||||
|
||||
- `romulus-m.test.ts` - Tests for core cryptographic functions
|
||||
- `encrypt.test.ts` and `decrypt.test.ts` - Tests for high-level API
|
||||
- `romulus-m-reference.test.ts` - Reference implementation tests
|
||||
|
||||
The TypeScript configuration automatically compiles on install and uses ts-jest for testing.
|
||||
11577
package-lock.json
generated
11577
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -5,7 +5,8 @@
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"lint": "ts-standard",
|
||||
"lint": "prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"test": "jest",
|
||||
"build": "tsc",
|
||||
"postinstall": "tsc"
|
||||
@@ -17,12 +18,11 @@
|
||||
"author": "Butlersaurus",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.0",
|
||||
"jest": "^27.4.7",
|
||||
"@types/jest": "^30.0.0",
|
||||
"jest": "^30.1.3",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^27.1.3",
|
||||
"ts-standard": "^11.0.0",
|
||||
"typescript": "^4.5.5"
|
||||
"ts-jest": "^29.4.1",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"jest": {
|
||||
"verbose": true,
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/uuid": "^8.3.4",
|
||||
"uuid": "^8.3.2"
|
||||
"@types/uuid": "^10.0.0",
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,5 +69,8 @@ export const C = [
|
||||
];
|
||||
|
||||
// Romulus-M cryptography specification constants.
|
||||
export const T_LENGTH = 16;
|
||||
export const BLOCK_SIZE = 16;
|
||||
export const KEY_SIZE = 16;
|
||||
export const NONCE_SIZE = 16;
|
||||
export const AUTH_TAG_SIZE = 16;
|
||||
export const COUNTER_LENGTH = 7;
|
||||
|
||||
@@ -1,38 +1,60 @@
|
||||
import { cryptoAeadDecrypt } from "./romulus-m";
|
||||
|
||||
interface DecryptResult {
|
||||
success: boolean;
|
||||
plaintext: Uint8Array;
|
||||
}
|
||||
import {
|
||||
validateKey,
|
||||
validateAssociatedData,
|
||||
validateCiphertext,
|
||||
DecryptResult,
|
||||
NONCE_SIZE,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Decrypt a Romulus-M encrypted message with automatic nonce handling.
|
||||
* The nonce is automatically extracted from the beginning of the input.
|
||||
* @param buffer The nonce-prepended ciphertext to decrypt (nonce || ciphertext || tag).
|
||||
* @param associatedData The associated data used for authentication.
|
||||
* @param key The 16-byte decryption key.
|
||||
* @returns A DecryptResult object containing success status and plaintext.
|
||||
* @throws {InvalidKeyError} If the key is invalid.
|
||||
* @throws {InvalidInputError} If buffer or associated data is invalid.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const key = new Uint8Array(16); // Same key used for encryption
|
||||
* const associatedData = new TextEncoder().encode("metadata");
|
||||
* const result = decrypt(encryptedBuffer, associatedData, key);
|
||||
* if (result.success) {
|
||||
* const message = new TextDecoder().decode(result.plaintext);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function decrypt(
|
||||
buffer: Uint8Array,
|
||||
associatedData: Uint8Array,
|
||||
key: Uint8Array,
|
||||
): DecryptResult {
|
||||
// Split nonce from ciphertext.
|
||||
const nonce = Array.from(buffer.slice(0, 16));
|
||||
const ciphertext = Array.from(buffer.slice(16));
|
||||
// Validate inputs
|
||||
const validatedBuffer = validateCiphertext(buffer);
|
||||
const validatedAssociatedData = validateAssociatedData(associatedData);
|
||||
const validatedKey = validateKey(key);
|
||||
|
||||
// Decrypt ciphertext using the associated data, nonce and encryption key.
|
||||
// Ensure buffer is long enough to contain a nonce
|
||||
if (validatedBuffer.length < NONCE_SIZE) {
|
||||
return {
|
||||
success: false,
|
||||
plaintext: new Uint8Array(),
|
||||
};
|
||||
}
|
||||
|
||||
// Extract nonce and ciphertext
|
||||
const nonce = validatedBuffer.slice(0, NONCE_SIZE);
|
||||
const ciphertext = validatedBuffer.slice(NONCE_SIZE);
|
||||
|
||||
// Decrypt using the low-level AEAD function
|
||||
const result = cryptoAeadDecrypt(
|
||||
ciphertext,
|
||||
Array.from(associatedData),
|
||||
validatedAssociatedData,
|
||||
nonce,
|
||||
Array.from(key),
|
||||
validatedKey,
|
||||
);
|
||||
|
||||
// Return the ciphertext and decryption status.
|
||||
return {
|
||||
success: result.success,
|
||||
plaintext: Uint8Array.from(result.plaintext),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
import { cryptoAeadEncrypt } from "./romulus-m";
|
||||
import { validateKey, validateAssociatedData, validateMessage } from "./types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
* Encrypt a message using the Romulus-M encryption algorithm.
|
||||
* N.B. A nonce is automatically prepended to the ciphertext using this function.
|
||||
* Encrypt a message using the Romulus-M AEAD algorithm with auto-generated nonce.
|
||||
* The nonce is automatically prepended to the ciphertext.
|
||||
* @param message The plaintext message to encrypt.
|
||||
* @param associatedData The associated data.
|
||||
* @param key The encryption key.
|
||||
* @returns The nonce-prepended ciphertext.
|
||||
* @param associatedData The associated data for authentication.
|
||||
* @param key The 16-byte encryption key.
|
||||
* @returns The nonce-prepended ciphertext (nonce || ciphertext || tag).
|
||||
* @throws {InvalidKeyError} If the key is invalid.
|
||||
* @throws {InvalidInputError} If message or associated data is invalid.
|
||||
* @example
|
||||
* ```typescript
|
||||
* const key = new Uint8Array(16); // 16 zero bytes for example
|
||||
* const message = new TextEncoder().encode("Hello, world!");
|
||||
* const associatedData = new TextEncoder().encode("metadata");
|
||||
* const encrypted = encrypt(message, associatedData, key);
|
||||
* ```
|
||||
*/
|
||||
export function encrypt(
|
||||
message: Uint8Array,
|
||||
associatedData: Uint8Array,
|
||||
key: Uint8Array,
|
||||
): Uint8Array {
|
||||
// Generate a nonce.
|
||||
// Validate inputs
|
||||
const validatedMessage = validateMessage(message);
|
||||
const validatedAssociatedData = validateAssociatedData(associatedData);
|
||||
const validatedKey = validateKey(key);
|
||||
|
||||
// Generate a random nonce using UUID v4
|
||||
const nonce = new Uint8Array(16);
|
||||
uuidv4({}, nonce);
|
||||
|
||||
// Encrypt the data using the associated data, newly generated nonce and encryption key.
|
||||
const ciphertext = Uint8Array.from(
|
||||
cryptoAeadEncrypt(
|
||||
Array.from(message),
|
||||
Array.from(associatedData),
|
||||
Array.from(nonce),
|
||||
Array.from(key),
|
||||
),
|
||||
// Encrypt using the low-level AEAD function
|
||||
const ciphertext = cryptoAeadEncrypt(
|
||||
validatedMessage,
|
||||
validatedAssociatedData,
|
||||
nonce,
|
||||
validatedKey,
|
||||
);
|
||||
|
||||
// Return the nonce-prepended ciphertext.
|
||||
return new Uint8Array([...nonce, ...ciphertext]);
|
||||
// Return nonce prepended to ciphertext
|
||||
const result = new Uint8Array(nonce.length + ciphertext.length);
|
||||
result.set(nonce, 0);
|
||||
result.set(ciphertext, nonce.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
767
src/romulus-m.ts
767
src/romulus-m.ts
@@ -1,458 +1,615 @@
|
||||
import { COUNTER_LENGTH } from "./constants";
|
||||
import { COUNTER_LENGTH, BLOCK_SIZE, AUTH_TAG_SIZE } from "./constants";
|
||||
import { tweakeyEncode, skinnyEncrypt } from "./skinny-128-384-plus";
|
||||
import {
|
||||
validateKey,
|
||||
validateNonce,
|
||||
validateAssociatedData,
|
||||
validateMessage,
|
||||
validateCiphertext,
|
||||
createByteArray,
|
||||
xorBytes,
|
||||
copyBytes,
|
||||
DecryptResult,
|
||||
InvalidInputError,
|
||||
RomulusError,
|
||||
Key,
|
||||
Nonce,
|
||||
} from "./types";
|
||||
|
||||
// Re-export DecryptResult for backward compatibility
|
||||
export type { DecryptResult };
|
||||
|
||||
/**
|
||||
* Parse message into blocks.
|
||||
* @param message The message to parse.
|
||||
* @param blockLength The block length.
|
||||
* @returns An array of blocks.
|
||||
* Parse message into blocks of specified length.
|
||||
* @param message The message to parse as a Uint8Array.
|
||||
* @param blockLength The block length in bytes (typically 16).
|
||||
* @returns An array of message blocks, with empty block at index 0.
|
||||
*/
|
||||
function parse(message: number[], blockLength: number): number[][] {
|
||||
// Keep track of position in message currently parsed into blocks.
|
||||
function parseMessage(message: Uint8Array, blockLength: number): Uint8Array[] {
|
||||
const blocks: Uint8Array[] = [new Uint8Array(0)]; // Empty block at index 0
|
||||
|
||||
let cursor = 0;
|
||||
|
||||
// Slice message into blocks.
|
||||
let ret: number[][] = [];
|
||||
// Create full blocks
|
||||
while (message.length - cursor >= blockLength) {
|
||||
ret.push(message.slice(cursor, cursor + blockLength));
|
||||
cursor = cursor + blockLength;
|
||||
blocks.push(message.slice(cursor, cursor + blockLength));
|
||||
cursor += blockLength;
|
||||
}
|
||||
|
||||
// Append any remaining blocks regardless of block length. These will be padded later.
|
||||
// Add final partial block if any bytes remain
|
||||
if (message.length - cursor > 0) {
|
||||
ret.push(message.slice(cursor));
|
||||
blocks.push(message.slice(cursor));
|
||||
}
|
||||
|
||||
// If no message, return a single block.
|
||||
// If message is empty, add one empty block (in addition to index 0)
|
||||
if (message.length === 0) {
|
||||
ret = [[]];
|
||||
blocks.push(new Uint8Array(0));
|
||||
}
|
||||
|
||||
// Insert empty array at position 0.
|
||||
ret.splice(0, 0, []);
|
||||
return ret;
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads the byte length of message to padLength. The final byte (when padded) contains the original message length.
|
||||
* @param message The message to pad.
|
||||
* @param padLength The length to pad the message to.
|
||||
* @returns A padded block.
|
||||
* Pads a message block to the specified length using Romulus-M padding scheme.
|
||||
* The final byte contains the original message length.
|
||||
* @param message The message block to pad.
|
||||
* @param padLength The target length after padding (typically 16).
|
||||
* @returns A padded block of the specified length.
|
||||
*/
|
||||
function pad(message: number[], padLength: number): number[] {
|
||||
// If there is no message, return a fully padded block.
|
||||
if (message.length === 0) {
|
||||
return Array(16);
|
||||
function padBlock(message: Uint8Array, padLength: number): Uint8Array {
|
||||
if (padLength <= 0 || padLength > 255) {
|
||||
throw new InvalidInputError(`Invalid pad length: ${padLength}`);
|
||||
}
|
||||
|
||||
// Return a copy of the message if no padding is required.
|
||||
// If message is empty, return zero-filled block
|
||||
if (message.length === 0) {
|
||||
return createByteArray(padLength, 0);
|
||||
}
|
||||
|
||||
// If message is already the correct length, return a copy
|
||||
if (message.length === padLength) {
|
||||
return Array.from(message);
|
||||
return copyBytes(message);
|
||||
}
|
||||
|
||||
// Pad a copy of the message to padLength.
|
||||
const ret = Array.from(message);
|
||||
const requiredPadding = padLength - message.length - 1;
|
||||
ret.push(...Array(requiredPadding));
|
||||
// If message is too long, this is an error
|
||||
if (message.length > padLength) {
|
||||
throw new InvalidInputError(
|
||||
`Message too long for padding: ${message.length} > ${padLength}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Set the final byte of the padded blocked to the length of the original message.
|
||||
ret[padLength - 1] = message.length;
|
||||
// Create padded block
|
||||
const padded = createByteArray(padLength, 0);
|
||||
padded.set(message, 0);
|
||||
|
||||
return ret;
|
||||
// Set the final byte to the original message length
|
||||
padded[padLength - 1] = message.length;
|
||||
|
||||
return padded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the key stream from the internal state by multiplying the state S and the constant matrix G.
|
||||
* @param state The state from which the key stream will be generated.
|
||||
* @returns The key stream.
|
||||
* Generate the keystream from the internal state using the linear transformation G.
|
||||
* Implements the transformation: G(x) = (x >> 1) ⊕ (x & 0x80) ⊕ ((x & 0x01) << 7)
|
||||
* @param state The 16-byte internal state.
|
||||
* @returns The 16-byte keystream.
|
||||
*/
|
||||
function g(state: number[]): number[] {
|
||||
return state.map((x) => {
|
||||
return (x >> 1) ^ (x & 0x80) ^ ((x & 0x01) << 7);
|
||||
});
|
||||
function generateKeystream(state: Uint8Array): Uint8Array {
|
||||
if (state.length !== BLOCK_SIZE) {
|
||||
throw new InvalidInputError(
|
||||
`State must be ${BLOCK_SIZE} bytes, got ${state.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const keystream = new Uint8Array(BLOCK_SIZE);
|
||||
for (let i = 0; i < BLOCK_SIZE; i++) {
|
||||
const x = state[i];
|
||||
keystream[i] = (x >> 1) ^ (x & 0x80) ^ ((x & 0x01) << 7);
|
||||
}
|
||||
return keystream;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* State update function for encryption: ρ(S, M) = (S ⊕ M, M ⊕ G(S))
|
||||
* @param state The current 16-byte internal state S.
|
||||
* @param messageBlock The 16-byte message block M.
|
||||
* @returns [nextState, ciphertextBlock] where nextState = S ⊕ M and ciphertextBlock = M ⊕ G(S)
|
||||
*/
|
||||
function rho(state: number[], mBlock: number[]): [number[], number[]] {
|
||||
// G(S)
|
||||
const gOfS = g(state);
|
||||
function stateUpdateEncrypt(
|
||||
state: Uint8Array,
|
||||
messageBlock: Uint8Array,
|
||||
): [Uint8Array, Uint8Array] {
|
||||
if (state.length !== BLOCK_SIZE) {
|
||||
throw new InvalidInputError(
|
||||
`State must be ${BLOCK_SIZE} bytes, got ${state.length}`,
|
||||
);
|
||||
}
|
||||
if (messageBlock.length !== BLOCK_SIZE) {
|
||||
throw new InvalidInputError(
|
||||
`Message block must be ${BLOCK_SIZE} bytes, got ${messageBlock.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Generate keystream G(S)
|
||||
const keystream = generateKeystream(state);
|
||||
|
||||
// C = M ⊕ G(S)
|
||||
const cBlock = Array.from(Array(16).keys()).map((i) => mBlock[i] ^ gOfS[i]);
|
||||
|
||||
// S' = M ⊕ S
|
||||
const nextState = Array.from(Array(16).keys()).map(
|
||||
(i) => state[i] ^ mBlock[i],
|
||||
);
|
||||
|
||||
return [nextState, cBlock];
|
||||
}
|
||||
|
||||
/**
|
||||
* The state update function. Pads a C block.
|
||||
* @param state The internal state, S.
|
||||
* @param cBlock A C block.
|
||||
* @returns [S', M] where M = C ⊕ G(S) and S' = C ⊕ M.
|
||||
*/
|
||||
function inverseRoh(state: number[], cBlock: number[]): [number[], number[]] {
|
||||
// G(S)
|
||||
const gOfS = g(state);
|
||||
|
||||
// M = C ⊕ G(S)
|
||||
const mBlock = Array.from(Array(16).keys()).map((i) => cBlock[i] ^ gOfS[i]);
|
||||
const ciphertextBlock = xorBytes(messageBlock, keystream);
|
||||
|
||||
// S' = S ⊕ M
|
||||
const nextState = Array.from(Array(16).keys()).map(
|
||||
(i) => state[i] ^ mBlock[i],
|
||||
);
|
||||
return [nextState, mBlock];
|
||||
const nextState = xorBytes(state, messageBlock);
|
||||
|
||||
return [nextState, ciphertextBlock];
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the 56 bit LFSR-based counter.
|
||||
* @param counter The old counter.
|
||||
* @returns An incremented counter.
|
||||
* State update function for decryption: ρ⁻¹(S, C) = (S ⊕ M, C ⊕ G(S)) where M = C ⊕ G(S)
|
||||
* @param state The current 16-byte internal state S.
|
||||
* @param ciphertextBlock The 16-byte ciphertext block C.
|
||||
* @returns [nextState, messageBlock] where M = C ⊕ G(S) and nextState = S ⊕ M
|
||||
*/
|
||||
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;
|
||||
function stateUpdateDecrypt(
|
||||
state: Uint8Array,
|
||||
ciphertextBlock: Uint8Array,
|
||||
): [Uint8Array, Uint8Array] {
|
||||
if (state.length !== BLOCK_SIZE) {
|
||||
throw new InvalidInputError(
|
||||
`State must be ${BLOCK_SIZE} bytes, got ${state.length}`,
|
||||
);
|
||||
}
|
||||
if (ciphertextBlock.length !== BLOCK_SIZE) {
|
||||
throw new InvalidInputError(
|
||||
`Ciphertext block must be ${BLOCK_SIZE} bytes, got ${ciphertextBlock.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
return counter;
|
||||
// Generate keystream G(S)
|
||||
const keystream = generateKeystream(state);
|
||||
|
||||
// M = C ⊕ G(S)
|
||||
const messageBlock = xorBytes(ciphertextBlock, keystream);
|
||||
|
||||
// S' = S ⊕ M
|
||||
const nextState = xorBytes(state, messageBlock);
|
||||
|
||||
return [nextState, messageBlock];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reset counter.
|
||||
* @returns A reset counter.
|
||||
* Increments the 56-bit LFSR-based counter (pure function).
|
||||
* @param counter The current 7-byte counter state.
|
||||
* @returns A new incremented counter (does not modify input).
|
||||
*/
|
||||
function resetCounter(): number[] {
|
||||
const counter = Array(COUNTER_LENGTH);
|
||||
function incrementCounter(counter: Uint8Array): Uint8Array {
|
||||
if (counter.length !== COUNTER_LENGTH) {
|
||||
throw new InvalidInputError(
|
||||
`Counter must be ${COUNTER_LENGTH} bytes, got ${counter.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create a copy to avoid mutation
|
||||
const newCounter = copyBytes(counter);
|
||||
|
||||
// Extract feedback bit from MSB of byte 6
|
||||
const fb0 = newCounter[6] >> 7;
|
||||
|
||||
// Shift left by 1 bit across all bytes (big-endian)
|
||||
newCounter[6] = (newCounter[6] << 1) | (newCounter[5] >> 7);
|
||||
newCounter[5] = (newCounter[5] << 1) | (newCounter[4] >> 7);
|
||||
newCounter[4] = (newCounter[4] << 1) | (newCounter[3] >> 7);
|
||||
newCounter[3] = (newCounter[3] << 1) | (newCounter[2] >> 7);
|
||||
newCounter[2] = (newCounter[2] << 1) | (newCounter[1] >> 7);
|
||||
newCounter[1] = (newCounter[1] << 1) | (newCounter[0] >> 7);
|
||||
|
||||
// Apply LFSR feedback polynomial if feedback bit is set
|
||||
if (fb0 === 1) {
|
||||
newCounter[0] = (newCounter[0] << 1) ^ 0x95;
|
||||
} else {
|
||||
newCounter[0] = newCounter[0] << 1;
|
||||
}
|
||||
|
||||
// Ensure all bytes remain in valid range
|
||||
for (let i = 0; i < COUNTER_LENGTH; i++) {
|
||||
newCounter[i] &= 0xff;
|
||||
}
|
||||
|
||||
return newCounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reset counter initialized to the starting state.
|
||||
* @returns A new 7-byte counter initialized to [1, 0, 0, 0, 0, 0, 0].
|
||||
*/
|
||||
function createResetCounter(): Uint8Array {
|
||||
const counter = createByteArray(COUNTER_LENGTH, 0);
|
||||
counter[0] = 1;
|
||||
return counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Calculate the domain separation value based on message and associated data characteristics.
|
||||
* @param combinedBlocks The parsed and concatenated message and associated data blocks.
|
||||
* @param messageBlockCount The number of message blocks.
|
||||
* @param associatedDataBlockCount The number of associated data blocks.
|
||||
* @returns The domain separation value for the final encryption step.
|
||||
*/
|
||||
function calculateDomainSeparation(
|
||||
combinedData: number[][],
|
||||
parsedMessageLength: number,
|
||||
parsedAssociatedDataLength: number,
|
||||
combinedBlocks: Uint8Array[],
|
||||
messageBlockCount: number,
|
||||
associatedDataBlockCount: number,
|
||||
): number {
|
||||
let domainSeparation = 16;
|
||||
|
||||
if (combinedData[parsedAssociatedDataLength].length < 16) {
|
||||
domainSeparation = domainSeparation ^ 2;
|
||||
// Check if the final associated data block is incomplete
|
||||
if (combinedBlocks[associatedDataBlockCount].length < BLOCK_SIZE) {
|
||||
domainSeparation ^= 2;
|
||||
}
|
||||
|
||||
// Check if the final message block is incomplete
|
||||
if (
|
||||
combinedData[parsedAssociatedDataLength + parsedMessageLength].length < 16
|
||||
combinedBlocks[associatedDataBlockCount + messageBlockCount].length <
|
||||
BLOCK_SIZE
|
||||
) {
|
||||
domainSeparation = domainSeparation ^ 1;
|
||||
domainSeparation ^= 1;
|
||||
}
|
||||
|
||||
if (parsedAssociatedDataLength % 2 === 0) {
|
||||
domainSeparation = domainSeparation ^ 8;
|
||||
// Check if associated data block count is even
|
||||
if (associatedDataBlockCount % 2 === 0) {
|
||||
domainSeparation ^= 8;
|
||||
}
|
||||
|
||||
if (parsedMessageLength % 2 === 0) {
|
||||
domainSeparation = domainSeparation ^ 4;
|
||||
// Check if message block count is even
|
||||
if (messageBlockCount % 2 === 0) {
|
||||
domainSeparation ^= 4;
|
||||
}
|
||||
|
||||
return domainSeparation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a message using the Romulus-M cryptography specification.
|
||||
* Encrypt a message using the Romulus-M AEAD 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.
|
||||
* @param key A 128 bit encryption key.
|
||||
* @returns The encrypted ciphertext.
|
||||
* @param message The message to encrypt as a byte array.
|
||||
* @param associatedData The associated data as a byte array.
|
||||
* @param nonce A 16-byte nonce.
|
||||
* @param key A 16-byte encryption key.
|
||||
* @returns The encrypted ciphertext with authentication tag appended.
|
||||
* @throws {InvalidInputError} If inputs have incorrect lengths or invalid values.
|
||||
*/
|
||||
export function cryptoAeadEncrypt(
|
||||
message: number[],
|
||||
associatedData: number[],
|
||||
nonce: number[],
|
||||
key: number[],
|
||||
): number[] {
|
||||
// Buffer for ciphertext.
|
||||
const ciphertext = [];
|
||||
message: Uint8Array,
|
||||
associatedData: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
key: Uint8Array,
|
||||
): Uint8Array {
|
||||
// Validate all inputs
|
||||
const validatedMessage = validateMessage(message);
|
||||
const validatedAssociatedData = validateAssociatedData(associatedData);
|
||||
const validatedNonce = validateNonce(nonce);
|
||||
const validatedKey = validateKey(key);
|
||||
|
||||
// Reset state and counter.
|
||||
let state = Array(16);
|
||||
let counter = resetCounter();
|
||||
try {
|
||||
// Initialize state and counter
|
||||
let state = createByteArray(BLOCK_SIZE, 0);
|
||||
let counter = createResetCounter();
|
||||
|
||||
// Carve message and associated data into blocks.
|
||||
const messageBlocks = parse(message, 16);
|
||||
// Parse message and associated data into blocks
|
||||
const messageBlocks = parseMessage(validatedMessage, BLOCK_SIZE);
|
||||
const messageBlockCount = messageBlocks.length - 1;
|
||||
|
||||
const associatedDataBlocks = parse(associatedData, 16);
|
||||
const associatedDataBlocks = parseMessage(
|
||||
validatedAssociatedData,
|
||||
BLOCK_SIZE,
|
||||
);
|
||||
const associatedDataBlockCount = associatedDataBlocks.length - 1;
|
||||
|
||||
// Concatenate the message and associated data blocks, excluding each array's first element.
|
||||
const combinedDataBlocks = associatedDataBlocks
|
||||
.slice(1)
|
||||
.concat(messageBlocks.slice(1));
|
||||
// Concatenate blocks (excluding empty block at index 0)
|
||||
const combinedBlocks = [
|
||||
createByteArray(0), // Empty block at index 0
|
||||
...associatedDataBlocks.slice(1),
|
||||
...messageBlocks.slice(1),
|
||||
];
|
||||
|
||||
// Insert empty array at position 0.
|
||||
combinedDataBlocks.splice(0, 0, []);
|
||||
|
||||
// Calculate domain separation for final encryption stage.
|
||||
// Calculate domain separation for final encryption stage
|
||||
const domainSeparation = calculateDomainSeparation(
|
||||
combinedDataBlocks,
|
||||
combinedBlocks,
|
||||
messageBlockCount,
|
||||
associatedDataBlockCount,
|
||||
);
|
||||
|
||||
// Pad combined data.
|
||||
combinedDataBlocks[associatedDataBlockCount] = pad(
|
||||
combinedDataBlocks[associatedDataBlockCount],
|
||||
16,
|
||||
// Pad the final blocks
|
||||
if (associatedDataBlockCount > 0) {
|
||||
combinedBlocks[associatedDataBlockCount] = padBlock(
|
||||
combinedBlocks[associatedDataBlockCount],
|
||||
BLOCK_SIZE,
|
||||
);
|
||||
combinedDataBlocks[associatedDataBlockCount + messageBlockCount] = pad(
|
||||
combinedDataBlocks[associatedDataBlockCount + messageBlockCount],
|
||||
16,
|
||||
);
|
||||
|
||||
// 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);
|
||||
if (i === Math.floor(associatedDataBlockCount / 2) + 1) {
|
||||
x = x ^ 4;
|
||||
}
|
||||
if (messageBlockCount > 0) {
|
||||
combinedBlocks[associatedDataBlockCount + messageBlockCount] = padBlock(
|
||||
combinedBlocks[associatedDataBlockCount + messageBlockCount],
|
||||
BLOCK_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
// Process associated data and message blocks
|
||||
let x = 8;
|
||||
const totalRounds =
|
||||
Math.floor((associatedDataBlockCount + messageBlockCount) / 2) + 1;
|
||||
|
||||
for (let i = 1; i < totalRounds; i++) {
|
||||
// State update with odd-indexed block
|
||||
[state] = stateUpdateEncrypt(state, combinedBlocks[2 * i - 1]);
|
||||
counter = incrementCounter(counter);
|
||||
|
||||
// Switch domain constant at transition point
|
||||
if (i === Math.floor(associatedDataBlockCount / 2) + 1) {
|
||||
x ^= 4;
|
||||
}
|
||||
|
||||
// SKINNY encryption with even-indexed block
|
||||
state = skinnyEncrypt(
|
||||
state,
|
||||
tweakeyEncode(counter, x, combinedDataBlocks[2 * i], key),
|
||||
tweakeyEncode(counter, x, combinedBlocks[2 * i], validatedKey),
|
||||
);
|
||||
counter = increaseCounter(counter);
|
||||
counter = incrementCounter(counter);
|
||||
}
|
||||
|
||||
// Handle final block based on parity
|
||||
const emptyBlock = createByteArray(BLOCK_SIZE, 0);
|
||||
if (associatedDataBlockCount % 2 === messageBlockCount % 2) {
|
||||
[state] = rho(state, Array(16));
|
||||
[state] = stateUpdateEncrypt(state, emptyBlock);
|
||||
} else {
|
||||
[state] = rho(
|
||||
[state] = stateUpdateEncrypt(
|
||||
state,
|
||||
combinedDataBlocks[associatedDataBlockCount + messageBlockCount],
|
||||
combinedBlocks[associatedDataBlockCount + messageBlockCount],
|
||||
);
|
||||
counter = increaseCounter(counter);
|
||||
counter = incrementCounter(counter);
|
||||
}
|
||||
|
||||
// Generate authentication tag.
|
||||
const [, authenticationTag] = rho(
|
||||
skinnyEncrypt(state, tweakeyEncode(counter, domainSeparation, nonce, key)),
|
||||
Array(16),
|
||||
// Generate authentication tag
|
||||
const encryptedState = skinnyEncrypt(
|
||||
state,
|
||||
tweakeyEncode(counter, domainSeparation, validatedNonce, validatedKey),
|
||||
);
|
||||
const [, authenticationTag] = stateUpdateEncrypt(
|
||||
encryptedState,
|
||||
emptyBlock,
|
||||
);
|
||||
|
||||
if (message.length === 0) {
|
||||
// If message is empty, return only the authentication tag
|
||||
if (validatedMessage.length === 0) {
|
||||
return authenticationTag;
|
||||
}
|
||||
|
||||
state = Array.from(authenticationTag);
|
||||
counter = resetCounter();
|
||||
// Encrypt the message blocks
|
||||
state = copyBytes(authenticationTag);
|
||||
counter = createResetCounter();
|
||||
|
||||
// Encrypt the message.
|
||||
const originalFinalMessageBlockLength =
|
||||
messageBlocks[messageBlockCount].length;
|
||||
messageBlocks[messageBlockCount] = pad(messageBlocks[messageBlockCount], 16);
|
||||
|
||||
for (let i = 1; i < messageBlockCount + 1; i++) {
|
||||
state = skinnyEncrypt(state, tweakeyEncode(counter, 4, nonce, key));
|
||||
|
||||
let cBlock;
|
||||
[state, cBlock] = rho(state, messageBlocks[i]);
|
||||
counter = increaseCounter(counter);
|
||||
|
||||
if (i < messageBlockCount) {
|
||||
ciphertext.push(...cBlock);
|
||||
} else {
|
||||
ciphertext.push(...cBlock.slice(0, originalFinalMessageBlockLength));
|
||||
}
|
||||
}
|
||||
|
||||
// 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[],
|
||||
): DecryptResult {
|
||||
// Buffer for decrypted 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 = Array(16);
|
||||
let counter = resetCounter();
|
||||
|
||||
if (ciphertext.length !== 0) {
|
||||
// Combine the ciphertext.
|
||||
state = Array.from(authenticationTag);
|
||||
const ciphertextBlocks = parse(ciphertext, 16);
|
||||
const ciphertextBlockCount = ciphertextBlocks.length - 1;
|
||||
const finalCiphertextBlockLength =
|
||||
ciphertextBlocks[ciphertextBlockCount].length;
|
||||
ciphertextBlocks[ciphertextBlockCount] = pad(
|
||||
ciphertextBlocks[ciphertextBlockCount],
|
||||
16,
|
||||
const ciphertext: number[] = [];
|
||||
const originalFinalBlockLength = messageBlocks[messageBlockCount].length;
|
||||
messageBlocks[messageBlockCount] = padBlock(
|
||||
messageBlocks[messageBlockCount],
|
||||
BLOCK_SIZE,
|
||||
);
|
||||
|
||||
for (let i = 1; i < ciphertextBlockCount + 1; i++) {
|
||||
state = skinnyEncrypt(state, tweakeyEncode(counter, 4, nonce, key));
|
||||
for (let i = 1; i <= messageBlockCount; i++) {
|
||||
state = skinnyEncrypt(
|
||||
state,
|
||||
tweakeyEncode(counter, 4, validatedNonce, validatedKey),
|
||||
);
|
||||
|
||||
let mBlock;
|
||||
[state, mBlock] = inverseRoh(state, ciphertextBlocks[i]);
|
||||
counter = increaseCounter(counter);
|
||||
const [nextState, ciphertextBlock] = stateUpdateEncrypt(
|
||||
state,
|
||||
messageBlocks[i],
|
||||
);
|
||||
state = nextState;
|
||||
counter = incrementCounter(counter);
|
||||
|
||||
// Add ciphertext to output (truncate final block to original length)
|
||||
if (i < messageBlockCount) {
|
||||
ciphertext.push(...Array.from(ciphertextBlock));
|
||||
} else {
|
||||
ciphertext.push(
|
||||
...Array.from(ciphertextBlock.slice(0, originalFinalBlockLength)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Append authentication tag to ciphertext
|
||||
ciphertext.push(...Array.from(authenticationTag));
|
||||
|
||||
return new Uint8Array(ciphertext);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
throw error;
|
||||
}
|
||||
throw new RomulusError(
|
||||
`Encryption failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a message using the Romulus-M AEAD cryptography specification.
|
||||
* See https://romulusae.github.io/romulus/docs/Romulusv1.3.pdf for more information.
|
||||
* @param ciphertext The ciphertext to decrypt (includes authentication tag).
|
||||
* @param associatedData The associated data as a byte array.
|
||||
* @param nonce The 16-byte nonce.
|
||||
* @param key The 16-byte decryption key.
|
||||
* @returns The decryption result with success status and plaintext.
|
||||
* @throws {InvalidInputError} If inputs have incorrect lengths or invalid values.
|
||||
*/
|
||||
export function cryptoAeadDecrypt(
|
||||
ciphertext: Uint8Array,
|
||||
associatedData: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
key: Uint8Array,
|
||||
): DecryptResult {
|
||||
// Validate all inputs
|
||||
const validatedCiphertext = validateCiphertext(ciphertext);
|
||||
const validatedAssociatedData = validateAssociatedData(associatedData);
|
||||
const validatedNonce = validateNonce(nonce);
|
||||
const validatedKey = validateKey(key);
|
||||
|
||||
try {
|
||||
// Extract authentication tag from the end of ciphertext
|
||||
const authenticationTag = validatedCiphertext.slice(-AUTH_TAG_SIZE);
|
||||
const actualCiphertext = validatedCiphertext.slice(0, -AUTH_TAG_SIZE);
|
||||
|
||||
let plaintextBytes: number[] = [];
|
||||
|
||||
// Decrypt message blocks if ciphertext is not empty
|
||||
if (actualCiphertext.length > 0) {
|
||||
let state = copyBytes(authenticationTag);
|
||||
let counter = createResetCounter();
|
||||
|
||||
// Parse ciphertext into blocks
|
||||
const ciphertextBlocks = parseMessage(actualCiphertext, BLOCK_SIZE);
|
||||
const ciphertextBlockCount = ciphertextBlocks.length - 1;
|
||||
const finalBlockLength = ciphertextBlocks[ciphertextBlockCount].length;
|
||||
|
||||
// Pad final block
|
||||
ciphertextBlocks[ciphertextBlockCount] = padBlock(
|
||||
ciphertextBlocks[ciphertextBlockCount],
|
||||
BLOCK_SIZE,
|
||||
);
|
||||
|
||||
// Decrypt each block
|
||||
for (let i = 1; i <= ciphertextBlockCount; i++) {
|
||||
state = skinnyEncrypt(
|
||||
state,
|
||||
tweakeyEncode(counter, 4, validatedNonce, validatedKey),
|
||||
);
|
||||
|
||||
const [nextState, messageBlock] = stateUpdateDecrypt(
|
||||
state,
|
||||
ciphertextBlocks[i],
|
||||
);
|
||||
state = nextState;
|
||||
counter = incrementCounter(counter);
|
||||
|
||||
// Add plaintext to output (truncate final block to original length)
|
||||
if (i < ciphertextBlockCount) {
|
||||
cleartext.push(...mBlock);
|
||||
plaintextBytes.push(...Array.from(messageBlock));
|
||||
} else {
|
||||
cleartext.push(...mBlock.slice(0, finalCiphertextBlockLength));
|
||||
plaintextBytes.push(
|
||||
...Array.from(messageBlock.slice(0, finalBlockLength)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state = [];
|
||||
}
|
||||
|
||||
// Reset state and counter.
|
||||
state = Array(16);
|
||||
counter = resetCounter();
|
||||
// Verify authentication by recomputing the tag
|
||||
const recoveredPlaintext = new Uint8Array(plaintextBytes);
|
||||
|
||||
// Carve the message and associated data into blocks.
|
||||
const messageBlocks = parse(cleartext, 16);
|
||||
const messageBlockLength = messageBlocks.length - 1;
|
||||
// Initialize for verification
|
||||
let state = createByteArray(BLOCK_SIZE, 0);
|
||||
let counter = createResetCounter();
|
||||
|
||||
const associatedDataBlocks = parse(associatedData, 16);
|
||||
// Parse recovered message and associated data
|
||||
const messageBlocks = parseMessage(recoveredPlaintext, BLOCK_SIZE);
|
||||
const messageBlockCount = messageBlocks.length - 1;
|
||||
|
||||
const associatedDataBlocks = parseMessage(
|
||||
validatedAssociatedData,
|
||||
BLOCK_SIZE,
|
||||
);
|
||||
const associatedDataBlockCount = associatedDataBlocks.length - 1;
|
||||
|
||||
// Concatenate the message and associated data blocks, excluding each array's first element.
|
||||
const combinedData = associatedDataBlocks
|
||||
.slice(1)
|
||||
.concat(messageBlocks.slice(1));
|
||||
// Combine blocks for verification
|
||||
const combinedBlocks = [
|
||||
createByteArray(0),
|
||||
...associatedDataBlocks.slice(1),
|
||||
...messageBlocks.slice(1),
|
||||
];
|
||||
|
||||
// Insert empty array at position 0.
|
||||
combinedData.splice(0, 0, []);
|
||||
|
||||
// Calculate domain separation for final decryption stage.
|
||||
// Calculate domain separation
|
||||
const domainSeparation = calculateDomainSeparation(
|
||||
combinedData,
|
||||
messageBlockLength,
|
||||
combinedBlocks,
|
||||
messageBlockCount,
|
||||
associatedDataBlockCount,
|
||||
);
|
||||
|
||||
// Pad combined data.
|
||||
combinedData[associatedDataBlockCount] = pad(
|
||||
combinedData[associatedDataBlockCount],
|
||||
16,
|
||||
// Pad final blocks
|
||||
if (associatedDataBlockCount > 0) {
|
||||
combinedBlocks[associatedDataBlockCount] = padBlock(
|
||||
combinedBlocks[associatedDataBlockCount],
|
||||
BLOCK_SIZE,
|
||||
);
|
||||
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]);
|
||||
counter = increaseCounter(counter);
|
||||
if (i === Math.floor(associatedDataBlockCount / 2) + 1) {
|
||||
x = x ^ 4;
|
||||
}
|
||||
if (messageBlockCount > 0) {
|
||||
combinedBlocks[associatedDataBlockCount + messageBlockCount] = padBlock(
|
||||
combinedBlocks[associatedDataBlockCount + messageBlockCount],
|
||||
BLOCK_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
// Process blocks for verification
|
||||
let x = 8;
|
||||
const totalRounds =
|
||||
Math.floor((associatedDataBlockCount + messageBlockCount) / 2) + 1;
|
||||
|
||||
for (let i = 1; i < totalRounds; i++) {
|
||||
[state] = stateUpdateEncrypt(state, combinedBlocks[2 * i - 1]);
|
||||
counter = incrementCounter(counter);
|
||||
|
||||
if (i === Math.floor(associatedDataBlockCount / 2) + 1) {
|
||||
x ^= 4;
|
||||
}
|
||||
|
||||
state = skinnyEncrypt(
|
||||
state,
|
||||
tweakeyEncode(counter, x, combinedData[2 * i], key),
|
||||
tweakeyEncode(counter, x, combinedBlocks[2 * i], validatedKey),
|
||||
);
|
||||
counter = increaseCounter(counter);
|
||||
counter = incrementCounter(counter);
|
||||
}
|
||||
|
||||
if (associatedDataBlockCount % 2 === messageBlockLength % 2) {
|
||||
[state] = rho(state, Array(16));
|
||||
// Handle final block
|
||||
const emptyBlock = createByteArray(BLOCK_SIZE, 0);
|
||||
if (associatedDataBlockCount % 2 === messageBlockCount % 2) {
|
||||
[state] = stateUpdateEncrypt(state, emptyBlock);
|
||||
} else {
|
||||
[state] = rho(
|
||||
[state] = stateUpdateEncrypt(
|
||||
state,
|
||||
combinedData[associatedDataBlockCount + messageBlockLength],
|
||||
combinedBlocks[associatedDataBlockCount + messageBlockCount],
|
||||
);
|
||||
counter = increaseCounter(counter);
|
||||
counter = incrementCounter(counter);
|
||||
}
|
||||
|
||||
// Calculate authentication tag.
|
||||
const [, computedTag] = rho(
|
||||
skinnyEncrypt(state, tweakeyEncode(counter, domainSeparation, nonce, key)),
|
||||
Array(16),
|
||||
// Compute authentication tag
|
||||
const encryptedState = skinnyEncrypt(
|
||||
state,
|
||||
tweakeyEncode(counter, domainSeparation, validatedNonce, validatedKey),
|
||||
);
|
||||
const [, computedTag] = stateUpdateEncrypt(encryptedState, emptyBlock);
|
||||
|
||||
// Validate authentication tag.
|
||||
// Constant-time comparison of authentication tags
|
||||
let compare = 0;
|
||||
for (let i = 0; i < 16; i++) {
|
||||
for (let i = 0; i < AUTH_TAG_SIZE; i++) {
|
||||
compare |= authenticationTag[i] ^ computedTag[i];
|
||||
}
|
||||
|
||||
if (compare !== 0) {
|
||||
// Authentication failed.
|
||||
// Authentication failed
|
||||
return {
|
||||
success: false,
|
||||
plaintext: [],
|
||||
plaintext: new Uint8Array(),
|
||||
};
|
||||
} else {
|
||||
// Decrypted successfully.
|
||||
// Decryption successful
|
||||
return {
|
||||
success: true,
|
||||
plaintext: cleartext,
|
||||
plaintext: recoveredPlaintext,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
throw error;
|
||||
}
|
||||
// Authentication failure or other error - return failure without throwing
|
||||
return {
|
||||
success: false,
|
||||
plaintext: new Uint8Array(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,93 +7,170 @@ import {
|
||||
LFSR_8_TK3,
|
||||
S8,
|
||||
C,
|
||||
BLOCK_SIZE,
|
||||
} from "./constants";
|
||||
import {
|
||||
validateKey,
|
||||
validateNonce,
|
||||
InvalidInputError,
|
||||
copyBytes,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param counter The counter state as a byte array.
|
||||
* @param domainSeparation The domain separation value.
|
||||
* @param nonce The 16-byte nonce.
|
||||
* @param key The 16-byte encryption key.
|
||||
* @returns The tweakey as a byte array.
|
||||
* @throws {InvalidInputError} If inputs have incorrect lengths or invalid values.
|
||||
*/
|
||||
export function tweakeyEncode(
|
||||
counter: number[],
|
||||
counter: Uint8Array,
|
||||
domainSeparation: number,
|
||||
nonce: number[],
|
||||
key: number[],
|
||||
): number[] {
|
||||
return counter.concat([domainSeparation ^ MEMBER_MASK], Array(8), nonce, key);
|
||||
nonce: Uint8Array,
|
||||
key: Uint8Array,
|
||||
): Uint8Array {
|
||||
if (counter.length !== 7) {
|
||||
throw new InvalidInputError(
|
||||
`Counter must be 7 bytes, got ${counter.length}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!Number.isInteger(domainSeparation) ||
|
||||
domainSeparation < 0 ||
|
||||
domainSeparation > 255
|
||||
) {
|
||||
throw new InvalidInputError(
|
||||
`Domain separation must be a valid byte (0-255), got ${domainSeparation}`,
|
||||
);
|
||||
}
|
||||
|
||||
const validatedNonce = validateNonce(nonce);
|
||||
const validatedKey = validateKey(key);
|
||||
|
||||
// Create tweakey: counter (7) + domain_sep^MEMBER_MASK (1) + padding (8) + nonce (16) + key (16) = 48 bytes
|
||||
const tweakey = new Uint8Array(TWEAK_LENGTH);
|
||||
let offset = 0;
|
||||
|
||||
// Copy counter
|
||||
tweakey.set(counter, offset);
|
||||
offset += counter.length;
|
||||
|
||||
// Add domain separation XORed with member mask
|
||||
tweakey[offset] = domainSeparation ^ MEMBER_MASK;
|
||||
offset += 1;
|
||||
|
||||
// Skip 8 bytes of padding (already zero-filled)
|
||||
offset += 8;
|
||||
|
||||
// Copy nonce
|
||||
tweakey.set(validatedNonce, offset);
|
||||
offset += BLOCK_SIZE;
|
||||
|
||||
// Copy key
|
||||
tweakey.set(validatedKey, offset);
|
||||
|
||||
return tweakey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Perform SKINNY-128/384+ block cipher encryption.
|
||||
* @param plaintext The 16-byte plaintext block to encrypt.
|
||||
* @param tweakey The 48-byte tweakey for encryption.
|
||||
* @returns The 16-byte ciphertext block.
|
||||
* @throws {InvalidInputError} If inputs have incorrect lengths.
|
||||
*/
|
||||
export function skinnyEncrypt(
|
||||
plaintext: number[],
|
||||
tweakey: number[],
|
||||
): number[] {
|
||||
const tk = Array(NB_ROUNDS + 1).fill(Array(TWEAK_LENGTH).fill(0));
|
||||
plaintext: Uint8Array,
|
||||
tweakey: Uint8Array,
|
||||
): Uint8Array {
|
||||
if (plaintext.length !== BLOCK_SIZE) {
|
||||
throw new InvalidInputError(
|
||||
`Plaintext must be ${BLOCK_SIZE} bytes, got ${plaintext.length}`,
|
||||
);
|
||||
}
|
||||
if (tweakey.length !== TWEAK_LENGTH) {
|
||||
throw new InvalidInputError(
|
||||
`Tweakey must be ${TWEAK_LENGTH} bytes, got ${tweakey.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
tk[0] = Array.from(Array(TWEAK_LENGTH).keys()).map((i) => tweakey[i]);
|
||||
// Pre-compute all round tweakeys (fix the array sharing bug)
|
||||
const tk: Uint8Array[] = new Array(NB_ROUNDS + 1);
|
||||
|
||||
// Initialize first round tweakey
|
||||
tk[0] = copyBytes(tweakey);
|
||||
|
||||
// Generate remaining round tweakeys
|
||||
for (let i = 0; i < NB_ROUNDS - 1; i++) {
|
||||
tk[i + 1] = Array.from(tk[i]);
|
||||
tk[i + 1] = copyBytes(tk[i]);
|
||||
|
||||
// Apply permutation table
|
||||
for (let j = 0; j < TWEAK_LENGTH; j++) {
|
||||
tk[i + 1][j] = tk[i][j - (j % 16) + PT[j % 16]];
|
||||
}
|
||||
|
||||
// Apply LFSR to TK2 and TK3 parts
|
||||
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.from(Array(16).keys()).map((i) => plaintext[i]);
|
||||
for (let i = 0; i < NB_ROUNDS; i++) {
|
||||
for (let j = 0; j < 16; j++) {
|
||||
// Initialize state with plaintext (fix inefficient array creation)
|
||||
const s = copyBytes(plaintext);
|
||||
|
||||
// Perform encryption rounds
|
||||
for (let round = 0; round < NB_ROUNDS; round++) {
|
||||
// SubCells: apply S-box to all bytes
|
||||
for (let j = 0; j < BLOCK_SIZE; j++) {
|
||||
s[j] = S8[s[j]];
|
||||
}
|
||||
|
||||
s[0] ^= C[i] & 0xf;
|
||||
s[4] ^= (C[i] >> 4) & 0xf;
|
||||
// AddConstants
|
||||
s[0] ^= C[round] & 0xf;
|
||||
s[4] ^= (C[round] >> 4) & 0xf;
|
||||
s[8] ^= 0x2;
|
||||
|
||||
// AddRoundTweakey: XOR first 8 bytes of state with tweakey
|
||||
for (let j = 0; j < 8; j++) {
|
||||
s[j] ^= tk[i][j] ^ tk[i][j + 16] ^ tk[i][j + 32];
|
||||
s[j] ^= tk[round][j] ^ tk[round][j + 16] ^ tk[round][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],
|
||||
];
|
||||
// ShiftRows: apply shift row transformation
|
||||
const temp = new Uint8Array(BLOCK_SIZE);
|
||||
temp[0] = s[0];
|
||||
temp[1] = s[1];
|
||||
temp[2] = s[2];
|
||||
temp[3] = s[3];
|
||||
temp[4] = s[7];
|
||||
temp[5] = s[4];
|
||||
temp[6] = s[5];
|
||||
temp[7] = s[6];
|
||||
temp[8] = s[10];
|
||||
temp[9] = s[11];
|
||||
temp[10] = s[8];
|
||||
temp[11] = s[9];
|
||||
temp[12] = s[13];
|
||||
temp[13] = s[14];
|
||||
temp[14] = s[15];
|
||||
temp[15] = s[12];
|
||||
s.set(temp);
|
||||
|
||||
for (let j = 0; j < 4; j++) {
|
||||
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];
|
||||
s[12 + j] = tmp[0 + j] ^ tmp[8 + j];
|
||||
// MixColumns: apply linear transformation to columns
|
||||
for (let col = 0; col < 4; col++) {
|
||||
const c0 = s[col];
|
||||
const c1 = s[4 + col];
|
||||
const c2 = s[8 + col];
|
||||
const c3 = s[12 + col];
|
||||
|
||||
s[col] = c0 ^ c2 ^ c3;
|
||||
s[4 + col] = c0;
|
||||
s[8 + col] = c1 ^ c2;
|
||||
s[12 + col] = c0 ^ c2;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(Array(16).keys()).map((i) => s[i]);
|
||||
return s;
|
||||
}
|
||||
|
||||
161
src/types.ts
Normal file
161
src/types.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Type definitions and validation utilities for the Romulus-M cryptographic library.
|
||||
*/
|
||||
|
||||
// Branded types for type safety
|
||||
declare const KeyBrand: unique symbol;
|
||||
declare const NonceBrand: unique symbol;
|
||||
declare const BlockBrand: unique symbol;
|
||||
declare const CounterBrand: unique symbol;
|
||||
|
||||
export type Key = Uint8Array & { readonly [KeyBrand]: true };
|
||||
export type Nonce = Uint8Array & { readonly [NonceBrand]: true };
|
||||
export type Block = Uint8Array & { readonly [BlockBrand]: true };
|
||||
export type Counter = Uint8Array & { readonly [CounterBrand]: true };
|
||||
|
||||
// Constants
|
||||
export const BLOCK_SIZE = 16;
|
||||
export const KEY_SIZE = 16;
|
||||
export const NONCE_SIZE = 16;
|
||||
export const COUNTER_SIZE = 7;
|
||||
export const AUTH_TAG_SIZE = 16;
|
||||
|
||||
// Custom error classes
|
||||
export class RomulusError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "RomulusError";
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidInputError extends RomulusError {
|
||||
constructor(message: string) {
|
||||
super(`Invalid input: ${message}`);
|
||||
this.name = "InvalidInputError";
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidKeyError extends InvalidInputError {
|
||||
constructor(message: string = "Key must be exactly 16 bytes") {
|
||||
super(`Key error: ${message}`);
|
||||
this.name = "InvalidKeyError";
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidNonceError extends InvalidInputError {
|
||||
constructor(message: string = "Nonce must be exactly 16 bytes") {
|
||||
super(`Nonce error: ${message}`);
|
||||
this.name = "InvalidNonceError";
|
||||
}
|
||||
}
|
||||
|
||||
// Validation functions
|
||||
export function isValidByteArray(arr: Uint8Array): boolean {
|
||||
return arr.every(
|
||||
(byte) => byte >= 0 && byte <= 255 && Number.isInteger(byte),
|
||||
);
|
||||
}
|
||||
|
||||
export function validateKey(key: Uint8Array): Key {
|
||||
if (!key || !(key instanceof Uint8Array)) {
|
||||
throw new InvalidKeyError("Key must be a Uint8Array");
|
||||
}
|
||||
if (key.length !== KEY_SIZE) {
|
||||
throw new InvalidKeyError(
|
||||
`Key must be exactly ${KEY_SIZE} bytes, got ${key.length}`,
|
||||
);
|
||||
}
|
||||
if (!isValidByteArray(key)) {
|
||||
throw new InvalidKeyError("Key must contain only valid bytes (0-255)");
|
||||
}
|
||||
return key as Key;
|
||||
}
|
||||
|
||||
export function validateNonce(nonce: Uint8Array): Nonce {
|
||||
if (!nonce || !(nonce instanceof Uint8Array)) {
|
||||
throw new InvalidNonceError("Nonce must be a Uint8Array");
|
||||
}
|
||||
if (nonce.length !== NONCE_SIZE) {
|
||||
throw new InvalidNonceError(
|
||||
`Nonce must be exactly ${NONCE_SIZE} bytes, got ${nonce.length}`,
|
||||
);
|
||||
}
|
||||
if (!isValidByteArray(nonce)) {
|
||||
throw new InvalidNonceError("Nonce must contain only valid bytes (0-255)");
|
||||
}
|
||||
return nonce as Nonce;
|
||||
}
|
||||
|
||||
export function validateAssociatedData(data: Uint8Array): Uint8Array {
|
||||
if (!data || !(data instanceof Uint8Array)) {
|
||||
throw new InvalidInputError("Associated data must be a Uint8Array");
|
||||
}
|
||||
if (!isValidByteArray(data)) {
|
||||
throw new InvalidInputError(
|
||||
"Associated data must contain only valid bytes (0-255)",
|
||||
);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function validateMessage(message: Uint8Array): Uint8Array {
|
||||
if (!message || !(message instanceof Uint8Array)) {
|
||||
throw new InvalidInputError("Message must be a Uint8Array");
|
||||
}
|
||||
if (!isValidByteArray(message)) {
|
||||
throw new InvalidInputError(
|
||||
"Message must contain only valid bytes (0-255)",
|
||||
);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
export function validateCiphertext(ciphertext: Uint8Array): Uint8Array {
|
||||
if (!ciphertext || !(ciphertext instanceof Uint8Array)) {
|
||||
throw new InvalidInputError("Ciphertext must be a Uint8Array");
|
||||
}
|
||||
if (ciphertext.length < AUTH_TAG_SIZE) {
|
||||
throw new InvalidInputError(
|
||||
`Ciphertext must be at least ${AUTH_TAG_SIZE} bytes (authentication tag size)`,
|
||||
);
|
||||
}
|
||||
if (!isValidByteArray(ciphertext)) {
|
||||
throw new InvalidInputError(
|
||||
"Ciphertext must contain only valid bytes (0-255)",
|
||||
);
|
||||
}
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
// Utility functions for efficient array operations
|
||||
export function createByteArray(size: number, fill: number = 0): Uint8Array {
|
||||
if (fill < 0 || fill > 255 || !Number.isInteger(fill)) {
|
||||
throw new InvalidInputError(
|
||||
`Fill value must be a valid byte (0-255), got ${fill}`,
|
||||
);
|
||||
}
|
||||
return new Uint8Array(size).fill(fill);
|
||||
}
|
||||
|
||||
export function xorBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
||||
if (a.length !== b.length) {
|
||||
throw new InvalidInputError(
|
||||
`Array lengths must match: ${a.length} !== ${b.length}`,
|
||||
);
|
||||
}
|
||||
const result = new Uint8Array(a.length);
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
result[i] = a[i] ^ b[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function copyBytes(source: Uint8Array): Uint8Array {
|
||||
return new Uint8Array(source);
|
||||
}
|
||||
|
||||
// Result interface
|
||||
export interface DecryptResult {
|
||||
success: boolean;
|
||||
plaintext: Uint8Array;
|
||||
}
|
||||
@@ -5,13 +5,12 @@ import {
|
||||
DecryptResult,
|
||||
} from "../src/romulus-m";
|
||||
|
||||
function parseHexString(string: string): number[] {
|
||||
const ret = [];
|
||||
function parseHexString(string: string): Uint8Array {
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i < string.length; i += 2) {
|
||||
ret.push(parseInt(string.slice(i, i + 2), 16));
|
||||
bytes.push(parseInt(string.slice(i, i + 2), 16));
|
||||
}
|
||||
|
||||
return ret;
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
test.each(referenceTests)(
|
||||
@@ -27,7 +26,7 @@ test.each(referenceTests)(
|
||||
|
||||
// Then
|
||||
const expectedResult = parseHexString(ciphertext);
|
||||
expect(result).toMatchObject(expectedResult);
|
||||
expect(result).toEqual(expectedResult);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -47,6 +46,6 @@ test.each(referenceTests)(
|
||||
success: true,
|
||||
plaintext: parseHexString(plaintext),
|
||||
};
|
||||
expect(result).toMatchObject(expectedResult);
|
||||
expect(result).toEqual(expectedResult);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { cryptoAeadDecrypt, cryptoAeadEncrypt } from "../src/romulus-m";
|
||||
|
||||
function stringToArray(string: string): number[] {
|
||||
function stringToUint8Array(string: string): Uint8Array {
|
||||
const encoder = new TextEncoder();
|
||||
return Array.from(encoder.encode(string));
|
||||
return 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(
|
||||
const message = stringToUint8Array("Hello, World! This is a test message.");
|
||||
const associatedData = stringToUint8Array("");
|
||||
const nonce = stringToUint8Array(
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
);
|
||||
const key = stringToArray(
|
||||
const key = stringToUint8Array(
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
);
|
||||
|
||||
@@ -20,23 +20,23 @@ test("Encrypt a message with no associated data.", () => {
|
||||
const result = cryptoAeadEncrypt(message, associatedData, nonce, key);
|
||||
|
||||
// Then
|
||||
const expectedResult = [
|
||||
const expectedResult = new Uint8Array([
|
||||
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);
|
||||
]);
|
||||
expect(result).toEqual(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(
|
||||
const message = stringToUint8Array("Hello, World! This is a test message.");
|
||||
const associatedData = stringToUint8Array("Some associated data.");
|
||||
const nonce = stringToUint8Array(
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
);
|
||||
const key = stringToArray(
|
||||
const key = stringToUint8Array(
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
);
|
||||
|
||||
@@ -44,28 +44,28 @@ test("Encrypt a message with associated data.", () => {
|
||||
const result = cryptoAeadEncrypt(message, associatedData, nonce, key);
|
||||
|
||||
// Then
|
||||
const expectedResult = [
|
||||
const expectedResult = new Uint8Array([
|
||||
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);
|
||||
]);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test("Decrypt a message with no associated data.", () => {
|
||||
// Given
|
||||
const ciphertext = [
|
||||
const ciphertext = new Uint8Array([
|
||||
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,
|
||||
];
|
||||
const associatedData = stringToArray("");
|
||||
const nonce = stringToArray(
|
||||
]);
|
||||
const associatedData = stringToUint8Array("");
|
||||
const nonce = stringToUint8Array(
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
);
|
||||
const key = stringToArray(
|
||||
const key = stringToUint8Array(
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
);
|
||||
|
||||
@@ -73,24 +73,26 @@ test("Decrypt a message with no associated data.", () => {
|
||||
const result = cryptoAeadDecrypt(ciphertext, associatedData, nonce, key);
|
||||
|
||||
// Then
|
||||
const expectedResult = stringToArray("Hello, World! This is a test message.");
|
||||
const expectedResult = stringToUint8Array(
|
||||
"Hello, World! This is a test message.",
|
||||
);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.plaintext).toMatchObject(expectedResult);
|
||||
expect(result.plaintext).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test("Decrypt a message with associated data.", () => {
|
||||
// Given
|
||||
const ciphertext = [
|
||||
const ciphertext = new Uint8Array([
|
||||
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 = stringToArray("Some associated data.");
|
||||
const nonce = stringToArray(
|
||||
]);
|
||||
const associatedData = stringToUint8Array("Some associated data.");
|
||||
const nonce = stringToUint8Array(
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
);
|
||||
const key = stringToArray(
|
||||
const key = stringToUint8Array(
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
);
|
||||
|
||||
@@ -98,7 +100,9 @@ test("Decrypt a message with associated data.", () => {
|
||||
const result = cryptoAeadDecrypt(ciphertext, associatedData, nonce, key);
|
||||
|
||||
// Then
|
||||
const expectedResult = stringToArray("Hello, World! This is a test message.");
|
||||
const expectedResult = stringToUint8Array(
|
||||
"Hello, World! This is a test message.",
|
||||
);
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.plaintext).toMatchObject(expectedResult);
|
||||
expect(result.plaintext).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"module": "node16" /* Specify what module code is generated. */,
|
||||
"moduleResolution": "node16" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
"rootDir": "src" /* Specify the root folder within your source files. */,
|
||||
"sourceMap": true /* Create source map files for emitted JavaScript files. */,
|
||||
"outDir": "dist" /* Specify an output folder for all emitted files. */,
|
||||
@@ -10,6 +11,7 @@
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
|
||||
"declaration": true
|
||||
},
|
||||
"exclude": ["tests", "dist"]
|
||||
|
||||
Reference in New Issue
Block a user