Skip to content

Commit e264e20

Browse files
Add Salsa20 and XSalsa20 operation
1 parent c4e7c41 commit e264e20

File tree

6 files changed

+588
-0
lines changed

6 files changed

+588
-0
lines changed

src/core/config/Categories.json

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@
9595
"RC4",
9696
"RC4 Drop",
9797
"ChaCha",
98+
"Salsa20",
99+
"XSalsa20",
98100
"Rabbit",
99101
"SM4 Encrypt",
100102
"SM4 Decrypt",

src/core/operations/Salsa20.mjs

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/**
2+
* @author joostrijneveld [[email protected]]
3+
* @copyright Crown Copyright 2024
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import OperationError from "../errors/OperationError.mjs";
9+
import Utils from "../Utils.mjs";
10+
import { toHex } from "../lib/Hex.mjs";
11+
12+
13+
/**
14+
* Computes the Salsa20 permute function
15+
*
16+
* @param {byteArray} x
17+
* @param {integer} rounds
18+
*/
19+
export function salsa20_permute(x, rounds) {
20+
/**
21+
* Macro to compute a 32-bit rotate-left operation
22+
*
23+
* @param {integer} x
24+
* @param {integer} n
25+
* @returns {integer}
26+
*/
27+
function ROL32(x, n) {
28+
return ((x << n) & 0xFFFFFFFF) | (x >>> (32 - n));
29+
}
30+
31+
/**
32+
* Macro to compute a single Salsa20 quarterround operation
33+
*
34+
* @param {integer} x
35+
* @param {integer} a
36+
* @param {integer} b
37+
* @param {integer} c
38+
* @param {integer} d
39+
* @returns {integer}
40+
*/
41+
function quarterround(x, a, b, c, d) {
42+
x[b] ^= ROL32((x[a] + x[d]) & 0xFFFFFFFF, 7)
43+
x[c] ^= ROL32((x[b] + x[a]) & 0xFFFFFFFF, 9)
44+
x[d] ^= ROL32((x[c] + x[b]) & 0xFFFFFFFF, 13)
45+
x[a] ^= ROL32((x[d] + x[c]) & 0xFFFFFFFF, 18)
46+
}
47+
48+
for (let i = 0; i < rounds / 2; i++) {
49+
quarterround(x, 0, 4, 8, 12);
50+
quarterround(x, 5, 9, 13, 1);
51+
quarterround(x, 10, 14, 2, 6);
52+
quarterround(x, 15, 3, 7, 11);
53+
quarterround(x, 0, 1, 2, 3);
54+
quarterround(x, 5, 6, 7, 4);
55+
quarterround(x, 10, 11, 8, 9);
56+
quarterround(x, 15, 12, 13, 14);
57+
}
58+
}
59+
60+
/**
61+
* Computes the Salsa20 block function
62+
*
63+
* @param {byteArray} key
64+
* @param {byteArray} nonce
65+
* @param {byteArray} counter
66+
* @param {integer} rounds
67+
* @returns {byteArray}
68+
*/
69+
export function salsa20(key, nonce, counter, rounds) {
70+
const tau = "expand 16-byte k";
71+
const sigma = "expand 32-byte k";
72+
let state, c;
73+
if (key.length === 16) {
74+
c = Utils.strToByteArray(tau);
75+
key = key.concat(key);
76+
} else {
77+
c = Utils.strToByteArray(sigma);
78+
}
79+
80+
state = c.slice(0, 4);
81+
state = state.concat(key.slice(0, 16));
82+
state = state.concat(c.slice(4, 8));
83+
state = state.concat(nonce);
84+
state = state.concat(counter);
85+
state = state.concat(c.slice(8, 12));
86+
state = state.concat(key.slice(16, 32));
87+
state = state.concat(c.slice(12, 16));
88+
89+
const x = Array();
90+
for (let i = 0; i < 64; i += 4) {
91+
x.push(Utils.byteArrayToInt(state.slice(i, i + 4), "little"));
92+
}
93+
const a = [...x];
94+
95+
salsa20_permute(x, rounds);
96+
97+
for (let i = 0; i < 16; i++) {
98+
x[i] = (x[i] + a[i]) & 0xFFFFFFFF;
99+
}
100+
101+
let output = Array();
102+
for (let i = 0; i < 16; i++) {
103+
output = output.concat(Utils.intToByteArray(x[i], 4, "little"));
104+
}
105+
return output;
106+
}
107+
108+
/**
109+
* Salsa20 operation
110+
*/
111+
class Salsa20 extends Operation {
112+
113+
/**
114+
* Salsa20 constructor
115+
*/
116+
constructor() {
117+
super();
118+
119+
this.name = "Salsa20";
120+
this.module = "Default";
121+
this.description = "Salsa20 is a stream cipher designed by Daniel J. Bernstein and submitted to the eSTREAM project; Salsa20/8 and Salsa20/12 are round-reduced variants. It is closely related to the ChaCha stream cipher.<br><br><b>Key:</b> Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).<br><br><b>Nonce:</b> Salsa20 uses a nonce of 8 bytes (64 bits).<br><br><b>Counter:</b> Salsa uses a counter of 8 bytes (64 bits). The counter starts at zero at the start of the keystream, and is incremented at every 64 bytes.";
122+
this.infoURL = "https://wikipedia.org/wiki/Salsa20";
123+
this.inputType = "string";
124+
this.outputType = "string";
125+
this.args = [
126+
{
127+
"name": "Key",
128+
"type": "toggleString",
129+
"value": "",
130+
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
131+
},
132+
{
133+
"name": "Nonce",
134+
"type": "toggleString",
135+
"value": "",
136+
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64", "Integer"]
137+
},
138+
{
139+
"name": "Counter",
140+
"type": "number",
141+
"value": 0,
142+
"min": 0
143+
},
144+
{
145+
"name": "Rounds",
146+
"type": "option",
147+
"value": ["20", "12", "8"]
148+
},
149+
{
150+
"name": "Input",
151+
"type": "option",
152+
"value": ["Hex", "Raw"]
153+
},
154+
{
155+
"name": "Output",
156+
"type": "option",
157+
"value": ["Raw", "Hex"]
158+
}
159+
];
160+
}
161+
162+
/**
163+
* @param {string} input
164+
* @param {Object[]} args
165+
* @returns {string}
166+
*/
167+
run(input, args) {
168+
const key = Utils.convertToByteArray(args[0].string, args[0].option),
169+
nonceType = args[1].option,
170+
rounds = parseInt(args[3], 10),
171+
inputType = args[4],
172+
outputType = args[5];
173+
174+
if (key.length !== 16 && key.length !== 32) {
175+
throw new OperationError(`Invalid key length: ${key.length} bytes.
176+
177+
Salsa20 uses a key of 16 or 32 bytes (128 or 256 bits).`);
178+
}
179+
180+
let counter, nonce;
181+
if (nonceType === "Integer") {
182+
nonce = Utils.intToByteArray(parseInt(args[1].string, 10), 8, "little");
183+
} else {
184+
nonce = Utils.convertToByteArray(args[1].string, args[1].option);
185+
if (!(nonce.length === 8)) {
186+
throw new OperationError(`Invalid nonce length: ${nonce.length} bytes.
187+
188+
Salsa20 uses a nonce of 8 bytes (64 bits).`);
189+
}
190+
}
191+
counter = Utils.intToByteArray(args[2], 8, "little");
192+
193+
const output = [];
194+
input = Utils.convertToByteArray(input, inputType);
195+
196+
let counterAsInt = Utils.byteArrayToInt(counter, "little");
197+
for (let i = 0; i < input.length; i += 64) {
198+
counter = Utils.intToByteArray(counterAsInt, 8, "little");
199+
const stream = salsa20(key, nonce, counter, rounds);
200+
for (let j = 0; j < 64 && i + j < input.length; j++) {
201+
output.push(input[i + j] ^ stream[j]);
202+
}
203+
counterAsInt++;
204+
}
205+
206+
if (outputType === "Hex") {
207+
return toHex(output);
208+
} else {
209+
return Utils.arrayBufferToStr(Uint8Array.from(output).buffer);
210+
}
211+
}
212+
213+
/**
214+
* Highlight Salsa20
215+
*
216+
* @param {Object[]} pos
217+
* @param {number} pos[].start
218+
* @param {number} pos[].end
219+
* @param {Object[]} args
220+
* @returns {Object[]} pos
221+
*/
222+
highlight(pos, args) {
223+
const inputType = args[4],
224+
outputType = args[5];
225+
if (inputType === "Raw" && outputType === "Raw") {
226+
return pos;
227+
}
228+
}
229+
230+
/**
231+
* Highlight Salsa20 in reverse
232+
*
233+
* @param {Object[]} pos
234+
* @param {number} pos[].start
235+
* @param {number} pos[].end
236+
* @param {Object[]} args
237+
* @returns {Object[]} pos
238+
*/
239+
highlightReverse(pos, args) {
240+
const inputType = args[4],
241+
outputType = args[5];
242+
if (inputType === "Raw" && outputType === "Raw") {
243+
return pos;
244+
}
245+
}
246+
247+
}
248+
249+
export default Salsa20;

0 commit comments

Comments
 (0)