@digitalbazaar/minimal-cipher
Advanced tools
Comparing version
/*! | ||
* Copyright (c) 2019-2020 Digital Bazaar, Inc. All rights reserved. | ||
* Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. | ||
*/ | ||
import crypto from '../crypto.js'; | ||
import {ChaCha20Poly1305, KEY_LENGTH} from '@stablelib/chacha20poly1305'; | ||
import crypto from 'node:crypto'; | ||
import {default as webcrypto} from '../crypto.js'; | ||
@@ -17,3 +17,3 @@ export const JWE_ENC = 'C20P'; | ||
// generate content encryption key | ||
return crypto.getRandomValues(new Uint8Array(KEY_LENGTH)); | ||
return webcrypto.getRandomValues(new Uint8Array(32)); | ||
} | ||
@@ -40,22 +40,3 @@ | ||
} | ||
const cipher = new ChaCha20Poly1305(cek); | ||
// Note: Uses a random value here as a counter is not viable -- multiple | ||
// recipients may be trying to update at the same time and use the same | ||
// counter breaking security; using XChaCha20Poly1305 once available will | ||
// further reduce chances of a collision as it has a 192-bit IV | ||
const iv = crypto.getRandomValues(new Uint8Array(cipher.nonceLength)); | ||
// encrypt data | ||
const encrypted = cipher.seal(iv, data, additionalData); | ||
// split ciphertext and tag | ||
const ciphertext = encrypted.subarray(0, encrypted.length - cipher.tagLength); | ||
const tag = encrypted.subarray(encrypted.length - cipher.tagLength); | ||
return { | ||
ciphertext, | ||
iv, | ||
tag | ||
}; | ||
return _encrypt({data, additionalData, cek}); | ||
} | ||
@@ -91,8 +72,45 @@ | ||
// decrypt `ciphertext` | ||
const cipher = new ChaCha20Poly1305(cek); | ||
const encrypted = new Uint8Array(ciphertext.length + cipher.tagLength); | ||
encrypted.set(ciphertext); | ||
encrypted.set(tag, ciphertext.length); | ||
return cipher.open(iv, encrypted, additionalData); | ||
// decrypt `ciphertext` using node.js native implementation | ||
const decipher = crypto.createDecipheriv( | ||
'chacha20-poly1305', cek, iv, {authTagLength: 16}); | ||
decipher.setAuthTag(tag); | ||
if(additionalData) { | ||
decipher.setAAD(additionalData); | ||
} | ||
const decrypted = decipher.update(ciphertext); | ||
const final = decipher.final(); | ||
return final.length > 0 ? Buffer.concat([decrypted, final]) : decrypted; | ||
} | ||
// internal function exported for reuse by XChaCha20Poly1305 | ||
export async function _encrypt({data, additionalData, cek, iv}) { | ||
// Note: Use of a random value here as a counter is only viable for a | ||
// limited set of messages; using XChaCha20Poly1305 instead | ||
// probabilistically eliminates chances of a collision as it has a 192-bit IV | ||
if(iv === undefined) { | ||
iv = webcrypto.getRandomValues(new Uint8Array(12)); | ||
} | ||
// encrypt `data` using node.js native implementation | ||
const cipher = crypto.createCipheriv( | ||
'chacha20-poly1305', cek, iv, {authTagLength: 16}); | ||
if(additionalData) { | ||
cipher.setAAD(additionalData); | ||
} | ||
const encrypted = cipher.update(data); | ||
const final = cipher.final(); | ||
const ciphertext = final.length > 0 ? | ||
Buffer.concat([encrypted, final]) : encrypted; | ||
const tag = cipher.getAuthTag(); | ||
return {ciphertext, iv, tag}; | ||
} | ||
// internal function exported for reuse by XChaCha20Poly1305 | ||
export function _chacha20({key, nonce, src}) { | ||
// use node.js implementation | ||
const cipher = crypto.createCipheriv('chacha20', key, nonce); | ||
const dst = cipher.update(src); | ||
const final = cipher.final(); | ||
return final.length > 0 ? Buffer.concat([dst, final]) : dst; | ||
} |
/*! | ||
* Copyright (c) 2019-2020 Digital Bazaar, Inc. All rights reserved. | ||
* Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. | ||
*/ | ||
import crypto from '../crypto.js'; | ||
import {XChaCha20Poly1305, KEY_LENGTH} from '@stablelib/xchacha20poly1305'; | ||
import {_encrypt, decrypt as _decrypt, _chacha20} from './c20p.js'; | ||
// constants are based on the string: "expand 32-byte k" | ||
const CHACHA20_CONSTANTS = [ | ||
0x61707865, // "expa" referred to as the "sigma" constant | ||
0x3320646E, // "nd 3" keys used here must be 32-bytes | ||
0x79622D32, // "2-by" | ||
0x6B206574, // "te k" | ||
]; | ||
const LE = true; | ||
const NULL_DATA = new Uint8Array(64); | ||
export const JWE_ENC = 'XC20P'; | ||
@@ -17,3 +27,3 @@ | ||
// generate content encryption key | ||
return crypto.getRandomValues(new Uint8Array(KEY_LENGTH)); | ||
return crypto.getRandomValues(new Uint8Array(32)); | ||
} | ||
@@ -42,21 +52,15 @@ | ||
const cipher = new XChaCha20Poly1305(cek); | ||
// Note: Uses a random value here as a counter is not viable -- multiple | ||
// recipients may be trying to update at the same time and use the same | ||
// counter breaking security; using XChaCha20Poly1305 once available will | ||
// further reduce chances of a collision as it has a 192-bit IV | ||
const iv = crypto.getRandomValues(new Uint8Array(cipher.nonceLength)); | ||
// generate 24-byte (192-bit) XChaCha20Poly1305 IV and use it and `cek` to | ||
// generate subkey and 12-byte (96-bit) IV for use with ChaCha20Poly1305 | ||
const nonce = crypto.getRandomValues(new Uint8Array(24)); | ||
const {subkey, iv} = await _generateSubkey({cek, nonce}); | ||
// encrypt data | ||
const encrypted = cipher.seal(iv, data, additionalData); | ||
// split ciphertext and tag | ||
const ciphertext = encrypted.subarray(0, encrypted.length - cipher.tagLength); | ||
const tag = encrypted.subarray(encrypted.length - cipher.tagLength); | ||
return { | ||
ciphertext, | ||
iv, | ||
tag | ||
}; | ||
// run ChaCha20Poly1305 | ||
const result = await _encrypt({data, additionalData, cek: subkey, iv}); | ||
// return full XChaCha20Poly1305 nonce as IV | ||
result.iv = nonce; | ||
// wipe generated values | ||
subkey.fill(0); | ||
iv.fill(0); | ||
return result; | ||
} | ||
@@ -93,8 +97,77 @@ | ||
// generate subkey and 12-byte (96-bit) IV for use with ChaCha20Poly1305 from | ||
// `cek` and 24-byte (192-bit) XChaCha20Poly1305 IV | ||
const {subkey, iv: newIV} = await _generateSubkey({cek, nonce: iv}); | ||
// decrypt `ciphertext` | ||
const cipher = new XChaCha20Poly1305(cek); | ||
const encrypted = new Uint8Array(ciphertext.length + cipher.tagLength); | ||
encrypted.set(ciphertext); | ||
encrypted.set(tag, ciphertext.length); | ||
return cipher.open(iv, encrypted, additionalData); | ||
const result = await _decrypt( | ||
{ciphertext, iv: newIV, tag, additionalData, cek: subkey}); | ||
// wipe generated values | ||
subkey.fill(0); | ||
newIV.fill(0); | ||
return result; | ||
} | ||
async function _generateSubkey({cek, nonce}) { | ||
// generate subkey and 12-byte IV for ChaCha20Poly1305; first 4 bytes of | ||
// IV are NULL bytes, last 8 are the last 8 bytes of the randomly generated | ||
// 24-byte XChaCha20Poly1305 nonce | ||
const subkey = await _hchacha20({key: cek, nonce: nonce.subarray(0, 16)}); | ||
const iv = new Uint8Array(12); | ||
iv.set(nonce.subarray(16), 4); | ||
return {subkey, iv}; | ||
} | ||
async function _hchacha20({key, nonce}) { | ||
/* HChaCha20's output is the first 16 bytes of internal state and last 16 | ||
bytes of ChaCha20 internal state after running its usual rounds. See: | ||
https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.2 | ||
ChaCha20's output is each 4 bytes of internal state (interpreted as a LE | ||
uint32) after running the usual rounds added to the initial internal state | ||
(again, with each 4 bytes interpreted as a LE uint32). | ||
Therefore, we can implement HChaCha20 by: | ||
1. Creating the ChaCha20 initial state by concatenating the ChaCha20 | ||
constants with the 32-byte key and the 16-byte nonce. | ||
2. Running ChaCha20 with the 32-byte key, 16-byte nonce, and zero-filled | ||
64-byte data to get 64 byte output. The zero-filled data makes the | ||
internal XOR operations no-ops. | ||
3. Read the first 16 bytes and last 16 bytes of ChaCha20 output as LE uint32s | ||
and subtract each corresponding LE uint32 from the initial state. | ||
4. Concatenate the resulting 32 bytes to produce the HChaCha20 output. */ | ||
// create initial ChaCha20 state as 16 LE uint32s (no need to convert to | ||
// 64 bytes as the numbers will be used directly below) | ||
const state = new Array(16); | ||
for(let i = 0; i < 4; ++i) { | ||
state[i] = CHACHA20_CONSTANTS[i]; | ||
} | ||
const dvKey = new DataView(key.buffer, key.byteOffset, key.length); | ||
for(let i = 0; i < 8; ++i) { | ||
state[i + 4] = dvKey.getUint32(i * 4, LE); | ||
} | ||
const dvNonce = new DataView(nonce.buffer, nonce.byteOffset, nonce.length); | ||
for(let i = 0; i < 4; ++i) { | ||
state[i + 12] = dvNonce.getUint32(i * 4, LE); | ||
} | ||
// run ChaCha20 | ||
const dst = await _chacha20({key, nonce, src: NULL_DATA}); | ||
// generate HChaCha20 output | ||
const out = new Uint8Array(32); | ||
const dvOut = new DataView(out.buffer, out.byteOffset, out.length); | ||
const dvDst = new DataView(dst.buffer, dst.byteOffset, dst.length); | ||
for(let i = 0; i < 4; ++i) { | ||
dvOut.setUint32(i * 4, (state[i] - dvDst.getUint32(i * 4, LE)) | 0); | ||
} | ||
for(let i = 0; i < 4; ++i) { | ||
dvOut.setUint32( | ||
i * 4 + 16, (state[i + 12] - dvDst.getUint32(i * 4 + 48, LE)) | 0); | ||
} | ||
return out; | ||
} |
{ | ||
"name": "@digitalbazaar/minimal-cipher", | ||
"version": "5.0.0", | ||
"version": "5.1.0", | ||
"description": "Minimal encryption/decryption JWE/CWE library.", | ||
@@ -9,2 +9,3 @@ "license": "BSD-3-Clause", | ||
"browser": { | ||
"./lib/algorithms/c20p.js": "./lib/algorithms/c20p-browser.js", | ||
"./lib/algorithms/x25519-helper.js": "./lib/algorithms/x25519-helper-browser.js", | ||
@@ -26,4 +27,4 @@ "./lib/crypto.js": "./lib/crypto-browser.js" | ||
"dependencies": { | ||
"@stablelib/chacha": "^1.0.1", | ||
"@stablelib/chacha20poly1305": "^1.0.1", | ||
"@stablelib/xchacha20poly1305": "^1.0.1", | ||
"base58-universal": "^2.0.0", | ||
@@ -30,0 +31,0 @@ "base64url-universal": "^2.0.0", |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
62075
14%21
5%1356
15.8%0
-100%+ Added
- Removed
- Removed
- Removed