Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript typings #8

Merged
merged 4 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading