Skip to content
Open
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
48 changes: 48 additions & 0 deletions job-results/reader-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,54 @@ describe('Encryption Library Tests', async () => {

expect(new TextDecoder().decode(entries[0].contents)).toEqual('hello world!')
})

it('throws if CryptoKey was imported without unwrapKey usage', async () => {
const publicKey = pemToArrayBuffer(readPublicKey())
const fingerprint = await fingerprintKeyData(publicKey)
const writer = new ResultsWriter([{ publicKey, fingerprint }])
await writer.addFile('test.data', toArrayBuffer('hello world!'))
const zip = await writer.generate()

const wrongKey = await crypto.subtle.importKey(
'pkcs8',
pemToArrayBuffer(readPrivateKey()),
{ name: 'RSA-OAEP', hash: 'SHA-256' },
false,
['decrypt'],
)

expect(() => new ResultsReader(zip, wrongKey, fingerprint)).toThrow(/unwrapKey/)
})

it('can read using a non-extractable CryptoKey', async () => {
const publicKey = pemToArrayBuffer(readPublicKey())
const fingerprint = await fingerprintKeyData(publicKey)
const writer = new ResultsWriter([{ publicKey, fingerprint }])

const content = Buffer.from('hello world!', 'utf-8')
await writer.addFile(
'test.data',
content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength),
)

const zip = await writer.generate()

const privateKeyBuf = pemToArrayBuffer(readPrivateKey())
const privateCryptoKey = await crypto.subtle.importKey(
'pkcs8',
privateKeyBuf,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
false,
['unwrapKey'],
)
expect(privateCryptoKey.extractable).toBe(false)

const reader = new ResultsReader(zip, privateCryptoKey, fingerprint)
const entries = await reader.extractFiles()

expect(entries).toHaveLength(1)
expect(new TextDecoder().decode(entries[0].contents)).toEqual('hello world!')
})
})

describe('listFiles and extractFile', async () => {
Expand Down
51 changes: 41 additions & 10 deletions job-results/reader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BlobReader, BlobWriter, TextWriter, ZipReader } from '@zip.js/zip.js'
import type { FileEntry as ZipFileEntry } from '@zip.js/zip.js'
import type { ResultsFile, ResultsManifest, FileEntry, FileInfo } from './types'
import { privateKeyFromBuffer } from '../util'
import { privateKeyFromBufferForUnwrap } from '../util'
import logger from '../lib/logger'

export class ResultsReader {
Expand All @@ -11,15 +11,46 @@ export class ResultsReader {

private zipReader: ZipReader<Blob>
private fingerprint: string
private privateKey: ArrayBuffer
private privateKey: ArrayBuffer | CryptoKey
private importedKey?: CryptoKey
private decoded = false

constructor(zipBlob: Blob, privateKey: ArrayBuffer, fingerprint: string) {
/**
* Construct a reader from a pre-imported `CryptoKey`.
*
* The `CryptoKey` must have been imported with `keyUsages: ['unwrapKey']`
* (RSA-OAEP, SHA-256). It may be (and is recommended to be) non-extractable,
* so raw key bytes never reside in JS memory.
*
* @param fingerprint SHA-256 hex of the SPKI public key. Required — it
* cannot be derived from a non-extractable key.
*/
constructor(zipBlob: Blob, privateKey: CryptoKey, fingerprint: string)
/**
* @deprecated Pass a `CryptoKey` instead so raw key bytes do not have to
* live in JS memory. Import as:
* `crypto.subtle.importKey('pkcs8', bytes, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, ['unwrapKey'])`.
*/
constructor(zipBlob: Blob, privateKey: ArrayBuffer, fingerprint: string)
constructor(zipBlob: Blob, privateKey: ArrayBuffer | CryptoKey, fingerprint: string) {
if (privateKey instanceof CryptoKey && !privateKey.usages.includes('unwrapKey')) {
throw new Error(
`ResultsReader: CryptoKey must be imported with usages: ['unwrapKey'] (got: ${JSON.stringify(privateKey.usages)})`,
)
}
this.zipReader = new ZipReader(new BlobReader(zipBlob))
this.fingerprint = fingerprint
this.privateKey = privateKey
}

private async getPrivateKey(): Promise<CryptoKey> {
if (this.privateKey instanceof CryptoKey) return this.privateKey
if (!this.importedKey) {
this.importedKey = await privateKeyFromBufferForUnwrap(this.privateKey)
}
return this.importedKey
}

async extractFiles() {
logger.info(`Extracting files`)

Expand Down Expand Up @@ -113,16 +144,16 @@ export class ResultsReader {

const encryptedKey = Buffer.from(encryptedKeyBase64, 'base64')

const rawKey = await crypto.subtle.decrypt(
{
name: 'RSA-OAEP',
},
await privateKeyFromBuffer(this.privateKey),
const key = await crypto.subtle.unwrapKey(
'raw',
encryptedKey,
await this.getPrivateKey(),
{ name: 'RSA-OAEP' },
{ name: 'AES-CBC', length: 256 },
false,
['decrypt'],
)

const key = await crypto.subtle.importKey('raw', rawKey, { name: 'AES-CBC' }, false, ['decrypt'])

logger.info(`Finished decrypting key`)

return key
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "si-encryption",
"version": "0.0.1",
"version": "0.1.0",
"description": "Enryption in use at SafeInsights",
"main": "index.js",
"type": "module",
Expand Down
6 changes: 6 additions & 0 deletions util/keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ export async function privateKeyFromBuffer(privateKeyBuffer: ArrayBuffer): Promi
return key
}

export async function privateKeyFromBufferForUnwrap(privateKeyBuffer: ArrayBuffer): Promise<CryptoKey> {
return crypto.subtle.importKey('pkcs8', privateKeyBuffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, [
'unwrapKey',
])
}

export async function fingerprintPublicKeyFromPrivateKey(privateKey: CryptoKey) {
logger.info(`Creating fingerprint from private key`)
// Export the private key as a JWK (JSON Web Key)
Expand Down
Loading