Skip to content

Commit

Permalink
add some crypto features
Browse files Browse the repository at this point in the history
  • Loading branch information
dmonad committed Feb 6, 2023
1 parent b618e73 commit 3b66a0c
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 3 deletions.
103 changes: 103 additions & 0 deletions crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/* eslint-env browser */

import * as encoding from './encoding.js'
import * as decoding from './decoding.js'
import * as string from './string.js'
import webcrypto from 'lib0/webcrypto'

/**
* @param {string | Uint8Array} data
* @return {Uint8Array}
*/
const toBinary = data => typeof data === 'string' ? string.encodeUtf8(data) : data

/**
* @experimental The API is not final!
*
* Derive an symmetric key using the Password-Based-Key-Derivation-Function-2.
*
* @param {string | Uint8Array} secret
* @param {string | Uint8Array} salt
* @param {Object} options
* @param {boolean} [options.extractable]
* @return {PromiseLike<CryptoKey>}
*/
export const deriveSymmetricKey = (secret, salt, { extractable = false } = {}) => {
const binSecret = toBinary(secret)
const binSalt = toBinary(salt)
return webcrypto.subtle.importKey(
'raw',
binSecret,
'PBKDF2',
false,
['deriveKey']
).then(keyMaterial =>
webcrypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: binSalt, // NIST recommends at least 64 bits
iterations: 600000, // OWASP recommends 600k iterations
hash: 'SHA-256'
},
keyMaterial,
{
name: 'AES-GCM',
length: 256
},
extractable,
['encrypt', 'decrypt']
)
)
}

/**
* @experimental The API is not final!
*
* Encrypt some data using AES-GCM method.
*
* @param {Uint8Array} data data to be encrypted
* @param {CryptoKey} key
* @return {PromiseLike<Uint8Array>} encrypted, base64 encoded message
*/
export const encrypt = (data, key) => {
const iv = webcrypto.getRandomValues(new Uint8Array(16)) // 92bit is enough. 128bit is recommended if space is not an issue.
return webcrypto.subtle.encrypt(
{
name: 'AES-GCM',
iv
},
key,
data
).then(cipher => {
const encryptedDataEncoder = encoding.createEncoder()
// iv may be sent in the clear to the other peers
encoding.writeUint8Array(encryptedDataEncoder, iv)
encoding.writeVarUint8Array(encryptedDataEncoder, new Uint8Array(cipher))
return encoding.toUint8Array(encryptedDataEncoder)
})
}

/**
* @experimental The API is not final!
*
* Decrypt some data using AES-GCM method.
*
* @param {Uint8Array} data
* @param {CryptoKey} key
* @return {PromiseLike<Uint8Array>} decrypted buffer
*/
export const decrypt = (data, key) => {
const dataDecoder = decoding.createDecoder(data)
const iv = decoding.readUint8Array(dataDecoder, 16)
const cipher = decoding.readVarUint8Array(dataDecoder)
return webcrypto.subtle.decrypt(
{
name: 'AES-GCM',
iv
},
key,
cipher
).then(data => new Uint8Array(data))
}

export const exportKey = webcrypto.subtle.exportKey.bind(webcrypto.subtle)
55 changes: 55 additions & 0 deletions crypto.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as cryptutils from 'lib0/crypto'
import * as t from './testing.js'
import * as prng from './prng.js'

/**
* @param {t.TestCase} tc
*/
export const testReapeatEncryption = async tc => {
const secret = prng.word(tc.prng)
const salt = prng.word(tc.prng)
const data = prng.uint8Array(tc.prng, 1000000)

/**
* @type {any}
*/
let encrypted
/**
* @type {any}
*/
let decrypted
/**
* @type {any}
*/
let key
await t.measureTimeAsync('Key generation', async () => {
key = await cryptutils.deriveSymmetricKey(secret, salt)
})
await t.measureTimeAsync('Encryption', async () => {
encrypted = await cryptutils.encrypt(data, key)
})
t.info(`Byte length: ${data.byteLength}b`)
t.info(`Encrypted length: ${encrypted.length}b`)
await t.measureTimeAsync('Decryption', async () => {
decrypted = await cryptutils.decrypt(encrypted, key)
})
t.compare(data, decrypted)
}

/**
* @param {t.TestCase} _tc
*/
export const testConsistentKeyGeneration = async _tc => {
const secret = 'qfycncpxhjktawlqkhc'
const salt = 'my nonce'
const expectedJwk = {
key_ops: ['encrypt', 'decrypt'],
ext: true,
kty: 'oct',
k: 'psAqoMh9apefdr8y1tdbNMVTLxb-tFekEFipYIOX5n8',
alg: 'A256GCM'
}
const key = await cryptutils.deriveSymmetricKey(secret, salt, { extractable: true })
const jwk = await cryptutils.exportKey('jwk', key)
t.compare(jwk, expectedJwk)
}
18 changes: 18 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@
"import": "./condititons.js",
"require": "./dist/conditions.cjs"
},
"./crypto": {
"types": "./crypto.d.ts",
"module": "./crypto.js",
"import": "./crypto.js",
"require": "./dist/crypto.cjs"
},
"./decoding.js": "./decoding.js",
"./dist/decoding.cjs": "./dist/decoding.cjs",
"./decoding": {
Expand Down Expand Up @@ -370,6 +376,18 @@
"module": "./websocket.js",
"import": "./websocket.js",
"require": "./dist/websocket.cjs"
},
"./webcrypto": {
"types": "./webcrypto.browser.d.ts",
"node": {
"import": "./webcrypto.node.js",
"require": "./dist/webcrypto.node.cjs"
},
"browser": {
"import": "./webcrypto.browser.js",
"require": "./dist/webcrypto.browser.cjs"
},
"module": "./webcrypto.browser.js"
}
},
"dependencies": {
Expand Down
2 changes: 2 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { runTests } from './testing.js'
import * as array from './array.test.js'
import * as broadcastchannel from './broadcastchannel.test.js'
import * as crypto from './crypto.test.js'
import * as logging from './logging.test.js'
import * as string from './string.test.js'
import * as encoding from './encoding.test.js'
Expand Down Expand Up @@ -41,6 +42,7 @@ if (isBrowser) {
runTests({
array,
broadcastchannel,
crypto,
logging,
string,
encoding,
Expand Down
10 changes: 7 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es2018",
"lib": ["es2018", "dom"], /* Specify library files to be included in the compilation. */
"target": "es2022",
"lib": ["es2022", "dom"], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */
"checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
Expand All @@ -16,7 +16,11 @@
"strict": true,
"noImplicitAny": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"paths": {
"lib0/*": ["./*"],
"lib0/webcrypto": ["./webcrypto.browser.js"]
}
},
"include": ["./*.js"],
"exclude": ["./dist"]
Expand Down
3 changes: 3 additions & 0 deletions webcrypto.browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-env browser */

export default crypto
4 changes: 4 additions & 0 deletions webcrypto.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

import { webcrypto } from 'node:crypto'

export default webcrypto

0 comments on commit 3b66a0c

Please sign in to comment.