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

Complete the Jest DI with crypto #234

Merged
merged 26 commits into from
May 27, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e6311df
Use ipfs-core instead of ipfs for a lighter dependency
matheus23 May 18, 2021
88d6097
Make yarn node --version be 15.x in nix-shell
matheus23 May 18, 2021
21e43ce
Implement the complete crypto DI for node
matheus23 May 18, 2021
dbc3b3d
Fix cbor encode/decode errors on node
matheus23 May 10, 2021
d391663
ESLint fixes
matheus23 May 19, 2021
3cc7931
Merge branch 'main' into matheus23/complete-jest-di
matheus23 May 19, 2021
71a0796
Fix eslint errors in index.ts
matheus23 May 19, 2021
376a3d6
Revert "Fix cbor encode/decode errors on node"
matheus23 May 19, 2021
d08d0ce
Fix type
matheus23 May 19, 2021
b8ffa04
Test cbor en/decoding
matheus23 May 19, 2021
f9b594c
Implement helper for totally in-memory ipfs
matheus23 May 20, 2021
830ae07
Add integration test & fix filesystem running in node
matheus23 May 20, 2021
08d6833
Remove logging
matheus23 May 20, 2021
4ad48db
Use types added to the workaround repo
matheus23 May 20, 2021
feed976
Fix CBOR decoding errors of encrypted data in node
matheus23 May 21, 2021
9409e0e
Switch back to borc
matheus23 May 21, 2021
39a6695
Update jest & puppeteer & base58-universal
matheus23 May 21, 2021
20de6bd
Add jest puppeteer types
matheus23 May 21, 2021
7015861
Make it possible to run multiple ipfs in-memory
matheus23 May 21, 2021
63bdbcc
Actually switch back from cborg to borc
matheus23 May 21, 2021
409f287
Remove unneeded Buffer.from call
matheus23 May 21, 2021
eab6bc8
Make jest properly exit after all tests
matheus23 May 26, 2021
5cbfd03
wtfnode
matheus23 May 26, 2021
c5d97ce
Revert "wtfnode"
matheus23 May 26, 2021
796acae
Force jest to exit, even if ipfs holds resources
matheus23 May 26, 2021
d475e48
Revert "Make jest properly exit after all tests"
matheus23 May 26, 2021
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
2 changes: 1 addition & 1 deletion src/common/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = "0.24.0"
export const VERSION = "0.24.2"
matheus23 marked this conversation as resolved.
Show resolved Hide resolved
305 changes: 288 additions & 17 deletions src/setup/jest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@

import * as browserCrypto from '../crypto/browser'
import crypto from 'crypto'
import * as ed25519 from 'noble-ed25519'
import utils from "keystore-idb/utils"
import { CharSize, Config, CryptoSystem, KeyStore, KeyUse, Msg, PublicKey, SymmKeyLength } from 'keystore-idb/types'
import config from "keystore-idb/config"
import aes from 'keystore-idb/aes'
import rsa from 'keystore-idb/rsa'

import { Storage } from '../../tests/storage/inMemory'
import { setDependencies } from './dependencies'
Expand All @@ -10,40 +15,306 @@ import { setDependencies } from './dependencies'
const webcrypto: Crypto = crypto.webcrypto
globalThis.crypto = webcrypto

export const sha256 = async (bytes: Uint8Array): Promise<Uint8Array> => {


//-------------------------------------
// Crypto node implementations
//-------------------------------------

const encrypt = async (data: Uint8Array, keyStr: string): Promise<Uint8Array> => {
const key = await aes.importKey(keyStr, { length: SymmKeyLength.B256 })
const encrypted = await aes.encryptBytes(data.buffer, key)
return new Uint8Array(encrypted)
}

const decrypt = async (encrypted: Uint8Array, keyStr: string): Promise<Uint8Array> => {
const key = await aes.importKey(keyStr, { length: SymmKeyLength.B256 })
const decryptedBuf = await aes.decryptBytes(encrypted.buffer, key)
return new Uint8Array(decryptedBuf)
}

const genKeyStr = async (): Promise<string> => {
const key = await aes.makeKey({ length: SymmKeyLength.B256 })
return aes.exportKey(key)
}

const decryptGCM = async (encrypted: string, keyStr: string, ivStr: string): Promise<string> => {
const iv = utils.base64ToArrBuf(ivStr)
const sessionKey = await webcrypto.subtle.importKey(
"raw",
utils.base64ToArrBuf(keyStr),
"AES-GCM",
false,
[ "encrypt", "decrypt" ]
)

// Decrypt secrets
const decrypted = await webcrypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
sessionKey,
utils.base64ToArrBuf(encrypted)
)
return utils.arrBufToStr(decrypted, CharSize.B8)
}

const sha256 = async (bytes: Uint8Array): Promise<Uint8Array> => {
const buf = bytes.buffer
const hash = await webcrypto.subtle.digest('SHA-256', buf)
return new Uint8Array(hash)
}

const rsaVerify = (message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): Promise<boolean> => {
const keyStr = utils.arrBufToBase64(publicKey.buffer)
return rsa.verify(message, signature, keyStr)
}

const ed25519Verify = (message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): Promise<boolean> => {
return ed25519.verify(signature, message, publicKey)
}




//-------------------------------------
// Node RSA Keystore
//-------------------------------------

class InMemoryRSAKeyStore implements KeyStore {

store: Record<string, any>;
cfg: Config;

constructor(cfg: Config, store: Record<string, any>) {
this.cfg = cfg
this.store = store
}

static async init(maybeCfg?: Partial<Config>): Promise<InMemoryRSAKeyStore> {
const cfg = config.normalize({
...(maybeCfg || {}),
type: CryptoSystem.RSA
})

const { rsaSize, hashAlg, readKeyName, writeKeyName } = cfg

let store: Record<string, any> = {}

store[readKeyName] = await rsa.makeKeypair(rsaSize, hashAlg, KeyUse.Read);
store[writeKeyName] = await rsa.makeKeypair(rsaSize, hashAlg, KeyUse.Write)

return new InMemoryRSAKeyStore(cfg, store)
}

async readKey() {
return this.store[this.cfg.readKeyName]
}

async writeKey() {
return this.store[this.cfg.writeKeyName]
}

async getSymmKey(keyName: string, cfg?: Partial<Config>): Promise<CryptoKey> {
const mergedCfg = config.merge(this.cfg, cfg)
const maybeKey = this.store[keyName]
if(maybeKey !== null) {
return maybeKey
}
const key = await aes.makeKey(config.symmKeyOpts(mergedCfg))
this.store[keyName] = key
return key
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I duplicate some code here from keystore-idb.

We should probably eventually make it possible to use our dependencies in node. However, doing that for keystore-idb could be questionable, given it has "idb" in its name 😄

Any way, this feels kind of quick-and-dirty right now. Let me know if you have any other ideas.


async keyExists(keyName: string): Promise<boolean> {
const key = this.store[keyName]
return key !== null
}

async deleteKey(keyName: string): Promise<void> {
delete this.store[keyName]
}

async destroy(): Promise<void> {
this.store = {}
}

async importSymmKey(keyStr: string, keyName: string, cfg?: Partial<Config>): Promise<void> {
const mergedCfg = config.merge(this.cfg, cfg)
const key = await aes.importKey(keyStr, config.symmKeyOpts(mergedCfg))
this.store[keyName] = key
}

async exportSymmKey(keyName: string, cfg?: Partial<Config>): Promise<string> {
const key = await this.getSymmKey(keyName, cfg)
return await aes.exportKey(key)
}

async encryptWithSymmKey(msg: string, keyName: string, cfg?: Partial<Config>): Promise<string> {
const mergedCfg = config.merge(this.cfg, cfg)
const key = await this.getSymmKey(keyName, cfg)
const cipherText = await aes.encryptBytes(
utils.strToArrBuf(msg, mergedCfg.charSize),
key,
config.symmKeyOpts(mergedCfg)
)
return utils.arrBufToBase64(cipherText)
}

async decryptWithSymmKey(cipherText: string, keyName: string, cfg?: Partial<Config>): Promise<string> {
const mergedCfg = config.merge(this.cfg, cfg)
const key = await this.getSymmKey(keyName, cfg)
const msgBytes = await aes.decryptBytes(
utils.base64ToArrBuf(cipherText),
key,
config.symmKeyOpts(mergedCfg)
)
return utils.arrBufToStr(msgBytes, mergedCfg.charSize)
}



async sign(msg: Msg, cfg?: Partial<Config>): Promise<string> {
const mergedCfg = config.merge(this.cfg, cfg)
const writeKey = await this.writeKey()

return utils.arrBufToBase64(await rsa.sign(
msg,
writeKey.privateKey,
mergedCfg.charSize
))
}

async verify(
msg: string,
sig: string,
publicKey: string | PublicKey,
cfg?: Partial<Config>
): Promise<boolean> {
const mergedCfg = config.merge(this.cfg, cfg)

return await rsa.verify(
msg,
sig,
publicKey,
mergedCfg.charSize,
mergedCfg.hashAlg
)
}

async encrypt(
msg: Msg,
publicKey: string | PublicKey,
cfg?: Partial<Config>
): Promise<string> {
const mergedCfg = config.merge(this.cfg, cfg)

return utils.arrBufToBase64(await rsa.encrypt(
msg,
publicKey,
mergedCfg.charSize,
mergedCfg.hashAlg
))
}

async decrypt(
cipherText: Msg,
publicKey?: string | PublicKey, // unused param so that keystore interfaces match
cfg?: Partial<Config>
): Promise<string> {
const readKey = await this.readKey()
const mergedCfg = config.merge(this.cfg, cfg)

return utils.arrBufToStr(
await rsa.decrypt(
cipherText,
readKey.privateKey,
),
mergedCfg.charSize
)
}

async publicReadKey(): Promise<string> {
const readKey = await this.readKey()
return rsa.getPublicKey(readKey)
}

async publicWriteKey(): Promise<string> {
const writeKey = await this.writeKey()
return rsa.getPublicKey(writeKey)
}
}


//-------------------------------------
// Dependency Injection Implementation
//-------------------------------------

const getKeystore = (() => {
let keystore: null | InMemoryRSAKeyStore = null;

return async function get() {
if (keystore == null) {
keystore = await InMemoryRSAKeyStore.init()
}
return keystore
}
})()

const inMemoryStorage = new Storage()

export const JEST_IMPLEMENTATION = {
hash: {
sha256: sha256
},
aes: {
encrypt: browserCrypto.encrypt,
decrypt: browserCrypto.decrypt,
genKeyStr: browserCrypto.genKeyStr,
decryptGCM: browserCrypto.decryptGCM,
encrypt: encrypt,
decrypt: decrypt,
genKeyStr: genKeyStr,
decryptGCM: decryptGCM,
},
rsa: {
verify: browserCrypto.rsaVerify
verify: rsaVerify
},
ed25519: {
verify: browserCrypto.ed25519Verify
verify: ed25519Verify
},
keystore: {
publicReadKey: browserCrypto.ksPublicReadKey,
publicWriteKey: browserCrypto.ksPublicWriteKey,
decrypt: browserCrypto.ksDecrypt,
sign: browserCrypto.ksSign,
importSymmKey: browserCrypto.ksImportSymmKey,
exportSymmKey: browserCrypto.ksExportSymmKey,
keyExists: browserCrypto.ksKeyExists,
getAlg: browserCrypto.ksGetAlg,
clear: browserCrypto.ksClear,
async publicReadKey(): Promise<string> {
const ks = await getKeystore()
return ks.publicReadKey()
},
async publicWriteKey(): Promise<string> {
const ks = await getKeystore()
return ks.publicWriteKey()
},
async decrypt(encrypted: string): Promise<string> {
const ks = await getKeystore()
return ks.decrypt(encrypted)
},
async sign(message: string, charSize: number): Promise<string> {
const ks = await getKeystore()
return ks.sign(message, { charSize })
},
async importSymmKey(key: string, name: string): Promise<void> {
const ks = await getKeystore()
return ks.importSymmKey(key, name)
},
async exportSymmKey(name: string): Promise<string> {
const ks = await getKeystore()
return ks.exportSymmKey(name)
},
async keyExists(name:string): Promise<boolean> {
const ks = await getKeystore()
return ks.keyExists(name)
},
async getAlg(): Promise<string> {
const ks = await getKeystore()
return ks.cfg.type
},
async clear(): Promise<void> {
},
},
storage: {
getItem: inMemoryStorage.getItem,
Expand Down