Salsa20 Encryption Algorithm
About Salsa20
A stream cipher designed by Daniel J. Bernstein in 2005
Salsa20 is a stream cipher that was submitted to the eSTREAM project by Daniel J. Bernstein. It's known for its speed and security, and has been widely adopted in various security applications.
Key Features
- 256-bit key (128-bit version also available)
- 64-bit nonce (initialization vector)
- 64-bit counter for position in the stream
- High performance in software implementations
- No known practical attacks against full rounds
- Simple design with ARX operations (Add-Rotate-XOR)
Technical Specifications
- Block size: 512 bits (64 bytes)
- State: 4×4 matrix of 32-bit words
- 20 rounds (Salsa20/20)
- Reduced round variants: Salsa20/8, Salsa20/12
- No S-boxes or table lookups (cache-timing resistant)
- Little-endian byte ordering
How Salsa20 Works
Salsa20 generates a keystream which is then XORed with the plaintext to produce ciphertext. The core function transforms a 64-byte block using a series of operations:
- Initialize a 4×4 matrix of 32-bit words with key, nonce, counter, and constants
- Apply 20 rounds of the quarter-round function to mix the state
- Add the original input to the final state (to ensure invertibility)
- Use the resulting 64-byte block as keystream
- XOR the keystream with plaintext to get ciphertext
Quarter-Round Function
The quarter-round function is the core operation in Salsa20. It operates on 4 words (a, b, c, d) and applies a series of ARX (Add-Rotate-XOR) operations:
b ^= (a + d) <<< 7 c ^= (b + a) <<< 9 d ^= (c + b) <<< 13 a ^= (d + c) <<< 18
Security Considerations
- No known practical attacks against full 20-round Salsa20
- Reduced-round variants (Salsa20/8, Salsa20/12) have been analyzed
- Nonce reuse is catastrophic (never reuse the same key-nonce pair)
- No authentication built-in (should be used with a MAC)
- Resistant to timing attacks due to lack of data-dependent operations
Applications and Variants
Salsa20 and its variants are used in various security applications:
- NaCl cryptographic library
- ChaCha20 (a variant with improved diffusion)
- XSalsa20 (extended nonce variant used in NaCl)
- Signal Protocol (in combination with other algorithms)
- TLS 1.3 (ChaCha20-Poly1305)
Salsa20 Animation
Visual representation of the Salsa20 encryption process
Salsa20 Animation
Salsa20 Visualization
State Matrix Transformation
Quarter-Round Function
Salsa20 Rounds
Encryption Process
Salsa20 Simulator
Try encrypting and decrypting with Salsa20
Salsa20 Simulator
Salsa20 Encryption Simulator
Salsa20 Implementation
JavaScript implementation of the Salsa20 algorithm
Salsa20 Implementation
// Salsa20 Core Implementation in JavaScript
// Helper functions
function rotl(value, shift) {
return ((value << shift) | (value >>> (32 - shift))) >>> 0;
}
// Convert between little-endian words and byte arrays
function littleEndianToInt(bytes, pos) {
return (
(bytes[pos] & 0xff) |
((bytes[pos + 1] & 0xff) << 8) |
((bytes[pos + 2] & 0xff) << 16) |
((bytes[pos + 3] & 0xff) << 24)
) >>> 0;
}
function intToLittleEndian(x, out, pos) {
out[pos] = x & 0xff;
out[pos + 1] = (x >>> 8) & 0xff;
out[pos + 2] = (x >>> 16) & 0xff;
out[pos + 3] = (x >>> 24) & 0xff;
}
// Quarter round function
function quarterRound(y, a, b, c, d) {
y[b] = (y[b] ^ rotl(y[a] + y[d], 7)) >>> 0;
y[c] = (y[c] ^ rotl(y[b] + y[a], 9)) >>> 0;
y[d] = (y[d] ^ rotl(y[c] + y[b], 13)) >>> 0;
y[a] = (y[a] ^ rotl(y[d] + y[c], 18)) >>> 0;
return y;
}
// Core Salsa20 function
function salsa20Core(input, rounds) {
// Create working array
const x = new Uint32Array(16);
for (let i = 0; i < 16; i++) {
x[i] = input[i];
}
// Main loop
for (let i = rounds; i > 0; i -= 2) {
// Column round
quarterRound(x, 0, 4, 8, 12);
quarterRound(x, 5, 9, 13, 1);
quarterRound(x, 10, 14, 2, 6);
quarterRound(x, 15, 3, 7, 11);
// Row round
quarterRound(x, 0, 1, 2, 3);
quarterRound(x, 5, 6, 7, 4);
quarterRound(x, 10, 11, 8, 9);
quarterRound(x, 15, 12, 13, 14);
}
// Add input to output
const output = new Uint32Array(16);
for (let i = 0; i < 16; i++) {
output[i] = (x[i] + input[i]) >>> 0;
}
return output;
}
// Expand key and nonce into initial state
function expandKey(key, nonce) {
const state = new Uint32Array(16);
// Constants for 256-bit key
const sigma = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574];
// Set up initial state with constants
state[0] = sigma[0];
state[5] = sigma[1];
state[10] = sigma[2];
state[15] = sigma[3];
// Add key (256-bit)
state[1] = littleEndianToInt(key, 0);
state[2] = littleEndianToInt(key, 4);
state[3] = littleEndianToInt(key, 8);
state[4] = littleEndianToInt(key, 12);
state[11] = littleEndianToInt(key, 16);
state[12] = littleEndianToInt(key, 20);
state[13] = littleEndianToInt(key, 24);
state[14] = littleEndianToInt(key, 28);
// Add nonce and counter
state[6] = littleEndianToInt(nonce, 0);
state[7] = littleEndianToInt(nonce, 4);
state[8] = 0; // Counter (low)
state[9] = 0; // Counter (high)
return state;
}
// Generate keystream block
function generateKeystreamBlock(state, rounds) {
const result = new Uint8Array(64);
const block = salsa20Core(state, rounds);
// Convert words to bytes
for (let i = 0; i < 16; i++) {
intToLittleEndian(block[i], result, i * 4);
}
// Increment counter
state[8] = (state[8] + 1) >>> 0;
if (state[8] === 0) {
state[9] = (state[9] + 1) >>> 0;
}
return result;
}
// Encrypt/decrypt function (XOR with keystream)
function processBlock(input, output, state, rounds) {
const keystream = generateKeystreamBlock(state, rounds);
for (let i = 0; i < input.length && i < 64; i++) {
output[i] = input[i] ^ keystream[i];
}
}
// Main Salsa20 encryption/decryption function
function salsa20(input, key, nonce, rounds = 20) {
const output = new Uint8Array(input.length);
const state = expandKey(key, nonce);
for (let i = 0; i < input.length; i += 64) {
const chunk = input.subarray(i, Math.min(i + 64, input.length));
processBlock(chunk, output.subarray(i, i + chunk.length), state, rounds);
}
return output;
}
// Helper to convert hex string to byte array
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
// Helper to convert byte array to hex string
function bytesToHex(bytes) {
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// Example usage
function encryptSalsa20(plaintext, keyHex, nonceHex, rounds = 20) {
const plaintextBytes = new TextEncoder().encode(plaintext);
const keyBytes = hexToBytes(keyHex);
const nonceBytes = hexToBytes(nonceHex);
const ciphertext = salsa20(plaintextBytes, keyBytes, nonceBytes, rounds);
return bytesToHex(ciphertext);
}
function decryptSalsa20(ciphertextHex, keyHex, nonceHex, rounds = 20) {
const ciphertextBytes = hexToBytes(ciphertextHex);
const keyBytes = hexToBytes(keyHex);
const nonceBytes = hexToBytes(nonceHex);
const plaintextBytes = salsa20(ciphertextBytes, keyBytes, nonceBytes, rounds);
return new TextDecoder().decode(plaintextBytes);
}
Implementation Notes
- This is a pure JavaScript implementation of Salsa20 for educational purposes.
- For production use, consider using established cryptographic libraries.
- The implementation follows the Salsa20 specification by Daniel J. Bernstein.
- The code includes the core algorithm, key expansion, and helper functions.
- Salsa20 is a stream cipher, so it generates a keystream that is XORed with the plaintext.
- The same function is used for both encryption and decryption (symmetric).
- The implementation supports the standard 20 rounds, but can be configured for 8 or 12 rounds.