177 lines
6.9 KiB
JavaScript
177 lines
6.9 KiB
JavaScript
// Copyright (C) 2016 Dmitry Chestnykh
|
|
// MIT License. See LICENSE file for details.
|
|
import { streamXOR, stream } from "@stablelib/chacha";
|
|
import { Poly1305 } from "@stablelib/poly1305";
|
|
import { wipe } from "@stablelib/wipe";
|
|
import { writeUint64LE } from "@stablelib/binary";
|
|
import { equal } from "@stablelib/constant-time";
|
|
export const KEY_LENGTH = 32;
|
|
export const NONCE_LENGTH = 12;
|
|
export const TAG_LENGTH = 16;
|
|
const ZEROS = new Uint8Array(16);
|
|
/**
|
|
* ChaCha20-Poly1305 Authenticated Encryption with Associated Data.
|
|
*
|
|
* Defined in RFC7539.
|
|
*/
|
|
export class ChaCha20Poly1305 {
|
|
nonceLength = NONCE_LENGTH;
|
|
tagLength = TAG_LENGTH;
|
|
_key;
|
|
/**
|
|
* Creates a new instance with the given 32-byte key.
|
|
*/
|
|
constructor(key) {
|
|
if (key.length !== KEY_LENGTH) {
|
|
throw new Error("ChaCha20Poly1305 needs 32-byte key");
|
|
}
|
|
// Copy key.
|
|
this._key = new Uint8Array(key);
|
|
}
|
|
/**
|
|
* Encrypts and authenticates plaintext, authenticates associated data,
|
|
* and returns sealed ciphertext, which includes authentication tag.
|
|
*
|
|
* RFC7539 specifies 12 bytes for nonce. It may be this 12-byte nonce
|
|
* ("IV"), or full 16-byte counter (called "32-bit fixed-common part")
|
|
* and nonce.
|
|
*
|
|
* If dst is given (it must be the size of plaintext + the size of tag
|
|
* length) the result will be put into it. Dst and plaintext must not
|
|
* overlap.
|
|
*/
|
|
seal(nonce, plaintext, associatedData, dst) {
|
|
if (nonce.length > 16) {
|
|
throw new Error("ChaCha20Poly1305: incorrect nonce length");
|
|
}
|
|
// Allocate space for counter, and set nonce as last bytes of it.
|
|
const counter = new Uint8Array(16);
|
|
counter.set(nonce, counter.length - nonce.length);
|
|
// Generate authentication key by taking first 32-bytes of stream.
|
|
// We pass full counter, which has 12-byte nonce and 4-byte block counter,
|
|
// and it will get incremented after generating the block, which is
|
|
// exactly what we need: we only use the first 32 bytes of 64-byte
|
|
// ChaCha block and discard the next 32 bytes.
|
|
const authKey = new Uint8Array(32);
|
|
stream(this._key, counter, authKey, 4);
|
|
// Allocate space for sealed ciphertext.
|
|
const resultLength = plaintext.length + this.tagLength;
|
|
let result;
|
|
if (dst) {
|
|
if (dst.length !== resultLength) {
|
|
throw new Error("ChaCha20Poly1305: incorrect destination length");
|
|
}
|
|
result = dst;
|
|
}
|
|
else {
|
|
result = new Uint8Array(resultLength);
|
|
}
|
|
// Encrypt plaintext.
|
|
streamXOR(this._key, counter, plaintext, result, 4);
|
|
// Authenticate.
|
|
// XXX: can "simplify" here: pass full result (which is already padded
|
|
// due to zeroes prepared for tag), and ciphertext length instead of
|
|
// subarray of result.
|
|
this._authenticate(result.subarray(result.length - this.tagLength, result.length), authKey, result.subarray(0, result.length - this.tagLength), associatedData);
|
|
// Cleanup.
|
|
wipe(counter);
|
|
return result;
|
|
}
|
|
/**
|
|
* Authenticates sealed ciphertext (which includes authentication tag) and
|
|
* associated data, decrypts ciphertext and returns decrypted plaintext.
|
|
*
|
|
* RFC7539 specifies 12 bytes for nonce. It may be this 12-byte nonce
|
|
* ("IV"), or full 16-byte counter (called "32-bit fixed-common part")
|
|
* and nonce.
|
|
*
|
|
* If authentication fails, it returns null.
|
|
*
|
|
* If dst is given (it must be of ciphertext length minus tag length),
|
|
* the result will be put into it. Dst and plaintext must not overlap.
|
|
*/
|
|
open(nonce, sealed, associatedData, dst) {
|
|
if (nonce.length > 16) {
|
|
throw new Error("ChaCha20Poly1305: incorrect nonce length");
|
|
}
|
|
// Sealed ciphertext should at least contain tag.
|
|
if (sealed.length < this.tagLength) {
|
|
// TODO(dchest): should we throw here instead?
|
|
return null;
|
|
}
|
|
// Allocate space for counter, and set nonce as last bytes of it.
|
|
const counter = new Uint8Array(16);
|
|
counter.set(nonce, counter.length - nonce.length);
|
|
// Generate authentication key by taking first 32-bytes of stream.
|
|
const authKey = new Uint8Array(32);
|
|
stream(this._key, counter, authKey, 4);
|
|
// Authenticate.
|
|
// XXX: can simplify and avoid allocation: since authenticate()
|
|
// already allocates tag (from Poly1305.digest(), it can return)
|
|
// it instead of copying to calculatedTag. But then in seal()
|
|
// we'll need to copy it.
|
|
const calculatedTag = new Uint8Array(this.tagLength);
|
|
this._authenticate(calculatedTag, authKey, sealed.subarray(0, sealed.length - this.tagLength), associatedData);
|
|
// Constant-time compare tags and return null if they differ.
|
|
if (!equal(calculatedTag, sealed.subarray(sealed.length - this.tagLength, sealed.length))) {
|
|
return null;
|
|
}
|
|
// Allocate space for decrypted plaintext.
|
|
const resultLength = sealed.length - this.tagLength;
|
|
let result;
|
|
if (dst) {
|
|
if (dst.length !== resultLength) {
|
|
throw new Error("ChaCha20Poly1305: incorrect destination length");
|
|
}
|
|
result = dst;
|
|
}
|
|
else {
|
|
result = new Uint8Array(resultLength);
|
|
}
|
|
// Decrypt.
|
|
streamXOR(this._key, counter, sealed.subarray(0, sealed.length - this.tagLength), result, 4);
|
|
// Cleanup.
|
|
wipe(counter);
|
|
return result;
|
|
}
|
|
clean() {
|
|
wipe(this._key);
|
|
return this;
|
|
}
|
|
_authenticate(tagOut, authKey, ciphertext, associatedData) {
|
|
// Initialize Poly1305 with authKey.
|
|
const h = new Poly1305(authKey);
|
|
// Authenticate padded associated data.
|
|
if (associatedData) {
|
|
h.update(associatedData);
|
|
if (associatedData.length % 16 > 0) {
|
|
h.update(ZEROS.subarray(associatedData.length % 16));
|
|
}
|
|
}
|
|
// Authenticate padded ciphertext.
|
|
h.update(ciphertext);
|
|
if (ciphertext.length % 16 > 0) {
|
|
h.update(ZEROS.subarray(ciphertext.length % 16));
|
|
}
|
|
// Authenticate length of associated data.
|
|
// XXX: can avoid allocation here?
|
|
const length = new Uint8Array(8);
|
|
if (associatedData) {
|
|
writeUint64LE(associatedData.length, length);
|
|
}
|
|
h.update(length);
|
|
// Authenticate length of ciphertext.
|
|
writeUint64LE(ciphertext.length, length);
|
|
h.update(length);
|
|
// Get tag and copy it into tagOut.
|
|
const tag = h.digest();
|
|
for (let i = 0; i < tag.length; i++) {
|
|
tagOut[i] = tag[i];
|
|
}
|
|
// Cleanup.
|
|
h.clean();
|
|
wipe(tag);
|
|
wipe(length);
|
|
}
|
|
}
|
|
//# sourceMappingURL=chacha20poly1305.js.map
|