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

Coding Test #42

Open
wants to merge 49 commits into
base: readme2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
ec32e80
Merge pull request #9 from SocketDev/readme2
jhiesey Apr 20, 2021
be402be
0.1.0
feross Apr 20, 2021
e998d4b
Make transformer.transform optional
jhiesey Apr 21, 2021
dcb275b
Merge pull request #10 from SocketDev/make-transform-optional
feross Apr 21, 2021
d178d6d
0.1.1
feross Apr 21, 2021
3a7d7f4
Update airtap-sauce requirement from ^1.1.0 to ^1.1.2
dependabot[bot] Apr 26, 2021
cb4091b
Merge pull request #11 from SocketDev/dependabot/npm_and_yarn/airtap-…
feross May 3, 2021
3752ecc
Bump actions/checkout from 2 to 2.3.4
dependabot[bot] May 12, 2021
e0fb350
Merge pull request #13 from SocketDev/dependabot/github_actions/actio…
feross May 21, 2021
323b776
Add .whitesource configuration file
mend-bolt-for-github[bot] Jun 1, 2021
044b4a0
Merge pull request #14 from SocketDev/whitesource/configure
feross Jun 2, 2021
90d79f8
Delete .whitesource
feross Jun 3, 2021
5780af9
Return a promise from transformStream shim
jhiesey Jun 11, 2021
4099a6c
Make transformer optional
jhiesey Jun 11, 2021
27148f9
comment: Safari 14.1 supports TransformStream
feross Jun 16, 2021
5ea470c
Code review fixes
jhiesey Jun 16, 2021
fa846d7
Merge pull request #21 from SocketDev/transform-promise
jhiesey Jun 16, 2021
f548bba
0.2.0
feross Jun 16, 2021
784ee9d
fix for browsers that don't support ?. syntax
feross Jun 22, 2021
43a37d5
Merge pull request #23 from SocketDev/fix-old-browsers
feross Jun 22, 2021
161ae40
0.2.1
feross Jun 22, 2021
2b2e209
don't create transform object in native code path
feross Jun 22, 2021
4453475
0.2.2
feross Jun 22, 2021
9bfb16d
Enable decrypting partial streams
jhiesey Jun 22, 2021
dc18640
Test range decryption
jhiesey Jun 22, 2021
de1d7f9
Improve padding validation when seeking
jhiesey Jun 23, 2021
2f4bd4c
Code review fixes
jhiesey Jun 23, 2021
ec78012
Merge pull request #24 from SocketDev/decrypt-seek
jhiesey Jun 23, 2021
60730b8
0.3.0
jhiesey Jun 23, 2021
2700e53
Update ci.yml
feross Jun 25, 2021
fc42e5b
Merge pull request #26 from SocketDev/remove-environments
jhiesey Jun 25, 2021
c5b552f
Add missing methods to transform stream controller
jhiesey Jun 24, 2021
3a3db52
Always provide the same controller to transform stream methods
jhiesey Jun 24, 2021
d452516
Code review fixes
jhiesey Jun 25, 2021
3329a58
Merge pull request #25 from SocketDev/add-missing-transform-methods
jhiesey Jun 25, 2021
70f5652
0.3.1
jhiesey Jun 25, 2021
23ba6a1
Update tape requirement from ^5.2.2 to ^5.3.0
dependabot[bot] Jul 27, 2021
37e4130
Merge pull request #27 from SocketDev/dependabot/npm_and_yarn/tape-tw…
feross Jul 27, 2021
36af364
Bump actions/checkout from 2.3.4 to 2.4.0
dependabot[bot] Nov 3, 2021
fce6690
Update airtap requirement from ^4.0.3 to ^4.0.4
dependabot[bot] Nov 29, 2021
53a2bab
Update tape requirement from ^5.3.0 to ^5.4.0
dependabot[bot] Dec 27, 2021
9cade35
Merge pull request #35 from SocketDev/dependabot/npm_and_yarn/tape-tw…
feross Jan 7, 2022
308ffdc
Merge pull request #34 from SocketDev/dependabot/npm_and_yarn/airtap-…
feross Jan 7, 2022
7d2edc6
Merge pull request #32 from SocketDev/dependabot/github_actions/actio…
feross Jan 7, 2022
014a6f9
Add socket badge to README
Raynos Jan 19, 2023
0b42aac
Merge pull request #56 from SocketDev/Raynos-patch-1
feross Jan 19, 2023
b6c6641
Fix socket badge URL
Raynos Jan 20, 2023
2beaeac
unbreak CI
Raynos Jan 20, 2023
c25a0d9
Merge pull request #57 from SocketDev/Raynos-patch-1
Raynos Jan 20, 2023
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
18 changes: 5 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,17 @@ name: ci
- pull_request
jobs:
test:
name: Node ${{ matrix.node }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
environment: ci
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
node:
- '14'
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v2.4.0
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
node-version: 16
- run: npm install
- run: npm run build --if-present
- run: echo "127.0.0.1 airtap.local" | sudo tee -a /etc/hosts
- run: npm test
- run: npm run lint
env:
SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }}
SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }}
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# wormhole-crypto [![ci][ci-image]][ci-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url]
# wormhole-crypto

[![Socket Badge](https://socket.dev/api/badge/npm/package/wormhole-crypto)](https://socket.dev/npm/package/wormhole-crypto)
[![ci][ci-image]][ci-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url]

[ci-image]: https://img.shields.io/github/workflow/status/SocketDev/wormhole-crypto/ci/master
[ci-url]: https://github.com/SocketDev/wormhole-crypto/actions
Expand Down Expand Up @@ -187,6 +190,21 @@ Returns: `Promise[ReadableStream]`
Returns a `Promise` that resolves to a `ReadableStream` decryption stream that
consumes the data in `encryptedStream` and returns a plaintext version.

### `keychain.decryptStreamRange(offset, length, totalEncryptedLength)`

Type: `Function`

Returns: `Promise[{ ranges, decrypt }]`

Returns a `Promise` that resolves to a object containing `ranges`, which is an array of
objects containing `offset` and `length` integers specifying the encrypted byte ranges
that are needed to decrypt the client's specified range, and a `decrypt` function.

Once the client has gathered a stream for each byte range in `ranges`, the client
should call `decrypt(streams)`, where `streams` is an array of `ReadableStream` objects,
one for each of the requested ranges. `decrypt` will then return a `ReadableStream`
containing the plaintext data for the client's desired byte range.

#### `encryptedStream`

Type: `ReadableStream`
Expand Down
49 changes: 49 additions & 0 deletions lib/concat-streams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-env browser */

/**
* Concatenates the array of ReadableStreams passed in `inputStreams` into a single
* ReadableStream.
*
* @param {ReadableStream[]} inputStreams
* @returns ReadableStream
*/
export function concatStreams (inputStreams) {
let currentReader = null

// Move to the next stream
const nextStream = (controller) => {
const stream = inputStreams.shift()
if (stream !== undefined) {
currentReader = stream.getReader()
} else {
currentReader = null
controller.close()
}
}

return new ReadableStream({
start (controller) {
nextStream(controller)
},

async pull (controller) {
// eslint-disable-next-line no-unmodified-loop-condition
while (currentReader !== null) {
const { value, done } = await currentReader.read()
if (done) {
nextStream(controller)
} else {
controller.enqueue(value)
break
}
}
},

async cancel (reason) {
await Promise.all([
currentReader && currentReader.cancel(reason),
...inputStreams.map(stream => stream.cancel(reason))
])
}
})
}
123 changes: 116 additions & 7 deletions lib/ece.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* specification. See: https://tools.ietf.org/html/rfc8188
*/

import { concatStreams } from './concat-streams.js'
import { transformStream } from './transform-stream.js'
import { ExtractTransformer } from './extract-transformer.js'
import { SliceTransformer } from './slice-transformer.js'

const MODE_ENCRYPT = 'encrypt'
Expand Down Expand Up @@ -79,11 +81,12 @@ export function encryptStream (
const stream = transformStream(
input,
new SliceTransformer(rs - TAG_LENGTH - 1)
)
).readable

return transformStream(
stream,
new ECETransformer(MODE_ENCRYPT, secretKey, rs, salt)
)
).readable
}

/**
Expand All @@ -94,11 +97,92 @@ export function encryptStream (
* rs: int containing record size, optional
*/
export function decryptStream (input, secretKey, rs = RECORD_SIZE) {
const stream = transformStream(input, new SliceTransformer(HEADER_LENGTH, rs))
const stream = transformStream(input, new SliceTransformer(HEADER_LENGTH, rs)).readable

return transformStream(
stream,
new ECETransformer(MODE_DECRYPT, secretKey, rs)
)
new ECETransformer(MODE_DECRYPT, secretKey, rs, null)
).readable
}

/**
* Given a desired plaintext byte range specified by `offset` and `length`, and the
* total size of the encrypted stream in `totalEncryptedLength`, provides a mechanism to
* decrypt that range.
*
* To decrypt an arbitrary plaintext range, the client will need to supply multiple
* (currently always two) ranges of encrypted data. `decryptStreamRange` returns a promise
* that resolves to an object containing `ranges`, which is an array of { offset, length }
* entries specifying the needed encrypted byte ranges, and `encrypt`, a callback function.
*
* Once the client has gathered an array `streams` of encrypted ReadableStreams, one for
* each of these ranges, it should call `encrypt(streams)`. This will then return the final
* plaintext ReadableStream.
*
* secretKey: CryptoKey containing secret key of size KEY_LENGTH
* offset: int containing plaintext byte offset at which to start decryption
* length: int containing the number of plaintext bytes to decrypt
* totalEncryptedLength: The total number of bytes in the encrypted stream
* rs: int containing record size, optional
*/
export function decryptStreamRange (secretKey, offset, length, totalEncryptedLength, rs = RECORD_SIZE) {
if (!Number.isInteger(rs)) {
throw new TypeError('rs')
}

// Chunk metadata, tag and delimiter
const chunkMetaLength = TAG_LENGTH + 1

// First record needed to decrypt the range
const startRecord = Math.floor(offset / (rs - chunkMetaLength))
const offsetInStartRecord = offset % (rs - chunkMetaLength)

// Record after the last record needed to decrypt the range
const endRecord = Math.ceil((offset + length) / (rs - chunkMetaLength))

// Range needed for data (not header) stream
const dataOffset = HEADER_LENGTH + startRecord * rs
let dataEnd = HEADER_LENGTH + endRecord * rs // exclusive

// Determine if the stream ends at the end of the encrypted file.
// This is necessary to correctly validate the padding of the final record.
const endsPrematurely = dataEnd < totalEncryptedLength
if (!endsPrematurely) {
dataEnd = totalEncryptedLength
}

return {
ranges: [
{
offset: 0,
length: HEADER_LENGTH
}, {
offset: dataOffset,
length: dataEnd - dataOffset
}
],
decrypt: (streams) => {
if (!(streams.every(stream => stream instanceof ReadableStream))) {
throw new TypeError('stream')
}

// Combine the header and data streams, and then slice how ECETransformer expects
const encryptedStream = transformStream(concatStreams(streams), new SliceTransformer(HEADER_LENGTH, rs)).readable

// Plaintext stream of needed records
const plaintextStream = transformStream(
encryptedStream,
new ECETransformer(MODE_DECRYPT, secretKey, rs, null, {
startSeq: startRecord,
endSeq: endRecord,
endsPrematurely
})
).readable

// Extract the exact needed bytes from the plaintext stream
return transformStream(plaintextStream, new ExtractTransformer(offsetInStartRecord, length)).readable
}
}
}

function checkSecretKey (secretKey) {
Expand All @@ -123,7 +207,7 @@ function generateSalt (len) {
}

class ECETransformer {
constructor (mode, secretKey, rs, salt) {
constructor (mode, secretKey, rs, salt, seekOpts = {}) {
if (mode !== MODE_ENCRYPT && mode !== MODE_DECRYPT) {
throw new Error('mode must be either encrypt or decrypt')
}
Expand All @@ -136,6 +220,12 @@ class ECETransformer {
this.rs = rs
this.salt = salt

// seekOpts can contain (for decryption only):
// startSeq: first record sequence number
// endSeq: last record sequence number + 1 (exclusive)
// endsPrematurely: true if the last record should have non-final padding
this.seekOpts = seekOpts

// sequence number. -1 is the header, 0 is the first data chunk
this.seq = -1
this.prevChunk = null
Expand Down Expand Up @@ -307,9 +397,28 @@ class ECETransformer {

this.key = await this.generateKey()
this.nonceBase = await this.generateNonceBase()

const startSeq = this.seekOpts.startSeq
if (startSeq != null && startSeq > 0) {
// update the sequence number if decryption doesn't start
// at seq = 0
this.seq += startSeq
}
} else {
let expectEndPadding = false
if (isLast) {
// verify encrypted stream length even when seeking
const endSeq = this.seekOpts.endSeq
if (endSeq != null && endSeq !== this.seq + 1) {
throw new Error('Incorrect encrypted stream length')
}

// if the stream ends prematurely, expect a non-end padding byte
expectEndPadding = !this.seekOpts.endsPrematurely
}

controller.enqueue(
await this.decryptRecord(this.prevChunk, this.seq, isLast)
await this.decryptRecord(this.prevChunk, this.seq, expectEndPadding)
)
}
}
Expand Down
45 changes: 45 additions & 0 deletions lib/extract-transformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-env browser */

/**
* Tranform stream that extracts `length` bytes starting at `offset` and turns
* that result into a new stream. If the input stream ends before `length` bytes,
* the output stream will error.
*
* offset: The number of bytes to skip before starting the output stream
* length: The number of bytes to include in the output stream. All further
* input data will be discarded.
*/

export class ExtractTransformer {
constructor (offset, length) {
// desired range to extract
this.extractStart = offset
this.extractEnd = offset + length // exclusive end

this.offset = 0 // current offset into input stream
}

transform (chunk, controller) {
// The start and end of `chunk` relative to the entire input stream
const chunkStart = this.offset
const chunkEnd = this.offset + chunk.byteLength // exclusive end
this.offset = chunkEnd

// What part of `chunk` belongs in the output stream?
const sliceStart = Math.max(this.extractStart - chunkStart, 0)
const sliceEnd = Math.min(this.extractEnd - chunkStart, chunk.byteLength)

// This chunk is entirely outside the range to extract
if (sliceStart >= chunk.byteLength || sliceEnd <= 0) {
return
}

controller.enqueue(chunk.subarray(sliceStart, sliceEnd))
}

flush (controller) {
if (this.offset < this.extractEnd) {
controller.error(new Error('Stream passed through ExtractTransformer ended early'))
}
}
}
16 changes: 16 additions & 0 deletions lib/keychain.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {
decryptStream,
decryptStreamRange,
encryptStream
} from './ece.js'

Expand Down Expand Up @@ -140,6 +141,21 @@ export class Keychain {
return decryptStream(encryptedStream, mainKey)
}

async decryptStreamRange (offset, length, totalEncryptedLength) {
if (!Number.isInteger(offset)) {
throw new TypeError('offset')
}
if (!Number.isInteger(length)) {
throw new TypeError('length')
}
if (!Number.isInteger(totalEncryptedLength)) {
throw new TypeError('totalEncryptedLength')
}

const mainKey = await this.mainKeyPromise
return decryptStreamRange(mainKey, offset, length, totalEncryptedLength)
}

async encryptMeta (meta) {
if (!(meta instanceof Uint8Array)) {
throw new TypeError('meta')
Expand Down
Loading