Skip to content

Commit

Permalink
TypeScript typings (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahdinosaur committed Dec 7, 2023
1 parent e9530f4 commit 24d7318
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 58 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
npm-debug.log*
package-lock.json
*.d.ts
17 changes: 16 additions & 1 deletion js/pull-secret-channel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,24 @@
"name": "pull-secret-channel",
"version": "1.0.0",
"description": "Pull stream of authenticated encryption ChaCha20-Poly1305",
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"exports": {
".": "./src/index.js",
"./javascript": "./src/javascript.js"
},
"browser": {
"./src/index.js": "./src/javascript.js"
},
"files": [
"src/**/*"
],
"scripts": {
"test": "node --test"
"prepublishOnly": "npm run build",
"build": "npm run test:types && tsc --build",
"test": "npm run test:types && node --test",
"test:types": "tsc --build --clean"
},
"repository": {
"type": "git",
Expand Down
122 changes: 87 additions & 35 deletions js/pull-secret-channel/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// @ts-ignore
const pullThrough = require('pull-through')
// @ts-ignore
const pullReader = require('pull-reader')
const {
createEncrypter,
Expand All @@ -17,16 +19,36 @@ module.exports = {
TAG_SIZE,
}

/**
@typedef {Buffer | Uint8Array} B4A
@typedef {null | true | Error} End
@typedef {(end: End, cb: (end: End, data?: any) => void) => void} Source
@typedef {{
queue: (buf: B4A | null) => void
}} PullThroughThis
*/

/**
* @param {B4A} key
* @param {B4A} nonce
*/
function pullEncrypter(key, nonce) {
const encrypter = createEncrypter(key, nonce)

return pullThrough(
/**
* @this {PullThroughThis}
* @param {B4A} contentPlaintext
*/
function pullEncrypterData(contentPlaintext) {
const [lengthCiphertext, contentCiphertext] = encrypter.next(contentPlaintext)
this.queue(lengthCiphertext)
this.queue(contentCiphertext)
},

/**
* @this {PullThroughThis}
*/
function pullEncrypterEnd() {
const endCiphertext = encrypter.end()
this.queue(endCiphertext)
Expand All @@ -35,67 +57,97 @@ function pullEncrypter(key, nonce) {
)
}

/**
* @param {B4A} key
* @param {B4A} nonce
*/
function pullDecrypter(key, nonce) {
const decrypter = createDecrypter(key, nonce)

/** @type {End} */
let ending = null
const reader = pullReader()

/**
* @param {Source} read
*/
return function pullDecrypterThrough(read) {
reader(read)

/**
* @param {End} end
* @param {(end: End, data?: B4A) => void} cb
*/
return function pullDecrypterSource(end, cb) {
if (end) return reader.abort(end, cb)
if (ending) return cb(ending)

reader.read(LENGTH_OR_END_CIPHERTEXT, function (err, lengthOrEndCiphertext) {
if (err) {
if (err === true) {
ending = new Error(
'pull-secret-channel/decrypter: stream ended before end-of-stream message',
)
} else {
ending = err
}
return cb(ending)
}

let lengthOrEnd
try {
lengthOrEnd = decrypter.lengthOrEnd(lengthOrEndCiphertext)
} catch (err) {
ending = err
// TODO attach error context
return abort(err)
}

if (lengthOrEnd.type === 'end-of-stream') {
ending = true
return cb(ending)
}

const { length } = lengthOrEnd
reader.read(length + TAG_SIZE, function (err, contentCiphertext) {
reader.read(
LENGTH_OR_END_CIPHERTEXT,

/**
* @param {End} err
* @param {B4A} lengthOrEndCiphertext
*/
function (err, lengthOrEndCiphertext) {
if (err) {
ending = err
if (err === true) {
ending = new Error(
'pull-secret-channel/decrypter: stream ended before end-of-stream message',
)
} else {
ending = err
}
return cb(ending)
}

let contentPlaintext
let lengthOrEnd
try {
contentPlaintext = decrypter.content(contentCiphertext)
} catch (err) {
lengthOrEnd = decrypter.lengthOrEnd(lengthOrEndCiphertext)
} catch (/** @type any */ err) {
ending = err
// TODO attach error context
return abort(err)
}

cb(null, contentPlaintext)
})
})
if (lengthOrEnd.type === 'end-of-stream') {
ending = true
return cb(ending)
}

const { length } = lengthOrEnd
reader.read(
length + TAG_SIZE,
/**
* @param {End} err
* @param {B4A} contentCiphertext
*/
function (err, contentCiphertext) {
if (err) {
ending = err
return cb(ending)
}

let contentPlaintext
try {
contentPlaintext = decrypter.content(contentCiphertext)
} catch (/** @type any */ err) {
ending = err
// TODO attach error context
return abort(err)
}

cb(null, contentPlaintext)
},
)
},
)

// use abort when the input was invalid,
// but the source hasn't actually ended yet.
/**
* @param {End} err
*/
function abort(err) {
ending = err || true
reader.abort(ending, cb)
Expand Down
5 changes: 5 additions & 0 deletions js/pull-secret-channel/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*.js"],
"exclude": ["node_modules/", "test/"]
}
15 changes: 14 additions & 1 deletion js/secret-channel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@
"name": "secret-channel",
"version": "1.0.0",
"description": "Streaming authenticated encryption using ChaCha20-Poly1305",
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"exports": {
".": "./src/index.js",
"./javascript": "./src/javascript.js"
},
"browser": {
"./src/index.js": "./src/javascript.js"
},
"files": [
"src/**/*"
],
"scripts": {
"test": "node --test"
"prepublishOnly": "npm run build",
"build": "npm run test:types && tsc --build",
"test": "npm run test:types && node --test",
"test:types": "tsc --build --clean"
},
"repository": {
"type": "git",
Expand All @@ -32,6 +44,7 @@
"homepage": "https://github.com/ahdinosaur/secret-channel#readme",
"dependencies": {
"@noble/ciphers": "^0.4.0",
"@types/sodium-native": "^2.3.9",
"b4a": "^1.6.4",
"debug": "^4.3.4",
"sodium-native": "^4.0.4"
Expand Down
31 changes: 28 additions & 3 deletions js/secret-channel/src/crypto-javascript.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
const b4a = require('b4a')
const { chacha20poly1305 } = require('@noble/ciphers/chacha')

const debug = require('./debug')

/**
* @typedef {import('./types').B4A} B4A
*/

module.exports = {
encrypt,
decrypt,
increment,
isZero,
}

/**
* @param {B4A} keyArg
* @param {B4A} nonceArg
* @param {B4A} plaintextArg
* @returns {B4A}
*/
function encrypt(keyArg, nonceArg, plaintextArg) {
// ensure args are Uint8Array's, even in Node.js
const key = Uint8Array.from(keyArg)
Expand All @@ -21,23 +30,39 @@ function encrypt(keyArg, nonceArg, plaintextArg) {
return ciphertext
}

/**
* @param {B4A} key
* @param {B4A} nonce
* @param {B4A} ciphertext
* @returns {B4A}
*/
function decrypt(key, nonce, ciphertext) {
return chacha20poly1305(key, nonce).encrypt(ciphertext)
}

/**
* @param {B4A} buf
* @returns {void}
*/
function increment(buf) {
const len = buf.length
let c = 1
for (let i = 0; i < len; i++) {
c += buf[i]
c += /** @type number */ (buf[i])
buf[i] = c
c >>= 8
}
}

/**
* @param {B4A} buf
* @returns {boolean}
*/
function isZero(buf) {
const len = buf.length
let d = 0
for (let i = 0; i < len; i++) d |= buf[i]
for (let i = 0; i < len; i++) {
d |= /** @type number */ (buf[i])
}
return d === 0
}
36 changes: 32 additions & 4 deletions js/secret-channel/src/crypto-native.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,67 @@
const b4a = require('b4a')
const {
// @ts-ignore
crypto_aead_chacha20poly1305_ietf_encrypt: sodiumEncrypt,
// @ts-ignore
crypto_aead_chacha20poly1305_ietf_decrypt: sodiumDecrypt,
// @ts-ignore
crypto_aead_chacha20poly1305_ietf_ABYTES: ABYTES,
sodium_increment: sodiumIncrement,
sodium_is_zero: sodiumIsZero,
} = require('sodium-native')

const debug = require('./debug')

/**
* @typedef {import('./types').B4A} B4A
*/

module.exports = {
encrypt,
decrypt,
increment,
isZero,
}

/**
* @param {B4A} key
* @param {B4A} nonce
* @param {B4A} plaintext
* @returns {B4A}
*/
function encrypt(key, nonce, plaintext) {
debug('encrypt( %h , %h , %h )', key.slice(0, 2), nonce, plaintext)
const ciphertext = b4a.allocUnsafe(plaintext.length + ABYTES)
const ciphertext = Buffer.allocUnsafe(plaintext.length + ABYTES)
sodiumEncrypt(ciphertext, plaintext, null, null, nonce, key)
debug('encrypt -> %h', ciphertext)
return ciphertext
}

/**
* @param {B4A} key
* @param {B4A} nonce
* @param {B4A} ciphertext
* @returns {B4A}
*/
function decrypt(key, nonce, ciphertext) {
const plaintext = b4a.allocUnsafe(ciphertext.length - ABYTES)
const plaintext = Buffer.allocUnsafe(ciphertext.length - ABYTES)
sodiumDecrypt(plaintext, null, ciphertext, null, nonce, key)
return plaintext
}

/**
* @param {B4A} buffer
* @returns {void}
*/
function increment(buffer) {
// @ts-ignore
sodiumIncrement(buffer)
}

/**
* @param {B4A} buffer
* @returns {boolean}
*/
function isZero(buffer) {
return sodiumIsZero(buffer)
// @ts-ignore
return sodiumIsZero(buffer, buffer.length)
}
Loading

0 comments on commit 24d7318

Please sign in to comment.