Skip to content

Commit

Permalink
v2: Fixed block size with padding (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahdinosaur committed Nov 23, 2023
1 parent 56164f4 commit d8a4bf2
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 29 deletions.
42 changes: 37 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# pull-secretstream

replacement for [`pull-box-stream`](https://github.com/dominictarr/pull-box-stream) using libsodium's [secretstream](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream)
Replacement for [`pull-box-stream`](https://github.com/dominictarr/pull-box-stream) using libsodium's [secretstream](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream)

## example
Uses a fixed ciphertext block size. (By default: 512 bytes.)

## Example

```js
const { randomBytes } = require('crypto')
const pull = require('pull-stream')
const { KEYBYTES, createEncryptStream, createDecryptStream } = require('pull-secretstream')
const { KEY_SIZE, createEncryptStream, createDecryptStream } = require('pull-secretstream')

// generate a random secret, `KEYBYTES` bytes long.
const key = randomBytes(KEYBYTES)
// generate a random secret, `KEY_SIZE` bytes long.
const key = randomBytes(KEY_SIZE)

const plaintext1 = Buffer.from('hello world')

Expand All @@ -34,3 +36,33 @@ pull(
}),
)
```

## API

### `createEncryptStream(key, ciphertextBlockSize = DEFAULT_BLOCK_SIZE)`

Returns a "through" pull-stream that:

- first sends the secretstream header,
- then encrypts incoming plaintext as secretstream ciphertext (of a fixed block size, padding if necessary),
- and when done, sends a secrestream message marked as the final.

### `createDecryptStream(key, ciphertextBlockSize = DEFAULT_BLOCK_SIZE)`

Returns a "through" pull-stream that:

- first recives the secretstream header,
- then decrypts incoming secretstream ciphertext as plaintext (unpadding if necessary),
- and is done when a secretstream message marked as final is received.

### `DEFAULT_BLOCK_SIZE`

512 bytes

### `KEY_SIZE`

32 bytes

### `getPlaintextBlockSize(ciphertextBlockSize)`

`cipherBlockSize` - 17 bytes (secretstream's additional data)
6 changes: 3 additions & 3 deletions examples/readme.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const { randomBytes } = require('crypto')
const pull = require('pull-stream')
const { KEYBYTES, createEncryptStream, createDecryptStream } = require('../')
const { KEY_SIZE, createEncryptStream, createDecryptStream } = require('../')

// generate a random secret, `KEYBYTES` bytes long.
const key = randomBytes(KEYBYTES)
// generate a random secret, `KEY_SIZE` bytes long.
const key = randomBytes(KEY_SIZE)

const plaintext1 = Buffer.from('hello world')

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@
},
"homepage": "https://github.com/ahdinosaur/pull-secretstream#readme",
"dependencies": {
"bl": "^6.0.8",
"debug": "^4.3.4",
"pull-cat": "^1.1.11",
"pull-header": "^0.0.0",
"pull-map-last": "^1.0.0",
"pull-reader": "^1.3.1",
"pull-stream": "^3.7.0",
"pull-through": "^1.0.18",
"sodium-secretstream": "^1.1.0"
"sodium-secretstream": "^1.1.0",
"sodium-universal": "^4.0.0"
}
}
89 changes: 71 additions & 18 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
const { KEYBYTES, HEADERBYTES, Push, Pull } = require('sodium-secretstream')
const { KEYBYTES, HEADERBYTES, ABYTES, Push, Pull } = require('sodium-secretstream')
const { BufferList } = require('bl')
const pull = require('pull-stream/pull')
const pullCat = require('pull-cat')
const pullHeader = require('pull-header')
const pullThrough = require('pull-through')
const createDebug = require('debug')
const { sodium_pad, sodium_unpad } = require('sodium-universal')

const DEFAULT_BLOCK_SIZE = 512 // bytes

createDebug.formatters.h = (v) => {
return v.toString('hex')
Expand All @@ -12,18 +16,26 @@ createDebug.formatters.h = (v) => {
const debug = createDebug('pull-secretstream')

module.exports = {
KEYBYTES,
KEY_SIZE: KEYBYTES,
DEFAULT_BLOCK_SIZE,
createEncryptStream,
createDecryptStream,
getPlaintextBlockSize,
}

function getPlaintextBlockSize(ciphertextBlockSize = DEFAULT_BLOCK_SIZE) {
return ciphertextBlockSize - ABYTES
}

function createEncryptStream(key) {
function createEncryptStream(key, ciphertextBlockSize = DEFAULT_BLOCK_SIZE) {
if (key.length !== KEYBYTES) {
throw new Error(`pull-secretstream/createEncryptStream: key must be byte length of ${KEYBYTES}`)
}
const debugKey = key.slice(0, 2)

const debugKey = key.slice(0, 2)
const encrypter = new Push(key)
const plaintextBlockSize = getPlaintextBlockSize(ciphertextBlockSize)
const plaintextBufferList = new BufferList()

const sendHeader = () => {
let hasSentHeader = false
Expand All @@ -42,13 +54,36 @@ function createEncryptStream(key) {

const encryptMap = pullThrough(
function encryptThroughData(plaintext) {
debug('%h : encrypting plaintext %h', debugKey, plaintext)
const ciphertext = encrypter.next(plaintext)
debug('%h : encrypted ciphertext %h', debugKey, ciphertext)
this.queue(ciphertext)
plaintextBufferList.append(plaintext)

// while we still have enough bytes to send full blocks
while (plaintextBufferList.length >= plaintextBlockSize) {
const plaintextBlock = plaintextBufferList.slice(0, plaintextBlockSize)
plaintextBufferList.consume(plaintextBlockSize)
debug('%h : encrypting block %h', debugKey, plaintextBlock)
const ciphertext = encrypter.next(plaintextBlock)
debug('%h : encrypted ciphertext %h', debugKey, ciphertext)
this.queue(ciphertext)
}

// send the remaining as a padded block
if (plaintextBufferList.length > 0) {
const plaintextLength = plaintextBufferList.length
const plaintextBlock = Buffer.alloc(plaintextBlockSize)
plaintextBufferList.copy(plaintextBlock, 0, 0, plaintextLength)
plaintextBufferList.consume(plaintextLength)
sodium_pad(plaintextBlock, plaintextLength, plaintextBlockSize)
debug('%h : encrypting padded block %h', debugKey, plaintextBlock)
const ciphertext = encrypter.next(plaintextBlock)
debug('%h : encrypted ciphertext %h', debugKey, ciphertext)
this.queue(ciphertext)
}
},
function encryptThroughEnd() {
const final = encrypter.final()
// send a block full of zeros with the final marker
const finalBlock = Buffer.alloc(plaintextBlockSize)
sodium_pad(finalBlock, 0, plaintextBlockSize)
const final = encrypter.final(finalBlock, Buffer.allocUnsafe(plaintextBlockSize + ABYTES))
debug('%h : encrypter final %h', debugKey, final)
this.queue(final)
this.queue(null)
Expand All @@ -60,13 +95,15 @@ function createEncryptStream(key) {
}
}

function createDecryptStream(key) {
function createDecryptStream(key, ciphertextBlockSize = DEFAULT_BLOCK_SIZE) {
if (key.length !== KEYBYTES) {
throw new Error(`pull-secretstream/createDecryptStream: key must be byte length of ${KEYBYTES}`)
}
const debugKey = key.slice(0, 2)

const debugKey = key.slice(0, 2)
const decrypter = new Pull(key)
const plaintextBlockSize = getPlaintextBlockSize(ciphertextBlockSize)
const ciphertextBufferList = new BufferList()

const receiveHeader = pullHeader(HEADERBYTES, (header) => {
debug('%h : decrypter receiving header %h', debugKey, header)
Expand All @@ -75,13 +112,29 @@ function createDecryptStream(key) {

const decryptMap = pullThrough(
function decryptThroughData(ciphertext) {
debug('%h : decrypting ciphertext %h', debugKey, ciphertext)
const plaintext = decrypter.next(ciphertext)
debug('%h : decrypted ciphertext %h', debugKey, plaintext)
this.queue(plaintext)
if (decrypter.final) {
debug('%h : decrypter final', debugKey)
this.emit('end')
ciphertextBufferList.append(ciphertext)

// while we still have enough bytes for full blocks
while (ciphertextBufferList.length >= ciphertextBlockSize) {
const ciphertextBlock = ciphertextBufferList.slice(0, ciphertextBlockSize)
ciphertextBufferList.consume(ciphertextBlockSize)
debug('%h : decrypting block %h', debugKey, ciphertextBlock)
const plaintextBlock = decrypter.next(ciphertextBlock)
debug('%h : decrypted plaintext %h', debugKey, plaintextBlock)
const plaintextLength = sodium_unpad(
plaintextBlock,
plaintextBlock.length,
plaintextBlockSize,
)
const plaintext = plaintextBlock.slice(0, plaintextLength)
debug('%h : unpadded plaintext %h', debugKey, plaintext)
this.queue(plaintext)

if (decrypter.final) {
debug('%h : decrypter final', debugKey)
this.emit('end')
break
}
}
},
function decryptThroughEnd() {
Expand Down
4 changes: 2 additions & 2 deletions test/basics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ const test = require('node:test')
const assert = require('node:assert')
const pull = require('pull-stream')
const { randomBytes } = require('crypto')
const { KEYBYTES, createEncryptStream, createDecryptStream } = require('../')
const { KEY_SIZE, createEncryptStream, createDecryptStream } = require('../')

test('test basic encryptStream and decryptStream', async (t) => {
// generate a random secret, `KEYBYTES` bytes long.
const key = randomBytes(KEYBYTES)
const key = randomBytes(KEY_SIZE)

const plaintext1 = Buffer.from('hello world')

Expand Down

0 comments on commit d8a4bf2

Please sign in to comment.