Symmetric Cryptography: Stream Ciphers in JavaScipt

Todd Brown
Nerd For Tech
Published in
5 min readMar 29, 2021

--

digital key

Symmetric Cryptography, in particular encryption, has a requirement that a message is both encrypted and decrypted with the same key. Under this umbrella we find Stream and Block ciphers. Stream Ciphers work on bits one at a time while Block Ciphers work on blocks (or groups) of bits. There is plenty written regarding Symmetric Cryptography as well as Stream and Block ciphers. This article is an exercise in implementing a Stream Cipher with modern JavaScript. The focus is on implementing a simple cipher and readability of the code — not speed nor secrecy. It is worth mentioning: if you need security inside of your application use a well known and tested algorithm. However, I encourage you to experiment and implement some of these ciphers on your own, it’s a great exercise.

Th basis of the algorithm is XOR, a boolean operator which returns true when the two bits being compared are different. It has the feature that

(a XOR b) XOR a == b

This is significant as the same key (a) can be applied once to encrypt and applied again to decrypt. Here is the truth table (well the left portion is the truth table) for XOR with reversal (right portion) . You can think of p as plain bit, kas the key bit, and c as the cipher bit.

p k | c  ->  c k | p
---------------------
1 1 | 0 -> 0 1 | 1
1 0 | 1 -> 1 0 | 1
0 1 | 1 -> 1 1 | 0
0 0 | 0 -> 1 0 | 0

The key (pun intended) to this methodology is the size of the key (number of bits) and the randomness of the key (Randomness is a well studied area). Thankfully randomness doesn’t affect the implementation of the cipher, however length is important to our implementation. A One Time Pad is ideal, in that the key size is identical to the plain text size, offering the most security. However using a OTP represents challenges that make it impractical (large size is difficult to exchange, generate…). We want a technology that is able to cycle over a series of bits where the end wraps back around to the beginning — a ring. Perpetual “Generator Functions” (semi-coroutines introduced in ES6) will suit the need perfectly.

const ByteKeyRing = function *(byteLength, keyBitArray) {
// store the length so we know when to wrap
const keyBitArrayLength = keyBitArray.length

// loop in perpetuity incrementing i
let i = 0
while(true) {

// build a byte according to the specified length
const byte = []
for(let n = 0; n < byteLength; n++)
{
// i mod keyBitArrayLength in {0..keyBitArrayLength-1}
const index = i % keyBitArrayLength

// store the bit
const bit = keyBitArray[index]
byte.push(bit)
i = i + 1
}

// return out a byte by joining to a string and parsing base 2
yield parseInt(byte.join(''),2)
}
}

The generator function takes in a byte length and an array of 0’s and 1’s. It iterates over that array building a byte and returning once it is filled it returns it to the invoker. The important detail is that the next request starts where the last request left off, the iterating over the array doesn’t start over. The modulo operator provides us a nice mechanism not to have to manage the wrapping ourselves. Consider an input array of a length of 4:

i  % 4 = x
----------
0 % 4 = 0
1 % 4 = 1
2 % 4 = 2
3 % 4 = 3
4 % 4 = 0
5 % 4 = 1
.
.
.
67 % 4 = 3

As i increases (in perpetuity) if we modulo by four the result will always be in the set {0, 1, 2, 3 }. Looking at the ring in detail

const ring = ByteKeyRing(8, [0,0,1])

const thirtySix = ring.next()
//=> 36 (00100100)

const oneFourtySix = ring.next()
//=> 146 (10010010)

const seventyThree = ring.next()
//=> 73 (01001001)

const thirtySixAgain = ring.next()
//=> 36 (00100100)

We are seeding the ring with the array [0,0,1] producing the pereptual stream 001001001001001001001001001001001…001. If we take the

  • left 8 most bits (00100100) we have the value 36,
  • followed next 8 bytes from that perpetual stream is (10010010) the value 146,
  • followed by the next 8 bytes (01001001) the value 73
  • THEN the entire sequence repeats.

This approach allows us an easy way of using a small key (in this case three bits) to encrypt a message of any size. The encryption/decryption is performed by a tiny amount of code that applies XOR to a buffer containing plain text returning a buffer of cipher text, where the key and the plain text buffer are inputs (readBuffer, xor, bufferOf are helpers defined in the source):

const cipherBuffer = (keyByte, byteBuffer) => {
const plainByte = readBuffer(byteBuffer)
const cipherByte = xor(keyByte, plainByte)
const cipherByteBuffer = bufferOf(cipherByte)
return cipherByteBuffer
}

The streaming bit is fairly easy given the support for Steams in Node. In this case we leverage the Transform Stream:

const { Transform } = require('stream');

const keyIterator = (byteKeyArray) => {
const byteSize = 8
const byteKeyRing = ByteKeyRing(byteSize, byteKeyArray)
return () => byteKeyRing.next().value
}

const streamCipher = (byteKeyArray) =>{

const nextKey = keyIterator(byteKeyArray)

return new Transform({
transform(byteBuffer, encoding, callback) {

const keyByte = nextKey()
const cipherByteBuffer = cipherBuffer(
keyByte, byteBuffer)
this.push(cipherByteBuffer);
callback();

}
});
}

Here keyIterator is used to abstract the Key Ring from the and the generator logic from the remaining cipher implementation. The real logic here are these four lines:

const keyByte = nextKey()
const cipherByteBuffer = cipherBuffer(keyByte, byteBuffer)

this.push(cipherByteBuffer);
callback();

Essentially three steps

  1. Generate the next key
  2. take the input buffer and perform the cipher on it
  3. move it down the stream (push and callback are TransformStream inner workings)

To use it you instantiate a streamCipher with a key and pipe some text through:

The simple message is converted to some cipher (again this is a simple cipher not secure). The round trip encrypt -> decrypt requires to instantiate two ciphers with the same key (the same key is very important) pipe text through one cipher (encrypt) and then through another cipher (decrypt).

It really is that simple! Code is here.

Originally published at https://www.linkedin.com.

--

--

Todd Brown
Nerd For Tech

A 25 year software industry veteran with a passion for functional programming, architecture, mentoring / team development, xp/agile and doing the right thing.