Merge pull request #691 from twiss/draft04

Implement RFC4880bis-04
This commit is contained in:
Sanjana Rajan 2018-04-30 12:16:20 -07:00 committed by GitHub
commit d562c147f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2356 additions and 340 deletions

View File

@ -229,8 +229,8 @@ module.exports = function(grunt) {
public: "public",
maxRetries: 3,
throttled: 2,
pollInterval: 4000,
'max-duration': 360,
pollInterval: 10000,
sauceConfig: {maxDuration: 1800, commandTimeout: 600, idleTimeout: 1000},
statusCheckAttempts: 200
}
}

View File

@ -54,17 +54,32 @@ OpenPGP.js [![Build Status](https://travis-ci.org/openpgpjs/openpgpjs.svg?branch
| p384 | ECDH | ECDSA | Yes | Yes* | Yes* |
| p521 | ECDH | ECDSA | Yes | Yes* | Yes* |
| secp256k1 | ECDH | ECDSA | Yes | Yes* | No |
| curve25519 | ECDH | N/A | Yes | No (TODO) | No |
| ed25519 | N/A | EdDSA | Yes | No (TODO) | No |
| brainpoolP256r1 | ECDH | ECDSA | Yes | No (TODO) | No |
| brainpoolP384r1 | ECDH | ECDSA | Yes | No (TODO) | No |
| brainpoolP512r1 | ECDH | ECDSA | Yes | No (TODO) | No |
| brainpoolP256r1 | ECDH | ECDSA | Yes | Yes* | No |
| brainpoolP384r1 | ECDH | ECDSA | Yes | Yes* | No |
| brainpoolP512r1 | ECDH | ECDSA | Yes | Yes* | No |
| curve25519 | ECDH | N/A | Yes | No | No |
| ed25519 | N/A | EdDSA | Yes | No | No |
* Version 2.x of the library has been built from the ground up with Uint8Arrays. This allows for much better performance and memory usage than strings.
* If the user's browser supports [native WebCrypto](https://caniuse.com/#feat=cryptography) via the `window.crypto.subtle` API, this will be used. Under Node.js the native [crypto module](https://nodejs.org/API/crypto.html#crypto_crypto) is used. This can be deactivated by setting `openpgp.config.use_native = false`.
* The library implements the [IETF proposal](https://tools.ietf.org/html/draft-ford-openpgp-format-00) for authenticated encryption [using native AES-GCM](https://github.com/openpgpjs/openpgpjs/pull/430). This makes symmetric encryption about 30x faster on supported platforms. Since the specification has not been finalized and other OpenPGP implementations haven't adopted it yet, the feature is currently behind a flag. You can activate it by setting `openpgp.config.aead_protect = true`. **Note: activating this setting can break compatibility with other OpenPGP implementations, so be careful if that's one of your requirements.**
* The library implements the [IETF proposal](https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04) for authenticated encryption using native AES-EAX, OCB, or GCM. This makes symmetric encryption up to 30x faster on supported platforms. Since the specification has not been finalized and other OpenPGP implementations haven't adopted it yet, the feature is currently behind a flag. **Note: activating this setting can break compatibility with other OpenPGP implementations, and also with future versions of OpenPGP.js. Don't use it with messages you want to store on disk or in a database.** You can enable it by setting:
```
openpgp.config.aead_protect = true
openpgp.config.aead_protect_version = 4
```
You can change the AEAD mode by setting one of the following options:
```
openpgp.config.aead_mode = openpgp.enums.aead.eax // Default, native
openpgp.config.aead_mode = openpgp.enums.aead.ocb // Non-native
openpgp.config.aead_mode = openpgp.enums.aead.experimental_gcm // **Non-standard**, fastest
```
We previously also implemented an [earlier version](https://tools.ietf.org/html/draft-ford-openpgp-format-00) of the draft (using GCM), which you could enable by simply setting `openpgp.config.aead_protect = true`. If you need to stay compatible with that version, don't set `openpgp.config.aead_protect_version = 4`.
* For environments that don't provide native crypto, the library falls back to [asm.js](https://caniuse.com/#feat=asmjs) implementations of AES, SHA-1, and SHA-256. We use [Rusha](https://github.com/srijs/rusha) and [asmCrypto Lite](https://github.com/openpgpjs/asmcrypto-lite) (a minimal subset of asmCrypto.js built specifically for OpenPGP.js).
@ -92,8 +107,6 @@ Here are some examples of how to use the v2.x+ API. For more elaborate examples
var openpgp = require('openpgp'); // use as CommonJS, AMD, ES6 module or via window.openpgp
openpgp.initWorker({ path:'openpgp.worker.js' }) // set the relative web worker path
openpgp.config.aead_protect = true // activate fast AES-GCM mode (not yet OpenPGP standard)
```
#### Encrypt and decrypt *Uint8Array* data with a password

View File

@ -19,6 +19,7 @@
* @requires config
* @requires encoding/armor
* @requires enums
* @requires util
* @requires packet
* @requires signature
* @module cleartext
@ -27,6 +28,7 @@
import config from './config';
import armor from './encoding/armor';
import enums from './enums';
import util from './util';
import packet from './packet';
import { Signature } from './signature';
import { createVerificationObjects, createSignaturePackets } from './message';
@ -43,7 +45,7 @@ export function CleartextMessage(text, signature) {
return new CleartextMessage(text, signature);
}
// normalize EOL to canonical form <CR><LF>
this.text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n/g, "\r\n");
this.text = util.canonicalizeEOL(util.removeTrailingSpaces(text));
if (signature && !(signature instanceof Signature)) {
throw new Error('Invalid signature input');
}
@ -122,7 +124,7 @@ CleartextMessage.prototype.verifyDetached = function(signature, keys, date=new D
*/
CleartextMessage.prototype.getText = function() {
// normalize end of line to \n
return this.text.replace(/\r\n/g, "\n");
return util.nativeEOL(this.text);
};
/**

View File

@ -51,6 +51,38 @@ export default {
* @property {Boolean} aead_protect
*/
aead_protect: false,
/**
* Use Authenticated Encryption with Additional Data (AEAD) protection for symmetric encryption.
* 0 means we implement a variant of {@link https://tools.ietf.org/html/draft-ford-openpgp-format-00|this IETF draft}.
* 4 means we implement {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04|RFC4880bis-04}.
* Only has an effect when aead_protect is set to true.
* @memberof module:config
* @property {Integer} aead_protect_version
*/
aead_protect_version: 4,
/**
* Default Authenticated Encryption with Additional Data (AEAD) encryption mode
* Only has an effect when aead_protect is set to true.
* **FUTURE OPENPGP.JS VERSIONS MAY BREAK COMPATIBILITY WHEN USING THIS OPTION**
* @memberof module:config
* @property {Integer} aead_mode Default AEAD mode {@link module:enums.aead}
*/
aead_mode: enums.aead.eax,
/**
* Chunk Size Byte for Authenticated Encryption with Additional Data (AEAD) mode
* Only has an effect when aead_protect is set to true.
* Must be an integer value from 0 to 56.
* @memberof module:config
* @property {Integer} aead_chunk_size_byte
*/
aead_chunk_size_byte: 12,
/**
* {@link https://tools.ietf.org/html/rfc4880#section-3.7.1.3|RFC4880 3.7.1.3}:
* Iteration Count Byte for S2K (String to Key)
* @memberof module:config
* @property {Integer} s2k_iteration_count_byte
*/
s2k_iteration_count_byte: 96,
/** Use integrity protection for symmetric encryption
* @memberof module:config
* @property {Boolean} integrity_protect

View File

@ -2,21 +2,20 @@
* @requires asmcrypto.js
*/
import { AES_ECB } from 'asmcrypto.js/src/aes/ecb/exports';
import { _AES_asm_instance, _AES_heap_instance } from 'asmcrypto.js/src/aes/exports';
import { AES_ECB } from 'asmcrypto.js/src/aes/ecb/ecb';
// TODO use webCrypto or nodeCrypto when possible.
function aes(length) {
const c = function(key) {
this.key = Uint8Array.from(key);
const aes_ecb = new AES_ECB(key, _AES_heap_instance, _AES_asm_instance);
this.encrypt = function(block) {
block = Uint8Array.from(block);
return Array.from(AES_ECB.encrypt(block, this.key, false));
return aes_ecb.encrypt(block).result;
};
this.decrypt = function(block) {
block = Uint8Array.from(block);
return Array.from(AES_ECB.decrypt(block, this.key, false));
return aes_ecb.decrypt(block).result;
};
};

98
src/crypto/cmac.js Normal file
View File

@ -0,0 +1,98 @@
/**
* @fileoverview This module implements AES-CMAC on top of
* native AES-CBC using either the WebCrypto API or Node.js' crypto API.
* @requires asmcrypto.js
* @requires util
* @module crypto/cmac
*/
import { AES_CBC } from 'asmcrypto.js/src/aes/cbc/exports';
import util from '../util';
const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
const Buffer = util.getNodeBuffer();
/**
* This implementation of CMAC is based on the description of OMAC in
* http://web.cs.ucdavis.edu/~rogaway/papers/eax.pdf. As per that
* document:
*
* We have made a small modification to the OMAC algorithm as it was
* originally presented, changing one of its two constants.
* Specifically, the constant 4 at line 85 was the constant 1/2 (the
* multiplicative inverse of 2) in the original definition of OMAC [14].
* The OMAC authors indicate that they will promulgate this modification
* [15], which slightly simplifies implementations.
*/
const blockLength = 16;
/**
* xor `padding` into the end of `data`. This function implements "the
* operation xor [which] xors the shorter string into the end of longer
* one". Since data is always as least as long as padding, we can
* simplify the implementation.
* @param {Uint8Array} data
* @param {Uint8Array} padding
*/
function rightXorMut(data, padding) {
const offset = data.length - blockLength;
for (let i = 0; i < blockLength; i++) {
data[i + offset] ^= padding[i];
}
return data;
}
function pad(data, padding, padding2) {
// if |M| in {n, 2n, 3n, ...}
if (data.length % blockLength === 0) {
// then return M xor→ B,
return rightXorMut(data, padding);
}
// else return (M || 10^(n1(|M| mod n))) xor→ P
const padded = new Uint8Array(data.length + (blockLength - data.length % blockLength));
padded.set(data);
padded[data.length] = 0b10000000;
return rightXorMut(padded, padding2);
}
const zeroBlock = new Uint8Array(blockLength);
export default async function CMAC(key) {
const cbc = await CBC(key);
// L ← E_K(0^n); B ← 2L; P ← 4L
const padding = util.double(await cbc(zeroBlock));
const padding2 = util.double(padding);
return async function(data) {
// return CBC_K(pad(M; B, P))
return (await cbc(pad(data, padding, padding2))).subarray(-blockLength);
};
}
async function CBC(key) {
if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
key = await webCrypto.importKey('raw', key, { name: 'AES-CBC', length: key.length * 8 }, false, ['encrypt']);
return async function(pt) {
const ct = await webCrypto.encrypt({ name: 'AES-CBC', iv: zeroBlock, length: blockLength * 8 }, key, pt);
return new Uint8Array(ct).subarray(0, ct.byteLength - blockLength);
};
}
if (util.getNodeCrypto()) { // Node crypto library
key = new Buffer(key);
return async function(pt) {
pt = new Buffer(pt);
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-cbc', key, zeroBlock);
const ct = en.update(pt);
return new Uint8Array(ct);
};
}
// asm.js fallback
return async function(pt) {
return AES_CBC.encrypt(pt, key, false, zeroBlock);
};
}

168
src/crypto/eax.js Normal file
View File

@ -0,0 +1,168 @@
// OpenPGP.js - An OpenPGP implementation in javascript
// Copyright (C) 2018 ProtonTech AG
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
/**
* @fileoverview This module implements AES-EAX en/decryption on top of
* native AES-CTR using either the WebCrypto API or Node.js' crypto API.
* @requires asmcrypto.js
* @requires crypto/cmac
* @requires util
* @module crypto/eax
*/
import { AES_CTR } from 'asmcrypto.js/src/aes/ctr/exports';
import CMAC from './cmac';
import util from '../util';
const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
const Buffer = util.getNodeBuffer();
const blockLength = 16;
const ivLength = blockLength;
const tagLength = blockLength;
const zero = new Uint8Array(blockLength);
const one = new Uint8Array(blockLength); one[blockLength - 1] = 1;
const two = new Uint8Array(blockLength); two[blockLength - 1] = 2;
async function OMAC(key) {
const cmac = await CMAC(key);
return function(t, message) {
return cmac(util.concatUint8Array([t, message]));
};
}
async function CTR(key) {
if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
key = await webCrypto.importKey('raw', key, { name: 'AES-CTR', length: key.length * 8 }, false, ['encrypt']);
return async function(pt, iv) {
const ct = await webCrypto.encrypt({ name: 'AES-CTR', counter: iv, length: blockLength * 8 }, key, pt);
return new Uint8Array(ct);
};
}
if (util.getNodeCrypto()) { // Node crypto library
key = new Buffer(key);
return async function(pt, iv) {
pt = new Buffer(pt);
iv = new Buffer(iv);
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-ctr', key, iv);
const ct = Buffer.concat([en.update(pt), en.final()]);
return new Uint8Array(ct);
};
}
// asm.js fallback
return async function(pt, iv) {
return AES_CTR.encrypt(pt, key, iv);
};
}
/**
* Class to en/decrypt using EAX mode.
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
* @param {Uint8Array} key The encryption key
*/
async function EAX(cipher, key) {
if (cipher.substr(0, 3) !== 'aes') {
throw new Error('EAX mode supports only AES cipher');
}
const [
omac,
ctr
] = await Promise.all([
OMAC(key),
CTR(key)
]);
return {
/**
* Encrypt plaintext input.
* @param {Uint8Array} plaintext The cleartext input to be encrypted
* @param {Uint8Array} nonce The nonce (16 bytes)
* @param {Uint8Array} adata Associated data to sign
* @returns {Promise<Uint8Array>} The ciphertext output
*/
encrypt: async function(plaintext, nonce, adata) {
const [
omacNonce,
omacAdata
] = await Promise.all([
omac(zero, nonce),
omac(one, adata)
]);
const ciphered = await ctr(plaintext, omacNonce);
const omacCiphered = await omac(two, ciphered);
const tag = omacCiphered; // Assumes that omac(*).length === tagLength.
for (let i = 0; i < tagLength; i++) {
tag[i] ^= omacAdata[i] ^ omacNonce[i];
}
return util.concatUint8Array([ciphered, tag]);
},
/**
* Decrypt ciphertext input.
* @param {Uint8Array} ciphertext The ciphertext input to be decrypted
* @param {Uint8Array} nonce The nonce (16 bytes)
* @param {Uint8Array} adata Associated data to verify
* @returns {Promise<Uint8Array>} The plaintext output
*/
decrypt: async function(ciphertext, nonce, adata) {
if (ciphertext.length < tagLength) throw new Error('Invalid EAX ciphertext');
const ciphered = ciphertext.subarray(0, -tagLength);
const ctTag = ciphertext.subarray(-tagLength);
const [
omacNonce,
omacAdata,
omacCiphered
] = await Promise.all([
omac(zero, nonce),
omac(one, adata),
omac(two, ciphered)
]);
const tag = omacCiphered; // Assumes that omac(*).length === tagLength.
for (let i = 0; i < tagLength; i++) {
tag[i] ^= omacAdata[i] ^ omacNonce[i];
}
if (!util.equalsUint8Array(ctTag, tag)) throw new Error('Authentication tag mismatch');
const plaintext = await ctr(ciphered, omacNonce);
return plaintext;
}
};
}
/**
* Get EAX nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.1|RFC4880bis-04, section 5.16.1}.
* @param {Uint8Array} iv The initialization vector (16 bytes)
* @param {Uint8Array} chunkIndex The chunk index (8 bytes)
*/
EAX.getNonce = function(iv, chunkIndex) {
const nonce = iv.slice();
for (let i = 0; i < chunkIndex.length; i++) {
nonce[8 + i] ^= chunkIndex[i];
}
return nonce;
};
EAX.blockLength = blockLength;
EAX.ivLength = ivLength;
EAX.tagLength = tagLength;
export default EAX;

View File

@ -32,93 +32,104 @@ const webCrypto = util.getWebCrypto(); // no GCM support in IE11, Safari 9
const nodeCrypto = util.getNodeCrypto();
const Buffer = util.getNodeBuffer();
const blockLength = 16;
const ivLength = 12; // size of the IV in bytes
const TAG_LEN = 16; // size of the tag in bytes
const tagLength = 16; // size of the tag in bytes
const ALGO = 'AES-GCM';
/**
* Encrypt plaintext input.
* Class to en/decrypt using GCM mode.
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
* @param {Uint8Array} plaintext The cleartext input to be encrypted
* @param {Uint8Array} key The encryption key
* @param {Uint8Array} iv The initialization vector (12 bytes)
* @returns {Promise<Uint8Array>} The ciphertext output
*/
function encrypt(cipher, plaintext, key, iv) {
async function GCM(cipher, key) {
if (cipher.substr(0, 3) !== 'aes') {
return Promise.reject(new Error('GCM mode supports only AES cipher'));
throw new Error('GCM mode supports only AES cipher');
}
if (webCrypto && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
return webEncrypt(plaintext, key, iv);
} else if (nodeCrypto) { // Node crypto library
return nodeEncrypt(plaintext, key, iv);
} // asm.js fallback
return Promise.resolve(AES_GCM.encrypt(plaintext, key, iv));
if (util.getWebCrypto() && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
const _key = await webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt', 'decrypt']);
return {
encrypt: async function(pt, iv, adata=new Uint8Array()) {
if (!pt.length) {
// iOS does not support GCM-en/decrypting empty messages
// Also, synchronous en/decryption might be faster in this case.
return AES_GCM.encrypt(pt, key, iv, adata);
}
const ct = await webCrypto.encrypt({ name: ALGO, iv, additionalData: adata }, _key, pt);
return new Uint8Array(ct);
},
decrypt: async function(ct, iv, adata=new Uint8Array()) {
if (ct.length === tagLength) {
// iOS does not support GCM-en/decrypting empty messages
// Also, synchronous en/decryption might be faster in this case.
return AES_GCM.decrypt(ct, key, iv, adata);
}
const pt = await webCrypto.decrypt({ name: ALGO, iv, additionalData: adata }, _key, ct);
return new Uint8Array(pt);
}
};
}
if (util.getNodeCrypto()) { // Node crypto library
key = new Buffer(key);
return {
encrypt: async function(pt, iv, adata=new Uint8Array()) {
pt = new Buffer(pt);
iv = new Buffer(iv);
adata = new Buffer(adata);
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-gcm', key, iv);
en.setAAD(adata);
const ct = Buffer.concat([en.update(pt), en.final(), en.getAuthTag()]); // append auth tag to ciphertext
return new Uint8Array(ct);
},
decrypt: async function(ct, iv, adata=new Uint8Array()) {
ct = new Buffer(ct);
iv = new Buffer(iv);
adata = new Buffer(adata);
const de = new nodeCrypto.createDecipheriv('aes-' + (key.length * 8) + '-gcm', key, iv);
de.setAAD(adata);
de.setAuthTag(ct.slice(ct.length - tagLength, ct.length)); // read auth tag at end of ciphertext
const pt = Buffer.concat([de.update(ct.slice(0, ct.length - tagLength)), de.final()]);
return new Uint8Array(pt);
}
};
}
return {
encrypt: async function(pt, iv, adata) {
return AES_GCM.encrypt(pt, key, iv, adata);
},
decrypt: async function(ct, iv, adata) {
return AES_GCM.decrypt(ct, key, iv, adata);
}
};
}
/**
* Decrypt ciphertext input.
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
* @param {Uint8Array} ciphertext The ciphertext input to be decrypted
* @param {Uint8Array} key The encryption key
* Get GCM nonce. Note: this operation is not defined by the standard.
* A future version of the standard may define GCM mode differently,
* hopefully under a different ID (we use Private/Experimental algorithm
* ID 100) so that we can maintain backwards compatibility.
* @param {Uint8Array} iv The initialization vector (12 bytes)
* @returns {Promise<Uint8Array>} The plaintext output
* @param {Uint8Array} chunkIndex The chunk index (8 bytes)
*/
function decrypt(cipher, ciphertext, key, iv) {
if (cipher.substr(0, 3) !== 'aes') {
return Promise.reject(new Error('GCM mode supports only AES cipher'));
GCM.getNonce = function(iv, chunkIndex) {
const nonce = iv.slice();
for (let i = 0; i < chunkIndex.length; i++) {
nonce[4 + i] ^= chunkIndex[i];
}
if (webCrypto && key.length !== 24) { // WebCrypto (no 192 bit support) see: https://www.chromium.org/blink/webcrypto#TOC-AES-support
return webDecrypt(ciphertext, key, iv);
} else if (nodeCrypto) { // Node crypto library
return nodeDecrypt(ciphertext, key, iv);
} // asm.js fallback
return Promise.resolve(AES_GCM.decrypt(ciphertext, key, iv));
}
export default {
ivLength,
encrypt,
decrypt
return nonce;
};
GCM.blockLength = blockLength;
GCM.ivLength = ivLength;
GCM.tagLength = tagLength;
//////////////////////////
// //
// Helper functions //
// //
//////////////////////////
function webEncrypt(pt, key, iv) {
return webCrypto.importKey('raw', key, { name: ALGO }, false, ['encrypt'])
.then(keyObj => webCrypto.encrypt({ name: ALGO, iv }, keyObj, pt))
.then(ct => new Uint8Array(ct));
}
function webDecrypt(ct, key, iv) {
return webCrypto.importKey('raw', key, { name: ALGO }, false, ['decrypt'])
.then(keyObj => webCrypto.decrypt({ name: ALGO, iv }, keyObj, ct))
.then(pt => new Uint8Array(pt));
}
function nodeEncrypt(pt, key, iv) {
pt = new Buffer(pt);
key = new Buffer(key);
iv = new Buffer(iv);
const en = new nodeCrypto.createCipheriv('aes-' + (key.length * 8) + '-gcm', key, iv);
const ct = Buffer.concat([en.update(pt), en.final(), en.getAuthTag()]); // append auth tag to ciphertext
return Promise.resolve(new Uint8Array(ct));
}
function nodeDecrypt(ct, key, iv) {
ct = new Buffer(ct);
key = new Buffer(key);
iv = new Buffer(iv);
const de = new nodeCrypto.createDecipheriv('aes-' + (key.length * 8) + '-gcm', key, iv);
de.setAuthTag(ct.slice(ct.length - TAG_LEN, ct.length)); // read auth tag at end of ciphertext
const pt = Buffer.concat([de.update(ct.slice(0, ct.length - TAG_LEN)), de.final()]);
return Promise.resolve(new Uint8Array(pt));
}
export default GCM;

View File

@ -13,6 +13,8 @@ import cipher from './cipher';
import hash from './hash';
import cfb from './cfb';
import gcm from './gcm';
import eax from './eax';
import ocb from './ocb';
import publicKey from './public_key';
import signature from './signature';
import random from './random';
@ -31,6 +33,11 @@ const mod = {
cfb: cfb,
/** @see module:crypto/gcm */
gcm: gcm,
experimental_gcm: gcm,
/** @see module:crypto/eax */
eax: eax,
/** @see module:crypto/ocb */
ocb: ocb,
/** @see module:crypto/public_key */
publicKey: publicKey,
/** @see module:crypto/signature */

274
src/crypto/ocb.js Normal file
View File

@ -0,0 +1,274 @@
// OpenPGP.js - An OpenPGP implementation in javascript
// Copyright (C) 2018 ProtonTech AG
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3.0 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
/**
* @fileoverview This module implements AES-OCB en/decryption.
* @requires crypto/cipher
* @requires util
* @module crypto/ocb
*/
import ciphers from './cipher';
import util from '../util';
const blockLength = 16;
const ivLength = 15;
// https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.2:
// While OCB [RFC7253] allows the authentication tag length to be of any
// number up to 128 bits long, this document requires a fixed
// authentication tag length of 128 bits (16 octets) for simplicity.
const tagLength = 16;
function ntz(n) {
let ntz = 0;
for(let i = 1; (n & i) === 0; i <<= 1) {
ntz++;
}
return ntz;
}
function xorMut(S, T) {
for (let i = 0; i < S.length; i++) {
S[i] ^= T[i];
}
return S;
}
function xor(S, T) {
return xorMut(S.slice(), T);
}
const zeroBlock = new Uint8Array(blockLength);
const one = new Uint8Array([1]);
/**
* Class to en/decrypt using OCB mode.
* @param {String} cipher The symmetric cipher algorithm to use e.g. 'aes128'
* @param {Uint8Array} key The encryption key
*/
async function OCB(cipher, key) {
let maxNtz = 0;
let encipher;
let decipher;
let mask;
constructKeyVariables(cipher, key);
function constructKeyVariables(cipher, key) {
const aes = new ciphers[cipher](key);
encipher = aes.encrypt.bind(aes);
decipher = aes.decrypt.bind(aes);
const mask_x = encipher(zeroBlock);
const mask_$ = util.double(mask_x);
mask = [];
mask[0] = util.double(mask_$);
mask.x = mask_x;
mask.$ = mask_$;
}
function extendKeyVariables(text, adata) {
const newMaxNtz = util.nbits(Math.max(text.length, adata.length) / blockLength | 0) - 1;
for (let i = maxNtz + 1; i <= newMaxNtz; i++) {
mask[i] = util.double(mask[i - 1]);
}
maxNtz = newMaxNtz;
}
function hash(adata) {
if (!adata.length) {
// Fast path
return zeroBlock;
}
//
// Consider A as a sequence of 128-bit blocks
//
const m = adata.length / blockLength | 0;
const offset = new Uint8Array(blockLength);
const sum = new Uint8Array(blockLength);
for (let i = 0; i < m; i++) {
xorMut(offset, mask[ntz(i + 1)]);
xorMut(sum, encipher(xor(offset, adata)));
adata = adata.subarray(blockLength);
}
//
// Process any final partial block; compute final hash value
//
if (adata.length) {
xorMut(offset, mask.x);
const cipherInput = new Uint8Array(blockLength);
cipherInput.set(adata, 0);
cipherInput[adata.length] = 0b10000000;
xorMut(cipherInput, offset);
xorMut(sum, encipher(cipherInput));
}
return sum;
}
/**
* Encrypt/decrypt data.
* @param {encipher|decipher} fn Encryption/decryption block cipher function
* @param {Uint8Array} text The cleartext or ciphertext (without tag) input
* @param {Uint8Array} nonce The nonce (15 bytes)
* @param {Uint8Array} adata Associated data to sign
* @returns {Promise<Uint8Array>} The ciphertext or plaintext output, with tag appended in both cases
*/
function crypt(fn, text, nonce, adata) {
//
// Consider P as a sequence of 128-bit blocks
//
const m = text.length / blockLength | 0;
//
// Key-dependent variables
//
extendKeyVariables(text, adata);
//
// Nonce-dependent and per-encryption variables
//
// Nonce = num2str(TAGLEN mod 128,7) || zeros(120-bitlen(N)) || 1 || N
// Note: We assume here that tagLength mod 16 == 0.
const paddedNonce = util.concatUint8Array([zeroBlock.subarray(0, ivLength - nonce.length), one, nonce]);
// bottom = str2num(Nonce[123..128])
const bottom = paddedNonce[blockLength - 1] & 0b111111;
// Ktop = ENCIPHER(K, Nonce[1..122] || zeros(6))
paddedNonce[blockLength - 1] &= 0b11000000;
const kTop = encipher(paddedNonce);
// Stretch = Ktop || (Ktop[1..64] xor Ktop[9..72])
const stretched = util.concatUint8Array([kTop, xor(kTop.subarray(0, 8), kTop.subarray(1, 9))]);
// Offset_0 = Stretch[1+bottom..128+bottom]
const offset = util.shiftRight(stretched.subarray(0 + (bottom >> 3), 17 + (bottom >> 3)), 8 - (bottom & 7)).subarray(1);
// Checksum_0 = zeros(128)
const checksum = new Uint8Array(blockLength);
const ct = new Uint8Array(text.length + tagLength);
//
// Process any whole blocks
//
let i;
let pos = 0;
for (i = 0; i < m; i++) {
// Offset_i = Offset_{i-1} xor L_{ntz(i)}
xorMut(offset, mask[ntz(i + 1)]);
// C_i = Offset_i xor ENCIPHER(K, P_i xor Offset_i)
// P_i = Offset_i xor DECIPHER(K, C_i xor Offset_i)
ct.set(xorMut(fn(xor(offset, text)), offset), pos);
// Checksum_i = Checksum_{i-1} xor P_i
xorMut(checksum, fn === encipher ? text : ct.subarray(pos));
text = text.subarray(blockLength);
pos += blockLength;
}
//
// Process any final partial block and compute raw tag
//
if (text.length) {
// Offset_* = Offset_m xor L_*
xorMut(offset, mask.x);
// Pad = ENCIPHER(K, Offset_*)
const padding = encipher(offset);
// C_* = P_* xor Pad[1..bitlen(P_*)]
ct.set(xor(text, padding), pos);
// Checksum_* = Checksum_m xor (P_* || 1 || new Uint8Array(127-bitlen(P_*)))
const xorInput = new Uint8Array(blockLength);
xorInput.set(fn === encipher ? text : ct.subarray(pos, -tagLength), 0);
xorInput[text.length] = 0b10000000;
xorMut(checksum, xorInput);
pos += text.length;
}
// Tag = ENCIPHER(K, Checksum_* xor Offset_* xor L_$) xor HASH(K,A)
const tag = xorMut(encipher(xorMut(xorMut(checksum, offset), mask.$)), hash(adata));
//
// Assemble ciphertext
//
// C = C_1 || C_2 || ... || C_m || C_* || Tag[1..TAGLEN]
ct.set(tag, pos);
return ct;
}
return {
/**
* Encrypt plaintext input.
* @param {Uint8Array} plaintext The cleartext input to be encrypted
* @param {Uint8Array} nonce The nonce (15 bytes)
* @param {Uint8Array} adata Associated data to sign
* @returns {Promise<Uint8Array>} The ciphertext output
*/
encrypt: async function(plaintext, nonce, adata) {
return crypt(encipher, plaintext, nonce, adata);
},
/**
* Decrypt ciphertext input.
* @param {Uint8Array} ciphertext The ciphertext input to be decrypted
* @param {Uint8Array} nonce The nonce (15 bytes)
* @param {Uint8Array} adata Associated data to sign
* @returns {Promise<Uint8Array>} The ciphertext output
*/
decrypt: async function(ciphertext, nonce, adata) {
if (ciphertext.length < tagLength) throw new Error('Invalid OCB ciphertext');
const tag = ciphertext.subarray(-tagLength);
ciphertext = ciphertext.subarray(0, -tagLength);
const crypted = crypt(decipher, ciphertext, nonce, adata);
// if (Tag[1..TAGLEN] == T)
if (util.equalsUint8Array(tag, crypted.subarray(-tagLength))) {
return crypted.subarray(0, -tagLength);
}
throw new Error('Authentication tag mismatch');
}
};
}
/**
* Get OCB nonce as defined by {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.16.2|RFC4880bis-04, section 5.16.2}.
* @param {Uint8Array} iv The initialization vector (15 bytes)
* @param {Uint8Array} chunkIndex The chunk index (8 bytes)
*/
OCB.getNonce = function(iv, chunkIndex) {
const nonce = iv.slice();
for (let i = 0; i < chunkIndex.length; i++) {
nonce[7 + i] ^= chunkIndex[i];
}
return nonce;
};
OCB.blockLength = blockLength;
OCB.ivLength = ivLength;
OCB.tagLength = tagLength;
export default OCB;

View File

@ -67,9 +67,8 @@ export default {
// truncated) hash function result is treated as a number and used
// directly in the DSA signature algorithm.
const h = new BN(
util.str_to_Uint8Array(
util.getLeftNBits(
util.Uint8Array_to_str(hash.digest(hash_algo, m)), q.bitLength())));
util.getLeftNBits(
hash.digest(hash_algo, m), q.bitLength()));
// FIPS-186-4, section 4.6:
// The values of r and s shall be checked to determine if r = 0 or s = 0.
// If either r = 0 or s = 0, a new value of k shall be generated, and the
@ -116,9 +115,8 @@ export default {
const redp = new BN.red(p);
const redq = new BN.red(q);
const h = new BN(
util.str_to_Uint8Array(
util.getLeftNBits(
util.Uint8Array_to_str(hash.digest(hash_algo, m)), q.bitLength())));
util.getLeftNBits(
hash.digest(hash_algo, m), q.bitLength()));
const w = s.toRed(redq).redInvm(); // s**-1 mod q
if (zero.cmp(w) === 0) {
util.print_debug("invalid DSA Signature");

View File

@ -182,14 +182,14 @@ Curve.prototype.keyFromPublic = function (pub) {
Curve.prototype.genKeyPair = async function () {
let keyPair;
if (webCrypto && this.web) {
if (this.web && util.getWebCrypto()) {
// If browser doesn't support a curve, we'll catch it
try {
keyPair = await webGenKeyPair(this.name);
} catch (err) {
util.print_debug("Browser did not support signing: " + err.message);
}
} else if (nodeCrypto && this.node) {
} else if (this.node && util.getNodeCrypto()) {
keyPair = await nodeGenKeyPair(this.name);
}

View File

@ -46,7 +46,7 @@ function buildEcdhParam(public_algo, oid, cipher_algo, hash_algo, fingerprint) {
new Uint8Array([public_algo]),
kdf_params.write(),
util.str_to_Uint8Array("Anonymous Sender "),
fingerprint
fingerprint.subarray(0, 20)
]);
}
@ -73,7 +73,6 @@ function kdf(hash_algo, X, length, param) {
* @async
*/
async function encrypt(oid, cipher_algo, hash_algo, m, Q, fingerprint) {
fingerprint = util.hex_to_Uint8Array(fingerprint);
const curve = new Curve(oid);
const param = buildEcdhParam(enums.publicKey.ecdh, oid, cipher_algo, hash_algo, fingerprint);
cipher_algo = enums.read(enums.symmetric, cipher_algo);
@ -102,7 +101,6 @@ async function encrypt(oid, cipher_algo, hash_algo, m, Q, fingerprint) {
* @async
*/
async function decrypt(oid, cipher_algo, hash_algo, V, C, d, fingerprint) {
fingerprint = util.hex_to_Uint8Array(fingerprint);
const curve = new Curve(oid);
const param = buildEcdhParam(enums.publicKey.ecdh, oid, cipher_algo, hash_algo, fingerprint);
cipher_algo = enums.read(enums.symmetric, cipher_algo);

View File

@ -45,7 +45,7 @@ function KeyPair(curve, options) {
}
KeyPair.prototype.sign = async function (message, hash_algo) {
if (webCrypto && this.curve.web) {
if (this.curve.web && util.getWebCrypto()) {
// If browser doesn't support a curve, we'll catch it
try {
// need to await to make sure browser succeeds
@ -54,7 +54,7 @@ KeyPair.prototype.sign = async function (message, hash_algo) {
} catch (err) {
util.print_debug("Browser did not support signing: " + err.message);
}
} else if (nodeCrypto && this.curve.node) {
} else if (this.curve.node && util.getNodeCrypto()) {
return nodeSign(this.curve, hash_algo, message, this.keyPair);
}
const digest = (typeof hash_algo === 'undefined') ? message : hash.digest(hash_algo, message);
@ -62,7 +62,7 @@ KeyPair.prototype.sign = async function (message, hash_algo) {
};
KeyPair.prototype.verify = async function (message, signature, hash_algo) {
if (webCrypto && this.curve.web) {
if (this.curve.web && util.getWebCrypto()) {
// If browser doesn't support a curve, we'll catch it
try {
// need to await to make sure browser succeeds
@ -71,7 +71,7 @@ KeyPair.prototype.verify = async function (message, signature, hash_algo) {
} catch (err) {
util.print_debug("Browser did not support signing: " + err.message);
}
} else if (nodeCrypto && this.curve.node) {
} else if (this.curve.node && util.getNodeCrypto()) {
return nodeVerify(this.curve, hash_algo, signature, message, this.keyPair.getPublic());
}
const digest = (typeof hash_algo === 'undefined') ? message : hash.digest(hash_algo, message);

View File

@ -88,7 +88,7 @@ export default {
gnu: 101
},
/** {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC4880, section 9.1}
/** {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-9.1|RFC4880bis-04, section 9.1}
* @enum {Integer}
* @readonly
*/
@ -109,7 +109,11 @@ export default {
ecdsa: 19,
/** EdDSA (Sign only)
* [{@link https://tools.ietf.org/html/draft-koch-eddsa-for-openpgp-04|Draft RFC}] */
eddsa: 22
eddsa: 22,
/** Reserved for AEDH */
aedh: 23,
/** Reserved for AEDSA */
aedsa: 24
},
/** {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC4880, section 9.2}
@ -167,6 +171,16 @@ export default {
'SHA-512': 10
},
/** {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-9.6|RFC4880bis-04, section 9.6}
* @enum {Integer}
* @readonly
*/
aead: {
eax: 1,
ocb: 2,
experimental_gcm: 100 // Private algorithm
},
/** A list of packet types and numeric tags associated with them.
* @enum {Integer}
* @readonly
@ -202,7 +216,9 @@ export default {
/** Text data 't' */
text: 't'.charCodeAt(),
/** Utf8 data 'u' */
utf8: 'u'.charCodeAt()
utf8: 'u'.charCodeAt(),
/** MIME message body part 'm' */
mime: 'm'.charCodeAt()
},
@ -356,7 +372,9 @@ export default {
reason_for_revocation: 29,
features: 30,
signature_target: 31,
embedded_signature: 32
embedded_signature: 32,
issuer_fingerprint: 33,
preferred_aead_algorithms: 34
},
/** Key flags
@ -408,6 +426,21 @@ export default {
signature: 6
},
/** {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.2.3.25|RFC4880bis-04, section 5.2.3.25}
* @enum {Integer}
* @readonly
*/
features: {
/** 0x01 - Modification Detection (packets 18 and 19) */
modification_detection: 1,
/** 0x02 - AEAD Encrypted Data Packet (packet 20) and version 5
* Symmetric-Key Encrypted Session Key Packets (packet 3) */
aead: 2,
/** 0x04 - Version 5 Public-Key Packet format and corresponding new
* fingerprint format */
v5_keys: 4
},
/** Asserts validity and converts from string/integer to integer. */
write: function(type, e) {
if (typeof e === 'number') {

View File

@ -468,7 +468,7 @@ Key.prototype.getExpirationTime = async function() {
if (this.primaryKey.version === 3) {
return getExpirationTime(this.primaryKey);
}
if (this.primaryKey.version === 4) {
if (this.primaryKey.version >= 4) {
const primaryUser = await this.getPrimaryUser(null);
const selfCert = primaryUser.selfCertification;
const keyExpiry = getExpirationTime(this.primaryKey, selfCert);
@ -1261,6 +1261,11 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) {
signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.aes192);
signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.cast5);
signaturePacket.preferredSymmetricAlgorithms.push(enums.symmetric.tripledes);
if (config.aead_protect && config.aead_protect_version === 4) {
signaturePacket.preferredAeadAlgorithms = [];
signaturePacket.preferredAeadAlgorithms.push(enums.aead.eax);
signaturePacket.preferredAeadAlgorithms.push(enums.aead.ocb);
}
signaturePacket.preferredHashAlgorithms = [];
// prefer fast asm.js implementations (SHA-256). SHA-1 will not be secure much longer...move to bottom of list
signaturePacket.preferredHashAlgorithms.push(enums.hash.sha256);
@ -1273,8 +1278,13 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) {
signaturePacket.isPrimaryUserID = true;
}
if (config.integrity_protect) {
signaturePacket.features = [];
signaturePacket.features.push(1); // Modification Detection
signaturePacket.features = [0];
signaturePacket.features[0] |= enums.features.modification_detection;
}
if (config.aead_protect && config.aead_protect_version === 4) {
signaturePacket.features || (signaturePacket.features = [0]);
signaturePacket.features[0] |= enums.features.aead;
signaturePacket.features[0] |= enums.features.v5_keys;
}
if (options.keyExpirationTime > 0) {
signaturePacket.keyExpirationTime = options.keyExpirationTime;
@ -1383,7 +1393,7 @@ function getExpirationTime(keyPacket, signature) {
expirationTime = keyPacket.created.getTime() + keyPacket.expirationTimeV3*24*3600*1000;
}
// check V4 expiration time
if (keyPacket.version === 4 && signature.keyNeverExpires === false) {
if (keyPacket.version >= 4 && signature.keyNeverExpires === false) {
expirationTime = keyPacket.created.getTime() + signature.keyExpirationTime*1000;
}
return expirationTime ? new Date(expirationTime) : Infinity;
@ -1392,14 +1402,15 @@ function getExpirationTime(keyPacket, signature) {
/**
* Returns the preferred signature hash algorithm of a key
* @param {object} key
* @param {Date} date (optional) use the given date for verification instead of the current time
* @returns {Promise<String>}
* @async
*/
export async function getPreferredHashAlgo(key) {
export async function getPreferredHashAlgo(key, date) {
let hash_algo = config.prefer_hash_algorithm;
let pref_algo = hash_algo;
if (key instanceof Key) {
const primaryUser = await key.getPrimaryUser();
const primaryUser = await key.getPrimaryUser(date);
if (primaryUser && primaryUser.selfCertification.preferredHashAlgorithms) {
[pref_algo] = primaryUser.selfCertification.preferredHashAlgorithms;
hash_algo = crypto.hash.getHashByteLength(hash_algo) <= crypto.hash.getHashByteLength(pref_algo) ?
@ -1425,30 +1436,34 @@ export async function getPreferredHashAlgo(key) {
}
/**
* Returns the preferred symmetric algorithm for a set of keys
* Returns the preferred symmetric/aead algorithm for a set of keys
* @param {symmetric|aead} type Type of preference to return
* @param {Array<module:key.Key>} keys Set of keys
* @param {Date} date (optional) use the given date for verification instead of the current time
* @returns {Promise<module:enums.symmetric>} Preferred symmetric algorithm
* @async
*/
export async function getPreferredSymAlgo(keys) {
export async function getPreferredAlgo(type, keys, date) {
const prefProperty = type === 'symmetric' ? 'preferredSymmetricAlgorithms' : 'preferredAeadAlgorithms';
const defaultAlgo = type === 'symmetric' ? config.encryption_cipher : config.aead_mode;
const prioMap = {};
await Promise.all(keys.map(async function(key) {
const primaryUser = await key.getPrimaryUser();
if (!primaryUser || !primaryUser.selfCertification.preferredSymmetricAlgorithms) {
return config.encryption_cipher;
const primaryUser = await key.getPrimaryUser(date);
if (!primaryUser || !primaryUser.selfCertification[prefProperty]) {
return defaultAlgo;
}
primaryUser.selfCertification.preferredSymmetricAlgorithms.forEach(function(algo, index) {
primaryUser.selfCertification[prefProperty].forEach(function(algo, index) {
const entry = prioMap[algo] || (prioMap[algo] = { prio: 0, count: 0, algo: algo });
entry.prio += 64 >> index;
entry.count++;
});
}));
let prefAlgo = { prio: 0, algo: config.encryption_cipher };
let prefAlgo = { prio: 0, algo: defaultAlgo };
for (const algo in prioMap) {
try {
if (algo !== enums.symmetric.plaintext &&
algo !== enums.symmetric.idea && // not implemented
enums.read(enums.symmetric, algo) && // known algorithm
if (algo !== enums[type].plaintext &&
algo !== enums[type].idea && // not implemented
enums.read(enums[type], algo) && // known algorithm
prioMap[algo].count === keys.length && // available for all keys
prioMap[algo].prio > prefAlgo.prio) {
prefAlgo = prioMap[algo];
@ -1457,3 +1472,23 @@ export async function getPreferredSymAlgo(keys) {
}
return prefAlgo.algo;
}
/**
* Returns whether aead is supported by all keys in the set
* @param {Array<module:key.Key>} keys Set of keys
* @param {Date} date (optional) use the given date for verification instead of the current time
* @returns {Promise<Boolean>}
* @async
*/
export async function isAeadSupported(keys, date) {
let supported = true;
// TODO replace when Promise.some or Promise.any are implemented
await Promise.all(keys.map(async function(key) {
const primaryUser = await key.getPrimaryUser(date);
if (!primaryUser || !primaryUser.selfCertification.features ||
!(primaryUser.selfCertification.features[0] & enums.features.aead)) {
supported = false;
}
}));
return supported;
}

View File

@ -36,7 +36,7 @@ import enums from './enums';
import util from './util';
import packet from './packet';
import { Signature } from './signature';
import { getPreferredHashAlgo, getPreferredSymAlgo } from './key';
import { getPreferredHashAlgo, getPreferredAlgo, isAeadSupported } from './key';
/**
@ -93,7 +93,7 @@ Message.prototype.getSigningKeyIds = function() {
* Decrypt the message. Either a private key, a session key, or a password must be specified.
* @param {Array<Key>} privateKeys (optional) private keys with decrypted secret data
* @param {Array<String>} passwords (optional) passwords used to decrypt
* @param {Array<Object>} sessionKeys (optional) session keys in the form: { data:Uint8Array, algorithm:String }
* @param {Array<Object>} sessionKeys (optional) session keys in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] }
* @returns {Promise<Message>} new message with decrypted content
* @async
*/
@ -157,7 +157,9 @@ Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) {
try {
await keyPacket.decrypt(password);
keyPackets.push(keyPacket);
} catch (err) {}
} catch (err) {
util.print_debug_error(err);
}
}));
}));
} else if (privateKeys) {
@ -180,7 +182,9 @@ Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) {
try {
await keyPacket.decrypt(privateKeyPacket);
keyPackets.push(keyPacket);
} catch (err) {}
} catch (err) {
util.print_debug_error(err);
}
}));
}));
} else {
@ -212,7 +216,7 @@ Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) {
*/
Message.prototype.getLiteralData = function() {
const literal = this.packets.findPacket(enums.packet.literal);
return (literal && literal.data) || null;
return (literal && literal.getBytes()) || null;
};
/**
@ -240,7 +244,7 @@ Message.prototype.getText = function() {
* Encrypt the message either with public keys, passwords, or both at once.
* @param {Array<Key>} keys (optional) public key(s) for message encryption
* @param {Array<String>} passwords (optional) password(s) for message encryption
* @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String }
* @param {Object} sessionKey (optional) session key in the form: { data:Uint8Array, algorithm:String, [aeadAlgorithm:String] }
* @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs
* @param {Date} date (optional) override the creation date of the literal package
* @returns {Promise<Message>} new message with encrypted content
@ -248,6 +252,7 @@ Message.prototype.getText = function() {
*/
Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard=false, date=new Date()) {
let symAlgo;
let aeadAlgo;
let symEncryptedPacket;
if (sessionKey) {
@ -255,11 +260,16 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard
throw new Error('Invalid session key for encryption.');
}
symAlgo = sessionKey.algorithm;
aeadAlgo = sessionKey.aeadAlgorithm;
sessionKey = sessionKey.data;
} else if (keys && keys.length) {
symAlgo = enums.read(enums.symmetric, await getPreferredSymAlgo(keys));
symAlgo = enums.read(enums.symmetric, await getPreferredAlgo('symmetric', keys, date));
if (config.aead_protect && config.aead_protect_version === 4 && await isAeadSupported(keys, date)) {
aeadAlgo = enums.read(enums.aead, await getPreferredAlgo('aead', keys, date));
}
} else if (passwords && passwords.length) {
symAlgo = enums.read(enums.symmetric, config.encryption_cipher);
aeadAlgo = enums.read(enums.aead, config.aead_mode);
} else {
throw new Error('No keys, passwords, or session key provided.');
}
@ -268,10 +278,11 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard
sessionKey = await crypto.generateSessionKey(symAlgo);
}
const msg = await encryptSessionKey(sessionKey, symAlgo, keys, passwords, wildcard, date);
const msg = await encryptSessionKey(sessionKey, symAlgo, aeadAlgo, keys, passwords, wildcard, date);
if (config.aead_protect) {
if (config.aead_protect && (config.aead_protect_version !== 4 || aeadAlgo)) {
symEncryptedPacket = new packet.SymEncryptedAEADProtected();
symEncryptedPacket.aeadAlgorithm = aeadAlgo;
} else if (config.integrity_protect) {
symEncryptedPacket = new packet.SymEncryptedIntegrityProtected();
} else {
@ -287,7 +298,8 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard
message: msg,
sessionKey: {
data: sessionKey,
algorithm: symAlgo
algorithm: symAlgo,
aeadAlgorithm: aeadAlgo
}
};
};
@ -296,6 +308,7 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard
* Encrypt a session key either with public keys, passwords, or both at once.
* @param {Uint8Array} sessionKey session key for encryption
* @param {String} symAlgo session key algorithm
* @param {String} aeadAlgo (optional) aead algorithm, e.g. 'eax' or 'ocb'
* @param {Array<Key>} publicKeys (optional) public key(s) for message encryption
* @param {Array<String>} passwords (optional) for message encryption
* @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs
@ -303,7 +316,7 @@ Message.prototype.encrypt = async function(keys, passwords, sessionKey, wildcard
* @returns {Promise<Message>} new message with encrypted content
* @async
*/
export async function encryptSessionKey(sessionKey, symAlgo, publicKeys, passwords, wildcard=false, date=new Date()) {
export async function encryptSessionKey(sessionKey, symAlgo, aeadAlgo, publicKeys, passwords, wildcard=false, date=new Date()) {
const packetlist = new packet.List();
if (publicKeys) {
@ -336,10 +349,13 @@ export async function encryptSessionKey(sessionKey, symAlgo, publicKeys, passwor
const sum = (accumulator, currentValue) => accumulator + currentValue;
const encryptPassword = async function(sessionKey, symAlgo, password) {
const encryptPassword = async function(sessionKey, symAlgo, aeadAlgo, password) {
const symEncryptedSessionKeyPacket = new packet.SymEncryptedSessionKey();
symEncryptedSessionKeyPacket.sessionKey = sessionKey;
symEncryptedSessionKeyPacket.sessionKeyAlgorithm = symAlgo;
if (aeadAlgo) {
symEncryptedSessionKeyPacket.aeadAlgorithm = aeadAlgo;
}
await symEncryptedSessionKeyPacket.encrypt(password);
if (config.password_collision_check) {
@ -353,7 +369,7 @@ export async function encryptSessionKey(sessionKey, symAlgo, publicKeys, passwor
return symEncryptedSessionKeyPacket;
};
const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, symAlgo, pwd)));
const results = await Promise.all(passwords.map(pwd => encryptPassword(sessionKey, symAlgo, aeadAlgo, pwd)));
packetlist.concat(results);
}
@ -378,8 +394,8 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new
let i;
let existingSigPacketlist;
const literalFormat = enums.write(enums.literal, literalDataPacket.format);
const signatureType = literalFormat === enums.literal.binary ?
// If data packet was created from Uint8Array, use binary, otherwise use text
const signatureType = literalDataPacket.text === null ?
enums.signature.binary : enums.signature.text;
if (signature) {
@ -409,7 +425,7 @@ Message.prototype.sign = async function(privateKeys=[], signature=null, date=new
}
const onePassSig = new packet.OnePassSignature();
onePassSig.type = signatureType;
onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey);
onePassSig.hashAlgorithm = await getPreferredHashAlgo(privateKey, date);
onePassSig.publicKeyAlgorithm = signingKeyPacket.algorithm;
onePassSig.signingKeyId = signingKeyPacket.getKeyId();
if (i === privateKeys.length - 1) {
@ -474,8 +490,8 @@ Message.prototype.signDetached = async function(privateKeys=[], signature=null,
export async function createSignaturePackets(literalDataPacket, privateKeys, signature=null, date=new Date()) {
const packetlist = new packet.List();
const literalFormat = enums.write(enums.literal, literalDataPacket.format);
const signatureType = literalFormat === enums.literal.binary ?
// If data packet was created from Uint8Array, use binary, otherwise use text
const signatureType = literalDataPacket.text === null ?
enums.signature.binary : enums.signature.text;
await Promise.all(privateKeys.map(async function(privateKey) {
@ -493,7 +509,7 @@ export async function createSignaturePackets(literalDataPacket, privateKeys, sig
const signaturePacket = new packet.Signature(date);
signaturePacket.signatureType = signatureType;
signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm;
signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey);
signaturePacket.hashAlgorithm = await getPreferredHashAlgo(privateKey, date);
await signaturePacket.sign(signingKeyPacket, literalDataPacket);
return signaturePacket;
})).then(signatureList => {
@ -564,15 +580,9 @@ export async function createVerificationObjects(signatureList, literalDataList,
}
}));
// If this is a text signature, canonicalize line endings of the data
const literalDataPacket = literalDataList[0];
if (signature.signatureType === enums.signature.text) {
literalDataPacket.setText(literalDataPacket.getText());
}
const verifiedSig = {
keyid: signature.issuerKeyId,
valid: keyPacket ? await signature.verify(keyPacket, literalDataPacket) : null
valid: keyPacket ? await signature.verify(keyPacket, literalDataList[0]) : null
};
const packetlist = new packet.List();
@ -641,13 +651,14 @@ export function read(input) {
* @param {String} text
* @param {String} filename (optional)
* @param {Date} date (optional)
* @param {utf8|binary|text|mime} type (optional) data packet type
* @returns {module:message.Message} new message object
* @static
*/
export function fromText(text, filename, date=new Date()) {
export function fromText(text, filename, date=new Date(), type='utf8') {
const literalDataPacket = new packet.Literal(date);
// text will be converted to UTF8
literalDataPacket.setText(text);
literalDataPacket.setText(text, type);
if (filename !== undefined) {
literalDataPacket.setFilename(filename);
}
@ -661,19 +672,17 @@ export function fromText(text, filename, date=new Date()) {
* @param {Uint8Array} bytes
* @param {String} filename (optional)
* @param {Date} date (optional)
* @param {utf8|binary|text|mime} type (optional) data packet type
* @returns {module:message.Message} new message object
* @static
*/
export function fromBinary(bytes, filename, date=new Date()) {
export function fromBinary(bytes, filename, date=new Date(), type='binary') {
if (!util.isUint8Array(bytes)) {
throw new Error('Data must be in the form of a Uint8Array');
}
const literalDataPacket = new packet.Literal(date);
if (filename) {
literalDataPacket.setFilename(filename);
}
literalDataPacket.setBytes(bytes, enums.read(enums.literal, enums.literal.binary));
literalDataPacket.setBytes(bytes, type);
if (filename !== undefined) {
literalDataPacket.setFilename(filename);
}

View File

@ -23,6 +23,7 @@
* @requires cleartext
* @requires key
* @requires config
* @requires enums
* @requires util
* @requires polyfills
* @requires worker/async_proxy
@ -41,6 +42,7 @@ import * as messageLib from './message';
import { CleartextMessage } from './cleartext';
import { generate, reformat } from './key';
import config from './config/config';
import enums from './enums';
import util from './util';
import AsyncProxy from './worker/async_proxy';
@ -213,6 +215,7 @@ export function encryptKey({ privateKey, passphrase }) {
* Encrypts message text/data with public keys, passwords or both at once. At least either public keys or passwords
* must be specified. If private keys are specified, those will be used to sign the message.
* @param {String|Uint8Array} data text/data to be encrypted as JavaScript binary string or Uint8Array
* @param {utf8|binary|text|mime} dataType (optional) data packet type
* @param {Key|Array<Key>} publicKeys (optional) array of keys or single key, used to encrypt the message
* @param {Key|Array<Key>} privateKeys (optional) private keys for signing. If omitted message will not be signed
* @param {String|Array<String>} passwords (optional) array of passwords or a single password to encrypt the message
@ -231,15 +234,15 @@ export function encryptKey({ privateKey, passphrase }) {
* @async
* @static
*/
export function encrypt({ data, publicKeys, privateKeys, passwords, sessionKey, filename, compression=config.compression, armor=true, detached=false, signature=null, returnSessionKey=false, wildcard=false, date=new Date()}) {
export function encrypt({ data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression=config.compression, armor=true, detached=false, signature=null, returnSessionKey=false, wildcard=false, date=new Date()}) {
checkData(data); publicKeys = toArray(publicKeys); privateKeys = toArray(privateKeys); passwords = toArray(passwords);
if (!nativeAEAD() && asyncProxy) { // use web worker if web crypto apis are not supported
return asyncProxy.delegate('encrypt', { data, publicKeys, privateKeys, passwords, sessionKey, filename, compression, armor, detached, signature, returnSessionKey, wildcard, date });
return asyncProxy.delegate('encrypt', { data, dataType, publicKeys, privateKeys, passwords, sessionKey, filename, compression, armor, detached, signature, returnSessionKey, wildcard, date });
}
const result = {};
return Promise.resolve().then(async function() {
let message = createMessage(data, filename, date);
let message = createMessage(data, filename, date, dataType);
if (!privateKeys) {
privateKeys = [];
}
@ -314,6 +317,7 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe
/**
* Signs a cleartext message.
* @param {String | Uint8Array} data cleartext input to be signed
* @param {utf8|binary|text|mime} dataType (optional) data packet type
* @param {Key|Array<Key>} privateKeys array of keys or single key with decrypted secret key data to sign cleartext
* @param {Boolean} armor (optional) if the return value should be ascii armored or the message object
* @param {Boolean} detached (optional) if the return value should contain a detached signature
@ -324,19 +328,19 @@ export function decrypt({ message, privateKeys, passwords, sessionKeys, publicKe
* @async
* @static
*/
export function sign({ data, privateKeys, armor=true, detached=false, date=new Date() }) {
export function sign({ data, dataType, privateKeys, armor=true, detached=false, date=new Date() }) {
checkData(data);
privateKeys = toArray(privateKeys);
if (asyncProxy) { // use web worker if available
return asyncProxy.delegate('sign', {
data, privateKeys, armor, detached, date
data, dataType, privateKeys, armor, detached, date
});
}
const result = {};
return Promise.resolve().then(async function() {
let message = util.isString(data) ? new CleartextMessage(data) : messageLib.fromBinary(data);
let message = util.isString(data) ? new CleartextMessage(data) : messageLib.fromBinary(data, dataType);
if (detached) {
const signature = await message.signDetached(privateKeys, undefined, date);
@ -395,6 +399,7 @@ export function verify({ message, publicKeys, signature=null, date=new Date() })
* or passwords must be specified.
* @param {Uint8Array} data the session key to be encrypted e.g. 16 random bytes (for aes128)
* @param {String} algorithm algorithm of the symmetric session key e.g. 'aes128' or 'aes256'
* @param {String} aeadAlgorithm (optional) aead algorithm, e.g. 'eax' or 'ocb'
* @param {Key|Array<Key>} publicKeys (optional) array of public keys or single key, used to encrypt the key
* @param {String|Array<String>} passwords (optional) passwords for the message
* @param {Boolean} wildcard (optional) use a key ID of 0 instead of the public key IDs
@ -402,16 +407,16 @@ export function verify({ message, publicKeys, signature=null, date=new Date() })
* @async
* @static
*/
export function encryptSessionKey({ data, algorithm, publicKeys, passwords, wildcard=false }) {
export function encryptSessionKey({ data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard=false }) {
checkBinary(data); checkString(algorithm, 'algorithm'); publicKeys = toArray(publicKeys); passwords = toArray(passwords);
if (asyncProxy) { // use web worker if available
return asyncProxy.delegate('encryptSessionKey', { data, algorithm, publicKeys, passwords, wildcard });
return asyncProxy.delegate('encryptSessionKey', { data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard });
}
return Promise.resolve().then(async function() {
return { message: await messageLib.encryptSessionKey(data, algorithm, publicKeys, passwords, wildcard) };
return { message: await messageLib.encryptSessionKey(data, algorithm, aeadAlgorithm, publicKeys, passwords, wildcard) };
}).catch(onError.bind(null, 'Error encrypting session key'));
}
@ -526,14 +531,15 @@ function toArray(param) {
* @param {String|Uint8Array} data the payload for the message
* @param {String} filename the literal data packet's filename
* @param {Date} date the creation date of the package
* @param {utf8|binary|text|mime} type (optional) data packet type
* @returns {Message} a message object
*/
function createMessage(data, filename, date=new Date()) {
function createMessage(data, filename, date=new Date(), type) {
let msg;
if (util.isUint8Array(data)) {
msg = messageLib.fromBinary(data, filename, date);
msg = messageLib.fromBinary(data, filename, date, type);
} else if (util.isString(data)) {
msg = messageLib.fromText(data, filename, date);
msg = messageLib.fromText(data, filename, date, type);
} else {
throw new Error('Data must be of type String or Uint8Array');
}
@ -568,19 +574,26 @@ function parseMessage(message, format) {
*/
function onError(message, error) {
// log the stack trace
if (config.debug) { console.error(error.stack); }
util.print_debug_error(error);
// update error message
error.message = message + ': ' + error.message;
try {
error.message = message + ': ' + error.message;
} catch(e) {}
throw error;
}
/**
* Check for AES-GCM support and configuration by the user. Only browsers that
* implement the current WebCrypto specification support native AES-GCM.
* Check for native AEAD support and configuration by the user. Only
* browsers that implement the current WebCrypto specification support
* native GCM. Native EAX is built on CTR and CBC, which current
* browsers support. OCB and CFB are not natively supported.
* @returns {Boolean} If authenticated encryption should be used
*/
function nativeAEAD() {
return util.getWebCrypto() && config.aead_protect;
return config.aead_protect && (
((config.aead_protect_version !== 4 || config.aead_mode === enums.aead.experimental_gcm) && util.getWebCrypto()) ||
(config.aead_protect_version === 4 && config.aead_mode === enums.aead.eax && util.getWebCrypto())
);
}

View File

@ -37,7 +37,8 @@ function Literal(date=new Date()) {
this.tag = enums.packet.literal;
this.format = 'utf8'; // default format for literal data packets
this.date = util.normalizeDate(date);
this.data = new Uint8Array(0); // literal data representation
this.text = null; // textual data representation
this.data = null; // literal data representation
this.filename = 'msg.txt';
}
@ -45,13 +46,12 @@ function Literal(date=new Date()) {
* Set the packet data to a javascript native string, end of line
* will be normalized to \r\n and by default text is converted to UTF8
* @param {String} text Any native javascript string
* @param {utf8|binary|text|mime} format (optional) The format of the string of bytes
*/
Literal.prototype.setText = function(text) {
// normalize EOL to \r\n
text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n/g, "\r\n");
this.format = 'utf8';
// encode UTF8
this.data = util.str_to_Uint8Array(util.encode_utf8(text));
Literal.prototype.setText = function(text, format='utf8') {
this.format = format;
this.text = text;
this.data = null;
};
/**
@ -60,20 +60,25 @@ Literal.prototype.setText = function(text) {
* @returns {String} literal data as text
*/
Literal.prototype.getText = function() {
if (this.text !== null) {
return this.text;
}
// decode UTF8
const text = util.decode_utf8(util.Uint8Array_to_str(this.data));
// normalize EOL to \n
return text.replace(/\r\n/g, '\n');
this.text = util.nativeEOL(text);
return this.text;
};
/**
* Set the packet data to value represented by the provided string of bytes.
* @param {Uint8Array} bytes The string of bytes
* @param {utf8|binary|text} format The format of the string of bytes
* @param {utf8|binary|text|mime} format The format of the string of bytes
*/
Literal.prototype.setBytes = function(bytes, format) {
this.format = format;
this.data = bytes;
this.text = null;
};
@ -82,6 +87,14 @@ Literal.prototype.setBytes = function(bytes, format) {
* @returns {Uint8Array} A sequence of bytes
*/
Literal.prototype.getBytes = function() {
if (this.data !== null) {
return this.data;
}
// normalize EOL to \r\n
const text = util.canonicalizeEOL(this.text);
// encode UTF8
this.data = util.str_to_Uint8Array(util.encode_utf8(text));
return this.data;
};

View File

@ -177,15 +177,12 @@ export default {
// 4.2.2.1. One-Octet Lengths
if (input[mypos] < 192) {
packet_length = input[mypos++];
util.print_debug("1 byte length:" + packet_length);
// 4.2.2.2. Two-Octet Lengths
} else if (input[mypos] >= 192 && input[mypos] < 224) {
packet_length = ((input[mypos++] - 192) << 8) + (input[mypos++]) + 192;
util.print_debug("2 byte length:" + packet_length);
// 4.2.2.4. Partial Body Lengths
} else if (input[mypos] > 223 && input[mypos] < 255) {
packet_length = 1 << (input[mypos++] & 0x1F);
util.print_debug("4 byte length:" + packet_length);
// EEEK, we're reading the full data here...
let mypos2 = mypos + packet_length;
bodydata = [input.subarray(mypos, mypos + packet_length)];

View File

@ -54,6 +54,7 @@ List.prototype.read = function (bytes) {
parsed.tag === enums.packet.compressed) {
throw e;
}
util.print_debug_error(e);
if (pushed) {
this.pop(); // drop unsupported packet
}

View File

@ -18,6 +18,7 @@
/**
* @requires type/keyid
* @requires type/mpi
* @requires config
* @requires crypto
* @requires enums
* @requires util
@ -25,6 +26,7 @@
import type_keyid from '../type/keyid';
import type_mpi from '../type/mpi';
import config from '../config';
import crypto from '../crypto';
import enums from '../enums';
import util from '../util';
@ -52,7 +54,7 @@ function PublicKey(date=new Date()) {
* Packet version
* @type {Integer}
*/
this.version = 4;
this.version = config.aead_protect && config.aead_protect_version === 4 ? 5 : 4;
/**
* Key creation date.
* @type {Date}
@ -88,10 +90,10 @@ function PublicKey(date=new Date()) {
*/
PublicKey.prototype.read = function (bytes) {
let pos = 0;
// A one-octet version number (3 or 4).
// A one-octet version number (3, 4 or 5).
this.version = bytes[pos++];
if (this.version === 3 || this.version === 4) {
if (this.version === 3 || this.version === 4 || this.version === 5) {
// - A four-octet number denoting the time that the key was created.
this.created = util.readDate(bytes.subarray(pos, pos + 4));
pos += 4;
@ -106,20 +108,25 @@ PublicKey.prototype.read = function (bytes) {
// - A one-octet number denoting the public-key algorithm of this key.
this.algorithm = enums.read(enums.publicKey, bytes[pos++]);
const algo = enums.write(enums.publicKey, this.algorithm);
if (this.version === 5) {
// - A four-octet scalar octet count for the following key material.
pos += 4;
}
// - A series of values comprising the key material. This is
// algorithm-specific and described in section XXXX.
const types = crypto.getPubKeyParamTypes(algo);
this.params = crypto.constructParams(types);
const b = bytes.subarray(pos, bytes.length);
let p = 0;
for (let i = 0; i < types.length && p < b.length; i++) {
p += this.params[i].read(b.subarray(p, b.length));
if (p > b.length) {
throw new Error('Error reading MPI @:' + p);
for (let i = 0; i < types.length && pos < bytes.length; i++) {
pos += this.params[i].read(bytes.subarray(pos, bytes.length));
if (pos > bytes.length) {
throw new Error('Error reading MPI @:' + pos);
}
}
return p + 6;
return pos;
}
throw new Error('Version ' + this.version + ' of the key packet is unsupported.');
};
@ -143,14 +150,18 @@ PublicKey.prototype.write = function () {
if (this.version === 3) {
arr.push(util.writeNumber(this.expirationTimeV3, 2));
}
// Algorithm-specific params
// A one-octet number denoting the public-key algorithm of this key
const algo = enums.write(enums.publicKey, this.algorithm);
const paramCount = crypto.getPubKeyParamTypes(algo).length;
arr.push(new Uint8Array([algo]));
for (let i = 0; i < paramCount; i++) {
arr.push(this.params[i].write());
}
const paramCount = crypto.getPubKeyParamTypes(algo).length;
const params = util.concatUint8Array(this.params.slice(0, paramCount).map(param => param.write()));
if (this.version === 5) {
// A four-octet scalar octet count for the following key material
arr.push(util.writeNumber(params.length, 4));
}
// Algorithm-specific params
arr.push(params);
return util.concatUint8Array(arr);
};
@ -178,7 +189,9 @@ PublicKey.prototype.getKeyId = function () {
return this.keyid;
}
this.keyid = new type_keyid();
if (this.version === 4) {
if (this.version === 5) {
this.keyid.read(util.hex_to_Uint8Array(this.getFingerprint()).subarray(0, 8));
} else if (this.version === 4) {
this.keyid.read(util.hex_to_Uint8Array(this.getFingerprint()).subarray(12, 20));
} else if (this.version === 3) {
const arr = this.params[0].write();
@ -189,28 +202,40 @@ PublicKey.prototype.getKeyId = function () {
/**
* Calculates the fingerprint of the key
* @returns {String} A string containing the fingerprint in lowercase hex
* @returns {Uint8Array} A Uint8Array containing the fingerprint
*/
PublicKey.prototype.getFingerprint = function () {
PublicKey.prototype.getFingerprintBytes = function () {
if (this.fingerprint) {
return this.fingerprint;
}
let toHash = '';
if (this.version === 4) {
let toHash;
if (this.version === 5) {
const bytes = this.writePublicKey();
toHash = util.concatUint8Array([new Uint8Array([0x9A]), util.writeNumber(bytes.length, 4), bytes]);
this.fingerprint = crypto.hash.sha256(toHash);
} else if (this.version === 4) {
toHash = this.writeOld();
this.fingerprint = util.Uint8Array_to_str(crypto.hash.sha1(toHash));
this.fingerprint = crypto.hash.sha1(toHash);
} else if (this.version === 3) {
const algo = enums.write(enums.publicKey, this.algorithm);
const paramCount = crypto.getPubKeyParamTypes(algo).length;
toHash = '';
for (let i = 0; i < paramCount; i++) {
toHash += this.params[i].toString();
}
this.fingerprint = util.Uint8Array_to_str(crypto.hash.md5(util.str_to_Uint8Array(toHash)));
this.fingerprint = crypto.hash.md5(util.str_to_Uint8Array(toHash));
}
this.fingerprint = util.str_to_hex(this.fingerprint);
return this.fingerprint;
};
/**
* Calculates the fingerprint of the key
* @returns {String} A string containing the fingerprint in lowercase hex
*/
PublicKey.prototype.getFingerprint = function () {
return util.Uint8Array_to_hex(this.getFingerprintBytes());
};
/**
* Returns algorithm information
* @returns {Promise<Object>} An object of the form {algorithm: String, bits:int, curve:String}

View File

@ -122,7 +122,7 @@ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) {
}
this.encrypted = await crypto.publicKeyEncrypt(
algo, key.params, toEncrypt, key.fingerprint);
algo, key.params, toEncrypt, key.getFingerprintBytes());
return true;
};
@ -138,7 +138,7 @@ PublicKeyEncryptedSessionKey.prototype.encrypt = async function (key) {
PublicKeyEncryptedSessionKey.prototype.decrypt = async function (key) {
const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm);
const result = await crypto.publicKeyDecrypt(
algo, key.params, this.encrypted, key.fingerprint);
algo, key.params, this.encrypted, key.getFingerprintBytes());
let checksum;
let decoded;

View File

@ -78,15 +78,17 @@ function get_hash_fn(hash) {
// Helper function
function parse_cleartext_params(hash_algorithm, cleartext, algorithm) {
const hashlen = get_hash_len(hash_algorithm);
const hashfn = get_hash_fn(hash_algorithm);
if (hash_algorithm) {
const hashlen = get_hash_len(hash_algorithm);
const hashfn = get_hash_fn(hash_algorithm);
const hashtext = util.Uint8Array_to_str(cleartext.subarray(cleartext.length - hashlen, cleartext.length));
cleartext = cleartext.subarray(0, cleartext.length - hashlen);
const hash = util.Uint8Array_to_str(hashfn(cleartext));
const hashtext = util.Uint8Array_to_str(cleartext.subarray(cleartext.length - hashlen, cleartext.length));
cleartext = cleartext.subarray(0, cleartext.length - hashlen);
const hash = util.Uint8Array_to_str(hashfn(cleartext));
if (hash !== hashtext) {
return new Error("Incorrect key passphrase");
if (hash !== hashtext) {
throw new Error("Incorrect key passphrase");
}
}
const algo = enums.write(enums.publicKey, algorithm);
@ -115,9 +117,13 @@ function write_cleartext_params(hash_algorithm, algorithm, params) {
const bytes = util.concatUint8Array(arr);
const hash = get_hash_fn(hash_algorithm)(bytes);
if (hash_algorithm) {
const hash = get_hash_fn(hash_algorithm)(bytes);
return util.concatUint8Array([bytes, hash]);
return util.concatUint8Array([bytes, hash]);
}
return bytes;
}
@ -125,7 +131,7 @@ function write_cleartext_params(hash_algorithm, algorithm, params) {
/**
* Internal parser for private keys as specified in
* {@link https://tools.ietf.org/html/rfc4880#section-5.5.3|RFC 4880 section 5.5.3}
* {@link https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-04#section-5.5.3|RFC4880bis-04 section 5.5.3}
* @param {String} bytes Input string to read the packet from
*/
SecretKey.prototype.read = function (bytes) {
@ -148,9 +154,6 @@ SecretKey.prototype.read = function (bytes) {
// key data. These algorithm-specific fields are as described
// below.
const privParams = parse_cleartext_params('mod', bytes.subarray(1, bytes.length), this.algorithm);
if (privParams instanceof Error) {
throw privParams;
}
this.params = this.params.concat(privParams);
this.isDecrypted = true;
}
@ -194,15 +197,30 @@ SecretKey.prototype.encrypt = async function (passphrase) {
const s2k = new type_s2k();
s2k.salt = await crypto.random.getRandomBytes(8);
const symmetric = 'aes256';
const cleartext = write_cleartext_params('sha1', this.algorithm, this.params);
const hash = this.version === 5 ? null : 'sha1';
const cleartext = write_cleartext_params(hash, this.algorithm, this.params);
const key = produceEncryptionKey(s2k, passphrase, symmetric);
const blockLen = crypto.cipher[symmetric].blockSize;
const iv = await crypto.random.getRandomBytes(blockLen);
const arr = [new Uint8Array([254, enums.write(enums.symmetric, symmetric)])];
arr.push(s2k.write());
arr.push(iv);
arr.push(crypto.cfb.normalEncrypt(symmetric, key, cleartext, iv));
let arr;
if (this.version === 5) {
const aead = 'eax';
const optionalFields = util.concatUint8Array([new Uint8Array([enums.write(enums.symmetric, symmetric), enums.write(enums.aead, aead)]), s2k.write(), iv]);
arr = [new Uint8Array([253, optionalFields.length])];
arr.push(optionalFields);
const mode = crypto[aead];
const modeInstance = await mode(symmetric, key);
const encrypted = await modeInstance.encrypt(cleartext, iv.subarray(0, mode.ivLength), new Uint8Array());
arr.push(util.writeNumber(encrypted.length, 4));
arr.push(encrypted);
} else {
arr = [new Uint8Array([254, enums.write(enums.symmetric, symmetric)])];
arr.push(s2k.write());
arr.push(iv);
arr.push(crypto.cfb.normalEncrypt(symmetric, key, cleartext, iv));
}
this.encrypted = util.concatUint8Array(arr);
return true;
@ -230,17 +248,31 @@ SecretKey.prototype.decrypt = async function (passphrase) {
let i = 0;
let symmetric;
let aead;
let key;
const s2k_usage = this.encrypted[i++];
// - [Optional] If string-to-key usage octet was 255 or 254, a one-
// octet symmetric encryption algorithm.
if (s2k_usage === 255 || s2k_usage === 254) {
// - Only for a version 5 packet, a one-octet scalar octet count of
// the next 4 optional fields.
if (this.version === 5) {
i++;
}
// - [Optional] If string-to-key usage octet was 255, 254, or 253, a
// one-octet symmetric encryption algorithm.
if (s2k_usage === 255 || s2k_usage === 254 || s2k_usage === 253) {
symmetric = this.encrypted[i++];
symmetric = enums.read(enums.symmetric, symmetric);
// - [Optional] If string-to-key usage octet was 255 or 254, a
// - [Optional] If string-to-key usage octet was 253, a one-octet
// AEAD algorithm.
if (s2k_usage === 253) {
aead = this.encrypted[i++];
aead = enums.read(enums.aead, aead);
}
// - [Optional] If string-to-key usage octet was 255, 254, or 253, a
// string-to-key specifier. The length of the string-to-key
// specifier is implied by its type, as described above.
const s2k = new type_s2k();
@ -263,16 +295,33 @@ SecretKey.prototype.decrypt = async function (passphrase) {
i += iv.length;
// - Only for a version 5 packet, a four-octet scalar octet count for
// the following key material.
if (this.version === 5) {
i += 4;
}
const ciphertext = this.encrypted.subarray(i, this.encrypted.length);
const cleartext = crypto.cfb.normalDecrypt(symmetric, key, ciphertext, iv);
const hash = s2k_usage === 254 ?
'sha1' :
let cleartext;
if (aead) {
const mode = crypto[aead];
try {
const modeInstance = await mode(symmetric, key);
cleartext = await modeInstance.decrypt(ciphertext, iv.subarray(0, mode.ivLength), new Uint8Array());
} catch(err) {
if (err.message === 'Authentication tag mismatch') {
throw new Error('Incorrect key passphrase: ' + err.message);
}
}
} else {
cleartext = crypto.cfb.normalDecrypt(symmetric, key, ciphertext, iv);
}
const hash =
s2k_usage === 253 ? null :
s2k_usage === 254 ? 'sha1' :
'mod';
const privParams = parse_cleartext_params(hash, cleartext, this.algorithm);
if (privParams instanceof Error) {
throw privParams;
}
this.params = this.params.concat(privParams);
this.isDecrypted = true;
this.encrypted = null;

View File

@ -84,6 +84,9 @@ function Signature(date=new Date()) {
this.signatureTargetHashAlgorithm = null;
this.signatureTargetHash = null;
this.embeddedSignature = null;
this.issuerKeyVersion = null;
this.issuerFingerprint = null;
this.preferredAeadAlgorithms = null;
this.verified = null;
this.revoked = null;
@ -222,6 +225,13 @@ Signature.prototype.sign = async function (key, data) {
const arr = [new Uint8Array([4, signatureType, publicKeyAlgorithm, hashAlgorithm])];
if (key.version === 5) {
// We could also generate this subpacket for version 4 keys, but for
// now we don't.
this.issuerKeyVersion = key.version;
this.issuerFingerprint = key.getFingerprintBytes();
}
this.issuerKeyId = key.getKeyId();
// Add hashed subpackets
@ -292,7 +302,9 @@ Signature.prototype.write_all_sub_packets = function () {
bytes = util.concatUint8Array([bytes, this.revocationKeyFingerprint]);
arr.push(write_sub_packet(sub.revocation_key, bytes));
}
if (!this.issuerKeyId.isNull()) {
if (!this.issuerKeyId.isNull() && this.issuerKeyVersion !== 5) {
// If the version of [the] key is greater than 4, this subpacket
// MUST NOT be included in the signature.
arr.push(write_sub_packet(sub.issuer, this.issuerKeyId.write()));
}
if (this.notation !== null) {
@ -355,6 +367,15 @@ Signature.prototype.write_all_sub_packets = function () {
if (this.embeddedSignature !== null) {
arr.push(write_sub_packet(sub.embedded_signature, this.embeddedSignature.write()));
}
if (this.issuerFingerprint !== null) {
bytes = [new Uint8Array([this.issuerKeyVersion]), this.issuerFingerprint];
bytes = util.concatUint8Array(bytes);
arr.push(write_sub_packet(sub.issuer_fingerprint, bytes));
}
if (this.preferredAeadAlgorithms !== null) {
bytes = util.str_to_Uint8Array(util.Uint8Array_to_str(this.preferredAeadAlgorithms));
arr.push(write_sub_packet(sub.preferred_aead_algorithms, bytes));
}
const result = util.concatUint8Array(arr);
const length = util.writeNumber(result.length, 2);
@ -531,6 +552,20 @@ Signature.prototype.read_sub_packet = function (bytes) {
this.embeddedSignature = new Signature();
this.embeddedSignature.read(bytes.subarray(mypos, bytes.length));
break;
case 33:
// Issuer Fingerprint
this.issuerKeyVersion = bytes[mypos++];
this.issuerFingerprint = bytes.subarray(mypos, bytes.length);
if (this.issuerKeyVersion === 5) {
this.issuerKeyId.read(this.issuerFingerprint);
} else {
this.issuerKeyId.read(this.issuerFingerprint.subarray(-8));
}
break;
case 34:
// Preferred AEAD Algorithms
read_array.call(this, 'preferredAeadAlgorithms', bytes.subarray(mypos, bytes.length));
break;
default:
util.print_debug("Unknown signature subpacket type " + type + " @:" + mypos);
}
@ -542,9 +577,15 @@ Signature.prototype.toSign = function (type, data) {
switch (type) {
case t.binary:
case t.text:
return data.getBytes();
case t.text: {
let text = data.getText();
// normalize EOL to \r\n
text = util.canonicalizeEOL(text);
// encode UTF8
return util.str_to_Uint8Array(util.encode_utf8(text));
}
case t.standalone:
return new Uint8Array(0);

View File

@ -16,17 +16,18 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
/**
* @requires config
* @requires crypto
* @requires enums
* @requires util
*/
import config from '../config';
import crypto from '../crypto';
import enums from '../enums';
import util from '../util';
const VERSION = 1; // A one-octet version number of the data packet.
const IV_LEN = crypto.gcm.ivLength; // currently only AES-GCM is supported
/**
* Implementation of the Symmetrically Encrypted Authenticated Encryption with
@ -40,6 +41,10 @@ const IV_LEN = crypto.gcm.ivLength; // currently only AES-GCM is supported
function SymEncryptedAEADProtected() {
this.tag = enums.packet.symEncryptedAEADProtected;
this.version = VERSION;
this.cipherAlgo = null;
this.aeadAlgorithm = 'eax';
this.aeadAlgo = null;
this.chunkSizeByte = null;
this.iv = null;
this.encrypted = null;
this.packets = null;
@ -56,8 +61,16 @@ SymEncryptedAEADProtected.prototype.read = function (bytes) {
throw new Error('Invalid packet version.');
}
offset++;
this.iv = bytes.subarray(offset, IV_LEN + offset);
offset += IV_LEN;
if (config.aead_protect_version === 4) {
this.cipherAlgo = bytes[offset++];
this.aeadAlgo = bytes[offset++];
this.chunkSizeByte = bytes[offset++];
} else {
this.aeadAlgo = enums.aead.experimental_gcm;
}
const mode = crypto[enums.read(enums.aead, this.aeadAlgo)];
this.iv = bytes.subarray(offset, mode.ivLength + offset);
offset += mode.ivLength;
this.encrypted = bytes.subarray(offset, bytes.length);
};
@ -66,6 +79,9 @@ SymEncryptedAEADProtected.prototype.read = function (bytes) {
* @returns {Uint8Array} The encrypted payload
*/
SymEncryptedAEADProtected.prototype.write = function () {
if (config.aead_protect_version === 4) {
return util.concatUint8Array([new Uint8Array([this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte]), this.iv, this.encrypted]);
}
return util.concatUint8Array([new Uint8Array([this.version]), this.iv, this.encrypted]);
};
@ -77,7 +93,15 @@ SymEncryptedAEADProtected.prototype.write = function () {
* @async
*/
SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorithm, key) {
this.packets.read(await crypto.gcm.decrypt(sessionKeyAlgorithm, this.encrypted, key, this.iv));
const mode = crypto[enums.read(enums.aead, this.aeadAlgo)];
if (config.aead_protect_version === 4) {
const data = this.encrypted.subarray(0, -mode.tagLength);
const authTag = this.encrypted.subarray(-mode.tagLength);
this.packets.read(await this.crypt('decrypt', key, data, authTag));
} else {
this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm);
this.packets.read(await this.crypt('decrypt', key, this.encrypted));
}
return true;
};
@ -89,7 +113,56 @@ SymEncryptedAEADProtected.prototype.decrypt = async function (sessionKeyAlgorith
* @async
*/
SymEncryptedAEADProtected.prototype.encrypt = async function (sessionKeyAlgorithm, key) {
this.iv = await crypto.random.getRandomBytes(IV_LEN); // generate new random IV
this.encrypted = await crypto.gcm.encrypt(sessionKeyAlgorithm, this.packets.write(), key, this.iv);
return true;
this.cipherAlgo = enums.write(enums.symmetric, sessionKeyAlgorithm);
this.aeadAlgo = config.aead_protect_version === 4 ? enums.write(enums.aead, this.aeadAlgorithm) : enums.aead.experimental_gcm;
const mode = crypto[enums.read(enums.aead, this.aeadAlgo)];
this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV
this.chunkSizeByte = config.aead_chunk_size_byte;
const data = this.packets.write();
this.encrypted = await this.crypt('encrypt', key, data, data.subarray(0, 0));
};
/**
* En/decrypt the payload.
* @param {encrypt|decrypt} fn Whether to encrypt or decrypt
* @param {Uint8Array} key The session key used to en/decrypt the payload
* @param {Uint8Array} data The data to en/decrypt
* @param {Uint8Array} finalChunk For encryption: empty final chunk; for decryption: final authentication tag
* @returns {Promise<Uint8Array>}
* @async
*/
SymEncryptedAEADProtected.prototype.crypt = async function (fn, key, data, finalChunk) {
const cipher = enums.read(enums.symmetric, this.cipherAlgo);
const mode = crypto[enums.read(enums.aead, this.aeadAlgo)];
const modeInstance = await mode(cipher, key);
if (config.aead_protect_version === 4) {
const tagLengthIfDecrypting = fn === 'decrypt' ? mode.tagLength : 0;
const chunkSize = 2 ** (this.chunkSizeByte + 6) + tagLengthIfDecrypting; // ((uint64_t)1 << (c + 6))
const adataBuffer = new ArrayBuffer(21);
const adataArray = new Uint8Array(adataBuffer, 0, 13);
const adataTagArray = new Uint8Array(adataBuffer);
const adataView = new DataView(adataBuffer);
const chunkIndexArray = new Uint8Array(adataBuffer, 5, 8);
adataArray.set([0xC0 | this.tag, this.version, this.cipherAlgo, this.aeadAlgo, this.chunkSizeByte], 0);
adataView.setInt32(13 + 4, data.length - tagLengthIfDecrypting * Math.ceil(data.length / chunkSize)); // Should be setInt64(13, ...)
const cryptedPromises = [];
for (let chunkIndex = 0; chunkIndex === 0 || data.length;) {
cryptedPromises.push(
modeInstance[fn](data.subarray(0, chunkSize), mode.getNonce(this.iv, chunkIndexArray), adataArray)
);
// We take a chunk of data, en/decrypt it, and shift `data` to the
// next chunk.
data = data.subarray(chunkSize);
adataView.setInt32(5 + 4, ++chunkIndex); // Should be setInt64(5, ...)
}
// After the final chunk, we either encrypt a final, empty data
// chunk to get the final authentication tag or validate that final
// authentication tag.
cryptedPromises.push(
modeInstance[fn](finalChunk, mode.getNonce(this.iv, chunkIndexArray), adataTagArray)
);
return util.concatUint8Array(await Promise.all(cryptedPromises));
} else {
return modeInstance[fn](data, this.iv);
}
};

View File

@ -17,12 +17,14 @@
/**
* @requires type/s2k
* @requires config
* @requires crypto
* @requires enums
* @requires util
*/
import type_s2k from '../type/s2k';
import config from '../config';
import crypto from '../crypto';
import enums from '../enums';
import util from '../util';
@ -47,12 +49,14 @@ import util from '../util';
*/
function SymEncryptedSessionKey() {
this.tag = enums.packet.symEncryptedSessionKey;
this.version = 4;
this.version = config.aead_protect && config.aead_protect_version === 4 ? 5 : 4;
this.sessionKey = null;
this.sessionKeyEncryptionAlgorithm = null;
this.sessionKeyAlgorithm = 'aes256';
this.aeadAlgorithm = enums.read(enums.aead, config.aead_mode);
this.encrypted = null;
this.s2k = null;
this.iv = null;
}
/**
@ -66,22 +70,35 @@ function SymEncryptedSessionKey() {
* @returns {module:packet.SymEncryptedSessionKey} Object representation
*/
SymEncryptedSessionKey.prototype.read = function(bytes) {
let offset = 0;
// A one-octet version number. The only currently defined version is 4.
[this.version] = bytes;
this.version = bytes[offset++];
// A one-octet number describing the symmetric algorithm used.
const algo = enums.read(enums.symmetric, bytes[1]);
const algo = enums.read(enums.symmetric, bytes[offset++]);
if (this.version === 5) {
// A one-octet AEAD algorithm.
this.aeadAlgorithm = enums.read(enums.aead, bytes[offset++]);
}
// A string-to-key (S2K) specifier, length as defined above.
this.s2k = new type_s2k();
const s2klength = this.s2k.read(bytes.subarray(2, bytes.length));
offset += this.s2k.read(bytes.subarray(offset, bytes.length));
// Optionally, the encrypted session key itself, which is decrypted
// with the string-to-key object.
const done = s2klength + 2;
if (this.version === 5) {
const mode = crypto[this.aeadAlgorithm];
if (done < bytes.length) {
this.encrypted = bytes.subarray(done, bytes.length);
// A starting initialization vector of size specified by the AEAD
// algorithm.
this.iv = bytes.subarray(offset, offset += mode.ivLength);
}
// The encrypted session key itself, which is decrypted with the
// string-to-key object. This is optional in version 4.
if (this.version === 5 || offset < bytes.length) {
this.encrypted = bytes.subarray(offset, bytes.length);
this.sessionKeyEncryptionAlgorithm = algo;
} else {
this.sessionKeyAlgorithm = algo;
@ -93,11 +110,18 @@ SymEncryptedSessionKey.prototype.write = function() {
this.sessionKeyAlgorithm :
this.sessionKeyEncryptionAlgorithm;
let bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo)]), this.s2k.write()]);
let bytes;
if (this.encrypted !== null) {
bytes = util.concatUint8Array([bytes, this.encrypted]);
if (this.version === 5) {
bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo), enums.write(enums.aead, this.aeadAlgorithm)]), this.s2k.write(), this.iv, this.encrypted]);
} else {
bytes = util.concatUint8Array([new Uint8Array([this.version, enums.write(enums.symmetric, algo)]), this.s2k.write()]);
if (this.encrypted !== null) {
bytes = util.concatUint8Array([bytes, this.encrypted]);
}
}
return bytes;
};
@ -115,14 +139,20 @@ SymEncryptedSessionKey.prototype.decrypt = async function(passphrase) {
const length = crypto.cipher[algo].keySize;
const key = this.s2k.produce_key(passphrase, length);
if (this.encrypted === null) {
this.sessionKey = key;
} else {
if (this.version === 5) {
const mode = crypto[this.aeadAlgorithm];
const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]);
const modeInstance = await mode(algo, key);
this.sessionKey = await modeInstance.decrypt(this.encrypted, this.iv, adata);
} else if (this.encrypted !== null) {
const decrypted = crypto.cfb.normalDecrypt(algo, key, this.encrypted, null);
this.sessionKeyAlgorithm = enums.read(enums.symmetric, decrypted[0]);
this.sessionKey = decrypted.subarray(1, decrypted.length);
} else {
this.sessionKey = key;
}
return true;
};
@ -145,14 +175,22 @@ SymEncryptedSessionKey.prototype.encrypt = async function(passphrase) {
const length = crypto.cipher[algo].keySize;
const key = this.s2k.produce_key(passphrase, length);
const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]);
if (this.sessionKey === null) {
this.sessionKey = await crypto.generateSessionKey(this.sessionKeyAlgorithm);
}
const private_key = util.concatUint8Array([algo_enum, this.sessionKey]);
this.encrypted = crypto.cfb.normalEncrypt(algo, key, private_key, null);
if (this.version === 5) {
const mode = crypto[this.aeadAlgorithm];
this.iv = await crypto.random.getRandomBytes(mode.ivLength); // generate new random IV
const adata = new Uint8Array([0xC0 | this.tag, this.version, enums.write(enums.symmetric, this.sessionKeyEncryptionAlgorithm), enums.write(enums.aead, this.aeadAlgorithm)]);
const modeInstance = await mode(algo, key);
this.encrypted = await modeInstance.encrypt(this.sessionKey, this.iv, adata);
} else {
const algo_enum = new Uint8Array([enums.write(enums.symmetric, this.sessionKeyAlgorithm)]);
const private_key = util.concatUint8Array([algo_enum, this.sessionKey]);
this.encrypted = crypto.cfb.normalEncrypt(algo, key, private_key, null);
}
return true;
};

View File

@ -24,15 +24,17 @@
* places, currently: to encrypt the secret part of private keys in the
* private keyring, and to convert passphrases to encryption keys for
* symmetrically encrypted messages.
* @requires config
* @requires crypto
* @requires enums
* @requires util
* @module type/s2k
*/
import config from '../config';
import crypto from '../crypto';
import enums from '../enums.js';
import util from '../util.js';
import crypto from '../crypto';
/**
* @constructor
@ -42,7 +44,8 @@ function S2K() {
this.algorithm = 'sha256';
/** @type {module:enums.s2k} */
this.type = 'iterated';
this.c = 96;
/** @type {Integer} */
this.c = config.s2k_iteration_count_byte;
/** Eight bytes of salt in a binary string.
* @type {String}
*/

View File

@ -362,6 +362,20 @@ export default {
}
},
/**
* Helper function to print a debug message. Debug
* messages are only printed if
* @link module:config/config.debug is set to true.
* Different than print_debug because will call Uint8Array_to_hex iff necessary.
* @param {String} str String of the debug message
*/
print_debug_hexarray_dump: function (str, arrToHex) {
if (config.debug) {
str += ': ' + util.Uint8Array_to_hex(arrToHex);
console.log(str);
}
},
/**
* Helper function to print a debug message. Debug
* messages are only printed if
@ -376,14 +390,25 @@ export default {
}
},
// TODO rewrite getLeftNBits to work with Uint8Arrays
getLeftNBits: function (string, bitcount) {
/**
* Helper function to print a debug error. Debug
* messages are only printed if
* @link module:config/config.debug is set to true.
* @param {String} str String of the debug message
*/
print_debug_error: function (error) {
if (config.debug) {
console.error(error);
}
},
getLeftNBits: function (array, bitcount) {
const rest = bitcount % 8;
if (rest === 0) {
return string.substring(0, bitcount / 8);
return array.subarray(0, bitcount / 8);
}
const bytes = (bitcount - rest) / 8 + 1;
const result = string.substring(0, bytes);
const result = array.subarray(0, bytes);
return util.shiftRight(result, 8 - rest); // +String.fromCharCode(string.charCodeAt(bytes -1) << (8-rest) & 0xFF);
},
@ -419,25 +444,41 @@ export default {
},
/**
* Shifting a string to n bits right
* @param {String} value The string to shift
* @param {Integer} bitcount Amount of bits to shift (MUST be smaller
* than 9)
* @returns {String} Resulting string.
* If S[1] == 0, then double(S) == (S[2..128] || 0);
* otherwise, double(S) == (S[2..128] || 0) xor
* (zeros(120) || 10000111).
*
* Both OCB and EAX (through CMAC) require this function to be constant-time.
*
* @param {Uint8Array} data
*/
shiftRight: function (value, bitcount) {
const temp = util.str_to_Uint8Array(value);
if (bitcount % 8 !== 0) {
for (let i = temp.length - 1; i >= 0; i--) {
temp[i] >>= bitcount % 8;
double: function(data) {
const double = new Uint8Array(data.length);
const last = data.length - 1;
for (let i = 0; i < last; i++) {
double[i] = (data[i] << 1) ^ (data[i + 1] >> 7);
}
double[last] = (data[last] << 1) ^ ((data[0] >> 7) * 0x87);
return double;
},
/**
* Shift a Uint8Array to the right by n bits
* @param {Uint8Array} array The array to shift
* @param {Integer} bits Amount of bits to shift (MUST be smaller
* than 8)
* @returns {String} Resulting array.
*/
shiftRight: function (array, bits) {
if (bits) {
for (let i = array.length - 1; i >= 0; i--) {
array[i] >>= bits;
if (i > 0) {
temp[i] |= (temp[i - 1] << (8 - (bitcount % 8))) & 0xFF;
array[i] |= (array[i - 1] << (8 - bits));
}
}
} else {
return value;
}
return util.Uint8Array_to_str(temp);
return array;
},
/**
@ -533,5 +574,26 @@ export default {
return false;
}
return /</.test(data) && />$/.test(data);
},
/**
* Normalize line endings to \r\n
*/
canonicalizeEOL: function(text) {
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r\n");
},
/**
* Convert line endings from canonicalized \r\n to native \n
*/
nativeEOL: function(text) {
return text.replace(/\r\n/g, "\n");
},
/**
* Remove trailing spaces and tabs from each line
*/
removeTrailingSpaces: function(text) {
return text.replace(/[ \t]+$/mg, "");
}
};

View File

@ -7,11 +7,13 @@ const { expect } = chai;
describe('AES Rijndael cipher test with test vectors from ecb_tbl.txt', function() {
function test_aes(input, key, output) {
const aes = new openpgp.crypto.cipher.aes128(key);
const aes = new openpgp.crypto.cipher.aes128(new Uint8Array(key));
const result = util.Uint8Array_to_str(aes.encrypt(new Uint8Array(input)));
const encrypted = aes.encrypt(new Uint8Array(input));
expect(encrypted).to.deep.equal(new Uint8Array(output));
return util.str_to_hex(result) === util.str_to_hex(util.Uint8Array_to_str(output));
const decrypted = aes.decrypt(new Uint8Array(output));
expect(decrypted).to.deep.equal(new Uint8Array(input));
}
const testvectors128 = [[[0x00,0x01,0x02,0x03,0x05,0x06,0x07,0x08,0x0A,0x0B,0x0C,0x0D,0x0F,0x10,0x11,0x12],[0x50,0x68,0x12,0xA4,0x5F,0x08,0xC8,0x89,0xB9,0x7F,0x59,0x80,0x03,0x8B,0x83,0x59],[0xD8,0xF5,0x32,0x53,0x82,0x89,0xEF,0x7D,0x06,0xB5,0x06,0xA4,0xFD,0x5B,0xE9,0xC9]],
@ -64,30 +66,21 @@ describe('AES Rijndael cipher test with test vectors from ecb_tbl.txt', function
it('128 bit key', function (done) {
for (let i = 0; i < testvectors128.length; i++) {
const res = test_aes(testvectors128[i][1],testvectors128[i][0],testvectors128[i][2]);
expect(res, 'block ' + util.Uint8Array_to_hex(testvectors128[i][1]) +
' and key '+util.Uint8Array_to_hex(testvectors128[i][0]) +
' should be '+util.Uint8Array_to_hex(testvectors128[i][2])).to.be.true;
test_aes(testvectors128[i][1],testvectors128[i][0],testvectors128[i][2]);
}
done();
});
it('192 bit key', function (done) {
for (let i = 0; i < testvectors192.length; i++) {
const res = test_aes(testvectors192[i][1],testvectors192[i][0],testvectors192[i][2]);
expect(res, 'block ' + util.Uint8Array_to_hex(testvectors192[i][1]) +
' and key ' + util.Uint8Array_to_hex(testvectors192[i][0])+
' should be ' + util.Uint8Array_to_hex(testvectors192[i][2])).to.be.true;
test_aes(testvectors192[i][1],testvectors192[i][0],testvectors192[i][2]);
}
done();
});
it('256 bit key', function (done) {
for (let i = 0; i < testvectors256.length; i++) {
const res = test_aes(testvectors256[i][1],testvectors256[i][0],testvectors256[i][2]);
expect(res, 'block ' + util.Uint8Array_to_hex(testvectors256[i][1]) +
' and key ' + util.Uint8Array_to_hex(testvectors256[i][0]) +
' should be ' + util.Uint8Array_to_hex(testvectors256[i][2])).to.be.true;
test_aes(testvectors256[i][1],testvectors256[i][0],testvectors256[i][2]);
}
done();
});

View File

@ -304,21 +304,22 @@ describe('API functional testing', function() {
});
}
function testAESGCM(plaintext) {
function testAESGCM(plaintext, nativeDecrypt) {
symmAlgos.forEach(function(algo) {
if(algo.substr(0,3) === 'aes') {
it(algo, async function() {
const key = await crypto.generateSessionKey(algo);
const iv = await crypto.random.getRandomBytes(crypto.gcm.ivLength);
let modeInstance = await crypto.gcm(algo, key);
return crypto.gcm.encrypt(
algo, util.str_to_Uint8Array(plaintext), key, iv
).then(function(ciphertext) {
return crypto.gcm.decrypt(algo, ciphertext, key, iv);
}).then(function(decrypted) {
const decryptedStr = util.Uint8Array_to_str(decrypted);
expect(decryptedStr).to.equal(plaintext);
});
const ciphertext = await modeInstance.encrypt(util.str_to_Uint8Array(plaintext), iv);
openpgp.config.use_native = nativeDecrypt;
modeInstance = await crypto.gcm(algo, key);
const decrypted = await modeInstance.decrypt(util.str_to_Uint8Array(util.Uint8Array_to_str(ciphertext)), iv);
const decryptedStr = util.Uint8Array_to_str(decrypted);
expect(decryptedStr).to.equal(plaintext);
});
}
});
@ -355,7 +356,7 @@ describe('API functional testing', function() {
openpgp.config.use_native = use_nativeVal;
});
testAESGCM("12345678901234567890123456789012345678901234567890");
testAESGCM("12345678901234567890123456789012345678901234567890", true);
});
describe('Symmetric AES-GCM (asm.js fallback)', function() {
@ -368,7 +369,20 @@ describe('API functional testing', function() {
openpgp.config.use_native = use_nativeVal;
});
testAESGCM("12345678901234567890123456789012345678901234567890");
testAESGCM("12345678901234567890123456789012345678901234567890", false);
});
describe('Symmetric AES-GCM (native encrypt, asm.js decrypt)', function() {
let use_nativeVal;
beforeEach(function() {
use_nativeVal = openpgp.config.use_native;
openpgp.config.use_native = true;
});
afterEach(function() {
openpgp.config.use_native = use_nativeVal;
});
testAESGCM("12345678901234567890123456789012345678901234567890", false);
});
it('Asymmetric using RSA with eme_pkcs1 padding', function () {

150
test/crypto/eax.js Normal file
View File

@ -0,0 +1,150 @@
// Modified by ProtonTech AG
// Adapted from https://github.com/artjomb/cryptojs-extension/blob/8c61d159/test/eax.js
const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp');
const chai = require('chai');
chai.use(require('chai-as-promised'));
const expect = chai.expect;
function testAESEAX() {
it('Passes all test vectors', async function() {
var vectors = [
// From http://www.cs.ucdavis.edu/~rogaway/papers/eax.pdf ...
{
msg: "",
key: "233952DEE4D5ED5F9B9C6D6FF80FF478",
nonce: "62EC67F9C3A4A407FCB2A8C49031A8B3",
header: "6BFB914FD07EAE6B",
ct: "E037830E8389F27B025A2D6527E79D01"
},
{
msg: "F7FB",
key: "91945D3F4DCBEE0BF45EF52255F095A4",
nonce: "BECAF043B0A23D843194BA972C66DEBD",
header: "FA3BFD4806EB53FA",
ct: "19DD5C4C9331049D0BDAB0277408F67967E5"
},
{
msg: "1A47CB4933",
key: "01F74AD64077F2E704C0F60ADA3DD523",
nonce: "70C3DB4F0D26368400A10ED05D2BFF5E",
header: "234A3463C1264AC6",
ct: "D851D5BAE03A59F238A23E39199DC9266626C40F80"
},
{
msg: "481C9E39B1",
key: "D07CF6CBB7F313BDDE66B727AFD3C5E8",
nonce: "8408DFFF3C1A2B1292DC199E46B7D617",
header: "33CCE2EABFF5A79D",
ct: "632A9D131AD4C168A4225D8E1FF755939974A7BEDE"
},
{
msg: "40D0C07DA5E4",
key: "35B6D0580005BBC12B0587124557D2C2",
nonce: "FDB6B06676EEDC5C61D74276E1F8E816",
header: "AEB96EAEBE2970E9",
ct: "071DFE16C675CB0677E536F73AFE6A14B74EE49844DD"
},
{
msg: "4DE3B35C3FC039245BD1FB7D",
key: "BD8E6E11475E60B268784C38C62FEB22",
nonce: "6EAC5C93072D8E8513F750935E46DA1B",
header: "D4482D1CA78DCE0F",
ct: "835BB4F15D743E350E728414ABB8644FD6CCB86947C5E10590210A4F"
},
{
msg: "8B0A79306C9CE7ED99DAE4F87F8DD61636",
key: "7C77D6E813BED5AC98BAA417477A2E7D",
nonce: "1A8C98DCD73D38393B2BF1569DEEFC19",
header: "65D2017990D62528",
ct: "02083E3979DA014812F59F11D52630DA30137327D10649B0AA6E1C181DB617D7F2"
},
{
msg: "1BDA122BCE8A8DBAF1877D962B8592DD2D56",
key: "5FFF20CAFAB119CA2FC73549E20F5B0D",
nonce: "DDE59B97D722156D4D9AFF2BC7559826",
header: "54B9F04E6A09189A",
ct: "2EC47B2C4954A489AFC7BA4897EDCDAE8CC33B60450599BD02C96382902AEF7F832A"
},
{
msg: "6CF36720872B8513F6EAB1A8A44438D5EF11",
key: "A4A4782BCFFD3EC5E7EF6D8C34A56123",
nonce: "B781FCF2F75FA5A8DE97A9CA48E522EC",
header: "899A175897561D7E",
ct: "0DE18FD0FDD91E7AF19F1D8EE8733938B1E8E7F6D2231618102FDB7FE55FF1991700"
},
{
msg: "CA40D7446E545FFAED3BD12A740A659FFBBB3CEAB7",
key: "8395FCF1E95BEBD697BD010BC766AAC3",
nonce: "22E7ADD93CFC6393C57EC0B3C17D6B44",
header: "126735FCC320D25A",
ct: "CB8920F87A6C75CFF39627B56E3ED197C552D295A7CFC46AFC253B4652B1AF3795B124AB6E"
},
];
const cipher = 'aes128';
for(const [i, vec] of vectors.entries()) {
const keyBytes = openpgp.util.hex_to_Uint8Array(vec.key),
msgBytes = openpgp.util.hex_to_Uint8Array(vec.msg),
nonceBytes = openpgp.util.hex_to_Uint8Array(vec.nonce),
headerBytes = openpgp.util.hex_to_Uint8Array(vec.header),
ctBytes = openpgp.util.hex_to_Uint8Array(vec.ct);
const eax = await openpgp.crypto.eax(cipher, keyBytes);
// encryption test
let ct = await eax.encrypt(msgBytes, nonceBytes, headerBytes);
expect(openpgp.util.Uint8Array_to_hex(ct)).to.equal(vec.ct.toLowerCase());
// decryption test with verification
let pt = await eax.decrypt(ctBytes, nonceBytes, headerBytes);
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase());
// tampering detection test
ct = await eax.encrypt(msgBytes, nonceBytes, headerBytes);
ct[2] ^= 8;
pt = eax.decrypt(ct, nonceBytes, headerBytes);
await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch')
// testing without additional data
ct = await eax.encrypt(msgBytes, nonceBytes, new Uint8Array());
pt = await eax.decrypt(ct, nonceBytes, new Uint8Array());
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase());
// testing with multiple additional data
ct = await eax.encrypt(msgBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes]));
pt = await eax.decrypt(ct, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes]));
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.msg.toLowerCase());
}
});
}
describe('Symmetric AES-EAX (native)', function() {
let use_nativeVal;
beforeEach(function() {
use_nativeVal = openpgp.config.use_native;
openpgp.config.use_native = true;
});
afterEach(function() {
openpgp.config.use_native = use_nativeVal;
});
testAESEAX();
});
describe('Symmetric AES-EAX (asm.js fallback)', function() {
let use_nativeVal;
beforeEach(function() {
use_nativeVal = openpgp.config.use_native;
openpgp.config.use_native = false;
});
afterEach(function() {
openpgp.config.use_native = use_nativeVal;
});
testAESEAX();
});

View File

@ -317,7 +317,7 @@ describe('Elliptic Curve Cryptography', function () {
new Uint8Array(ephemeral),
data,
new Uint8Array(priv),
fingerprint
new Uint8Array(fingerprint)
);
});
};
@ -344,17 +344,17 @@ describe('Elliptic Curve Cryptography', function () {
it('Invalid curve oid', function (done) {
expect(decrypt_message(
'', 2, 7, [], [], [], ''
'', 2, 7, [], [], [], []
)).to.be.rejectedWith(Error, /Not valid curve/).notify(done);
});
it('Invalid ephemeral key', function (done) {
expect(decrypt_message(
'secp256k1', 2, 7, [], [], [], ''
'secp256k1', 2, 7, [], [], [], []
)).to.be.rejectedWith(Error, /Unknown point format/).notify(done);
});
it('Invalid key data integrity', function (done) {
expect(decrypt_message(
'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_data, ''
'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_data, []
)).to.be.rejectedWith(Error, /Key Data Integrity failed/).notify(done);
});
});

View File

@ -6,4 +6,6 @@ describe('Crypto', function () {
require('./elliptic.js');
require('./pkcs5.js');
require('./aes_kw.js');
require('./eax.js');
require('./ocb.js');
});

183
test/crypto/ocb.js Normal file
View File

@ -0,0 +1,183 @@
// Modified by ProtonTech AG
// Adapted from https://github.com/artjomb/cryptojs-extension/blob/8c61d159/test/eax.js
const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp');
const chai = require('chai');
chai.use(require('chai-as-promised'));
const expect = chai.expect;
describe('Symmetric AES-OCB', function() {
it('Passes all test vectors', async function() {
const K = '000102030405060708090A0B0C0D0E0F';
const keyBytes = openpgp.util.hex_to_Uint8Array(K);
var vectors = [
// From https://tools.ietf.org/html/rfc7253#appendix-A
{
N: 'BBAA99887766554433221100',
A: '',
P: '',
C: '785407BFFFC8AD9EDCC5520AC9111EE6'
},
{
N: 'BBAA99887766554433221101',
A: '0001020304050607',
P: '0001020304050607',
C: '6820B3657B6F615A5725BDA0D3B4EB3A257C9AF1F8F03009'
},
{
N: 'BBAA99887766554433221102',
A: '0001020304050607',
P: '',
C: '81017F8203F081277152FADE694A0A00'
},
{
N: 'BBAA99887766554433221103',
A: '',
P: '0001020304050607',
C: '45DD69F8F5AAE72414054CD1F35D82760B2CD00D2F99BFA9'
},
{
N: 'BBAA99887766554433221104',
A: '000102030405060708090A0B0C0D0E0F',
P: '000102030405060708090A0B0C0D0E0F',
C: '571D535B60B277188BE5147170A9A22C3AD7A4FF3835B8C5701C1CCEC8FC3358'
},
{
N: 'BBAA99887766554433221105',
A: '000102030405060708090A0B0C0D0E0F',
P: '',
C: '8CF761B6902EF764462AD86498CA6B97'
},
{
N: 'BBAA99887766554433221106',
A: '',
P: '000102030405060708090A0B0C0D0E0F',
C: '5CE88EC2E0692706A915C00AEB8B2396F40E1C743F52436BDF06D8FA1ECA343D'
},
{
N: 'BBAA99887766554433221107',
A: '000102030405060708090A0B0C0D0E0F1011121314151617',
P: '000102030405060708090A0B0C0D0E0F1011121314151617',
C: '1CA2207308C87C010756104D8840CE1952F09673A448A122C92C62241051F57356D7F3C90BB0E07F'
},
{
N: 'BBAA99887766554433221108',
A: '000102030405060708090A0B0C0D0E0F1011121314151617',
P: '',
C: '6DC225A071FC1B9F7C69F93B0F1E10DE'
},
{
N: 'BBAA99887766554433221109',
A: '',
P: '000102030405060708090A0B0C0D0E0F1011121314151617',
C: '221BD0DE7FA6FE993ECCD769460A0AF2D6CDED0C395B1C3CE725F32494B9F914D85C0B1EB38357FF'
},
{
N: 'BBAA9988776655443322110A',
A: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F',
P: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F',
C: 'BD6F6C496201C69296C11EFD138A467ABD3C707924B964DEAFFC40319AF5A48540FBBA186C5553C68AD9F592A79A4240'
},
{
N: 'BBAA9988776655443322110B',
A: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F',
P: '',
C: 'FE80690BEE8A485D11F32965BC9D2A32'
},
{
N: 'BBAA9988776655443322110C',
A: '',
P: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F',
C: '2942BFC773BDA23CABC6ACFD9BFD5835BD300F0973792EF46040C53F1432BCDFB5E1DDE3BC18A5F840B52E653444D5DF'
},
{
N: 'BBAA9988776655443322110D',
A: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627',
P: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627',
C: 'D5CA91748410C1751FF8A2F618255B68A0A12E093FF454606E59F9C1D0DDC54B65E8628E568BAD7AED07BA06A4A69483A7035490C5769E60'
},
{
N: 'BBAA9988776655443322110E',
A: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627',
P: '',
C: 'C5CD9D1850C141E358649994EE701B68'
},
{
N: 'BBAA9988776655443322110F',
A: '',
P: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021222324252627',
C: '4412923493C57D5DE0D700F753CCE0D1D2D95060122E9F15A5DDBFC5787E50B5CC55EE507BCB084E479AD363AC366B95A98CA5F3000B1479'
}
];
const cipher = 'aes128';
for(const [i, vec] of vectors.entries()) {
const msgBytes = openpgp.util.hex_to_Uint8Array(vec.P),
nonceBytes = openpgp.util.hex_to_Uint8Array(vec.N),
headerBytes = openpgp.util.hex_to_Uint8Array(vec.A),
ctBytes = openpgp.util.hex_to_Uint8Array(vec.C);
const ocb = await openpgp.crypto.ocb(cipher, keyBytes);
// encryption test
let ct = await ocb.encrypt(msgBytes, nonceBytes, headerBytes);
expect(openpgp.util.Uint8Array_to_hex(ct)).to.equal(vec.C.toLowerCase());
// decryption test with verification
let pt = await ocb.decrypt(ctBytes, nonceBytes, headerBytes);
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase());
// tampering detection test
ct = await ocb.encrypt(msgBytes, nonceBytes, headerBytes);
ct[2] ^= 8;
pt = ocb.decrypt(ct, nonceBytes, headerBytes);
await expect(pt).to.eventually.be.rejectedWith('Authentication tag mismatch')
// testing without additional data
ct = await ocb.encrypt(msgBytes, nonceBytes, new Uint8Array());
pt = await ocb.decrypt(ct, nonceBytes, new Uint8Array());
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase());
// testing with multiple additional data
ct = await ocb.encrypt(msgBytes, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes]));
pt = await ocb.decrypt(ct, nonceBytes, openpgp.util.concatUint8Array([headerBytes, headerBytes, headerBytes]));
expect(openpgp.util.Uint8Array_to_hex(pt)).to.equal(vec.P.toLowerCase());
}
});
it('Different key size test vectors', async function() {
const TAGLEN = 128;
const outputs = {
128: '67E944D23256C5E0B6C61FA22FDF1EA2',
192: 'F673F2C3E7174AAE7BAE986CA9F29E17',
256: 'D90EB8E9C977C88B79DD793D7FFA161C'
};
for (const KEYLEN of [128, 192, 256]) {
const K = new Uint8Array(KEYLEN / 8);
K[K.length - 1] = TAGLEN;
const ocb = await openpgp.crypto.ocb('aes' + KEYLEN, K);
const C = [];
let N;
for (let i = 0; i < 128; i++) {
const S = new Uint8Array(i);
N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 1, 4)]);
C.push(await ocb.encrypt(S, N, S));
N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 2, 4)]);
C.push(await ocb.encrypt(S, N, new Uint8Array()));
N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(3 * i + 3, 4)]);
C.push(await ocb.encrypt(new Uint8Array(), N, S));
}
N = openpgp.util.concatUint8Array([new Uint8Array(8), openpgp.util.writeNumber(385, 4)]);
const output = await ocb.encrypt(new Uint8Array(), N, openpgp.util.concatUint8Array(C));
expect(openpgp.util.Uint8Array_to_hex(output)).to.equal(outputs[KEYLEN].toLowerCase());
}
});
});

View File

@ -1,11 +1,55 @@
const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp');
const stub = require('sinon/lib/sinon/stub');
const chai = require('chai');
chai.use(require('chai-as-promised'));
const { expect } = chai;
describe('Key', function() {
let webCrypto = openpgp.util.getWebCryptoAll();
if (webCrypto) {
let generateKey = webCrypto.generateKey;
let keyGenStub;
let keyGenValue;
beforeEach(function() {
keyGenStub = stub(webCrypto, 'generateKey');
keyGenStub.callsFake(function() {
if (!keyGenValue) {
keyGenValue = generateKey.apply(webCrypto, arguments);
}
return keyGenValue;
});
});
afterEach(function() {
keyGenStub.restore();
});
}
describe('V4', tests);
describe('V5', function() {
let aead_protectVal;
let aead_protect_versionVal;
beforeEach(function() {
aead_protectVal = openpgp.config.aead_protect;
aead_protect_versionVal = openpgp.config.aead_protect_version;
openpgp.config.aead_protect = true;
openpgp.config.aead_protect_version = 4;
});
afterEach(function() {
openpgp.config.aead_protect = aead_protectVal;
openpgp.config.aead_protect_version = aead_protect_versionVal;
});
tests();
});
});
function tests() {
const twoKeys =
['-----BEGIN PGP PUBLIC KEY BLOCK-----',
'Version: GnuPG v2.0.19 (GNU/Linux)',
@ -893,6 +937,21 @@ p92yZgB3r2+f6/GIe2+7
done();
});
it('Parsing V5 public key packet', function() {
// Manually modified from https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b2092/back.mkd#sample-eddsa-key
let packetBytes = openpgp.util.hex_to_Uint8Array(`
98 37 05 53 f3 5f 0b 16 00 00 00 2d 09 2b 06 01 04 01 da 47
0f 01 01 07 40 3f 09 89 94 bd d9 16 ed 40 53 19
79 34 e4 a8 7c 80 73 3a 12 80 d6 2f 80 10 99 2e
43 ee 3b 24 06
`.replace(/\s+/g, ''));
let packetlist = new openpgp.packet.List();
packetlist.read(packetBytes);
let key = packetlist[0];
expect(key).to.exist;
});
it('Testing key ID and fingerprint for V3 and V4 keys', function(done) {
const pubKeysV4 = openpgp.key.readArmored(twoKeys);
expect(pubKeysV4).to.exist;
@ -1134,32 +1193,71 @@ p92yZgB3r2+f6/GIe2+7
});
});
it('getPreferredSymAlgo() - one key - AES256', async function() {
it("getPreferredAlgo('symmetric') - one key - AES256", async function() {
const key1 = openpgp.key.readArmored(twoKeys).keys[0];
const prefAlgo = await openpgp.key.getPreferredSymAlgo([key1]);
const prefAlgo = await openpgp.key.getPreferredAlgo('symmetric', [key1]);
expect(prefAlgo).to.equal(openpgp.enums.symmetric.aes256);
});
it('getPreferredSymAlgo() - two key - AES128', async function() {
it("getPreferredAlgo('symmetric') - two key - AES128", async function() {
const keys = openpgp.key.readArmored(twoKeys).keys;
const key1 = keys[0];
const key2 = keys[1];
const primaryUser = await key2.getPrimaryUser();
primaryUser.selfCertification.preferredSymmetricAlgorithms = [6,7,3];
const prefAlgo = await openpgp.key.getPreferredSymAlgo([key1, key2]);
const prefAlgo = await openpgp.key.getPreferredAlgo('symmetric', [key1, key2]);
expect(prefAlgo).to.equal(openpgp.enums.symmetric.aes128);
});
it('getPreferredSymAlgo() - two key - one without pref', async function() {
it("getPreferredAlgo('symmetric') - two key - one without pref", async function() {
const keys = openpgp.key.readArmored(twoKeys).keys;
const key1 = keys[0];
const key2 = keys[1];
const primaryUser = await key2.getPrimaryUser();
primaryUser.selfCertification.preferredSymmetricAlgorithms = null;
const prefAlgo = await openpgp.key.getPreferredSymAlgo([key1, key2]);
const prefAlgo = await openpgp.key.getPreferredAlgo('symmetric', [key1, key2]);
expect(prefAlgo).to.equal(openpgp.config.encryption_cipher);
});
it("getPreferredAlgo('aead') - one key - OCB", async function() {
const key1 = openpgp.key.readArmored(twoKeys).keys[0];
const primaryUser = await key1.getPrimaryUser();
primaryUser.selfCertification.features = [7]; // Monkey-patch AEAD feature flag
primaryUser.selfCertification.preferredAeadAlgorithms = [2,1];
const prefAlgo = await openpgp.key.getPreferredAlgo('aead', [key1]);
expect(prefAlgo).to.equal(openpgp.enums.aead.ocb);
const supported = await openpgp.key.isAeadSupported([key1]);
expect(supported).to.be.true;
});
it("getPreferredAlgo('aead') - two key - one without pref", async function() {
const keys = openpgp.key.readArmored(twoKeys).keys;
const key1 = keys[0];
const key2 = keys[1];
const primaryUser = await key1.getPrimaryUser();
primaryUser.selfCertification.features = [7]; // Monkey-patch AEAD feature flag
primaryUser.selfCertification.preferredAeadAlgorithms = [2,1];
const primaryUser2 = await key2.getPrimaryUser();
primaryUser2.selfCertification.features = [7]; // Monkey-patch AEAD feature flag
const prefAlgo = await openpgp.key.getPreferredAlgo('aead', [key1, key2]);
expect(prefAlgo).to.equal(openpgp.config.aead_mode);
const supported = await openpgp.key.isAeadSupported([key1, key2]);
expect(supported).to.be.true;
});
it("getPreferredAlgo('aead') - two key - one with no support", async function() {
const keys = openpgp.key.readArmored(twoKeys).keys;
const key1 = keys[0];
const key2 = keys[1];
const primaryUser = await key1.getPrimaryUser();
primaryUser.selfCertification.features = [7]; // Monkey-patch AEAD feature flag
primaryUser.selfCertification.preferredAeadAlgorithms = [2,1];
const prefAlgo = await openpgp.key.getPreferredAlgo('aead', [key1, key2]);
expect(prefAlgo).to.equal(openpgp.config.aead_mode);
const supported = await openpgp.key.isAeadSupported([key1, key2]);
expect(supported).to.be.false;
});
it('Preferences of generated key', function() {
const testPref = function(key) {
// key flags
@ -1170,11 +1268,15 @@ p92yZgB3r2+f6/GIe2+7
expect(key.subKeys[0].bindingSignatures[0].keyFlags[0] & keyFlags.encrypt_storage).to.equal(keyFlags.encrypt_storage);
const sym = openpgp.enums.symmetric;
expect(key.users[0].selfCertifications[0].preferredSymmetricAlgorithms).to.eql([sym.aes256, sym.aes128, sym.aes192, sym.cast5, sym.tripledes]);
if (openpgp.config.aead_protect && openpgp.config.aead_protect_version === 4) {
const aead = openpgp.enums.aead;
expect(key.users[0].selfCertifications[0].preferredAeadAlgorithms).to.eql([aead.eax, aead.ocb]);
}
const hash = openpgp.enums.hash;
expect(key.users[0].selfCertifications[0].preferredHashAlgorithms).to.eql([hash.sha256, hash.sha512, hash.sha1]);
const compr = openpgp.enums.compression;
expect(key.users[0].selfCertifications[0].preferredCompressionAlgorithms).to.eql([compr.zlib, compr.zip]);
expect(key.users[0].selfCertifications[0].features).to.eql(openpgp.config.integrity_protect ? [1] : null); // modification detection
expect(key.users[0].selfCertifications[0].features).to.eql(openpgp.config.aead_protect && openpgp.config.aead_protect_version === 4 ? [7] : [1]);
};
const opt = {numBits: 512, userIds: 'test <a@b.com>', passphrase: 'hello'};
if (openpgp.util.getWebCryptoAll()) { opt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys
@ -1574,4 +1676,4 @@ p92yZgB3r2+f6/GIe2+7
expect(error.message).to.equal('Error encrypting message: Could not find valid key packet for encryption in key ' + key.primaryKey.getKeyId().toHex());
});
});
});
}

View File

@ -597,11 +597,15 @@ describe('OpenPGP.js public api tests', function() {
let zero_copyVal;
let use_nativeVal;
let aead_protectVal;
let aead_protect_versionVal;
let aead_modeVal;
let aead_chunk_size_byteVal;
beforeEach(function(done) {
publicKey = openpgp.key.readArmored(pub_key);
expect(publicKey.keys).to.have.length(1);
expect(publicKey.err).to.not.exist;
publicKeyNoAEAD = openpgp.key.readArmored(pub_key);
privateKey = openpgp.key.readArmored(priv_key);
expect(privateKey.keys).to.have.length(1);
expect(privateKey.err).to.not.exist;
@ -620,6 +624,9 @@ describe('OpenPGP.js public api tests', function() {
zero_copyVal = openpgp.config.zero_copy;
use_nativeVal = openpgp.config.use_native;
aead_protectVal = openpgp.config.aead_protect;
aead_protect_versionVal = openpgp.config.aead_protect_version;
aead_modeVal = openpgp.config.aead_mode;
aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte;
done();
});
@ -627,6 +634,9 @@ describe('OpenPGP.js public api tests', function() {
openpgp.config.zero_copy = zero_copyVal;
openpgp.config.use_native = use_nativeVal;
openpgp.config.aead_protect = aead_protectVal;
openpgp.config.aead_protect_version = aead_protect_versionVal;
openpgp.config.aead_mode = aead_modeVal;
openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal;
});
it('Decrypting key with wrong passphrase rejected', async function () {
@ -638,9 +648,8 @@ describe('OpenPGP.js public api tests', function() {
});
tryTests('CFB mode (asm.js)', tests, {
if: true,
if: !(typeof window !== 'undefined' && window.Worker),
beforeEach: function() {
openpgp.config.use_native = true;
openpgp.config.aead_protect = false;
}
});
@ -651,7 +660,6 @@ describe('OpenPGP.js public api tests', function() {
openpgp.initWorker({ path:'../dist/openpgp.worker.js' });
},
beforeEach: function() {
openpgp.config.use_native = true;
openpgp.config.aead_protect = false;
},
after: function() {
@ -659,11 +667,50 @@ describe('OpenPGP.js public api tests', function() {
}
});
tryTests('GCM mode (native)', tests, {
if: openpgp.util.getWebCrypto() || openpgp.util.getNodeCrypto(),
tryTests('GCM mode', tests, {
if: true,
beforeEach: function() {
openpgp.config.use_native = true;
openpgp.config.aead_protect = true;
openpgp.config.aead_protect_version = 0;
}
});
tryTests('GCM mode (draft04)', tests, {
if: true,
beforeEach: function() {
openpgp.config.aead_protect = true;
openpgp.config.aead_mode = openpgp.enums.aead.experimental_gcm;
// Monkey-patch AEAD feature flag
publicKey.keys[0].users[0].selfCertifications[0].features = [7];
publicKey_2000_2008.keys[0].users[0].selfCertifications[0].features = [7];
publicKey_2038_2045.keys[0].users[0].selfCertifications[0].features = [7];
}
});
tryTests('EAX mode (small chunk size)', tests, {
if: true,
beforeEach: function() {
openpgp.config.aead_protect = true;
openpgp.config.aead_chunk_size_byte = 0;
// Monkey-patch AEAD feature flag
publicKey.keys[0].users[0].selfCertifications[0].features = [7];
publicKey_2000_2008.keys[0].users[0].selfCertifications[0].features = [7];
publicKey_2038_2045.keys[0].users[0].selfCertifications[0].features = [7];
}
});
tryTests('OCB mode', tests, {
if: true,
beforeEach: function() {
openpgp.config.aead_protect = true;
openpgp.config.aead_mode = openpgp.enums.aead.ocb;
// Monkey-patch AEAD feature flag
publicKey.keys[0].users[0].selfCertifications[0].features = [7];
publicKey_2000_2008.keys[0].users[0].selfCertifications[0].features = [7];
publicKey_2038_2045.keys[0].users[0].selfCertifications[0].features = [7];
}
});
@ -779,6 +826,30 @@ describe('OpenPGP.js public api tests', function() {
});
});
it('roundtrip workflow: encrypt, decryptSessionKeys, decrypt with pgp key pair -- trailing spaces', function () {
const plaintext = 'space: \nspace and tab: \t\nno trailing space\n \ntab:\t\ntab and space:\t ';
let msgAsciiArmored;
return openpgp.encrypt({
data: plaintext,
publicKeys: publicKey.keys
}).then(function (encrypted) {
msgAsciiArmored = encrypted.data;
return openpgp.decryptSessionKeys({
message: openpgp.message.readArmored(msgAsciiArmored),
privateKeys: privateKey.keys[0]
});
}).then(function (decryptedSessionKeys) {
const message = openpgp.message.readArmored(msgAsciiArmored);
return openpgp.decrypt({
sessionKeys: decryptedSessionKeys[0],
message
});
}).then(function (decrypted) {
expect(decrypted.data).to.equal(plaintext);
});
});
it('roundtrip workflow: encrypt, decryptSessionKeys, decrypt with password', function () {
let msgAsciiArmored;
return openpgp.encrypt({
@ -987,20 +1058,21 @@ describe('OpenPGP.js public api tests', function() {
return openpgp.encrypt(encOpt).then(function (encrypted) {
expect(encrypted.data).to.match(/^-----BEGIN PGP MESSAGE/);
decOpt.message = openpgp.message.readArmored(encrypted.data);
expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect && openpgp.config.aead_protect_version !== 4);
return openpgp.decrypt(decOpt);
}).then(function (decrypted) {
expect(decrypted.data).to.equal(plaintext);
});
});
it('should encrypt using custom session key and decrypt using private key', function () {
it('should encrypt using custom session key and decrypt using private key', async function () {
const sessionKey = {
data: openpgp.crypto.generateSessionKey('aes128'),
data: await openpgp.crypto.generateSessionKey('aes128'),
algorithm: 'aes128'
};
const encOpt = {
data: plaintext,
sessionKeys: sessionKey,
sessionKey: sessionKey,
publicKeys: publicKey.keys
};
const decOpt = {
@ -1009,6 +1081,7 @@ describe('OpenPGP.js public api tests', function() {
return openpgp.encrypt(encOpt).then(function (encrypted) {
expect(encrypted.data).to.match(/^-----BEGIN PGP MESSAGE/);
decOpt.message = openpgp.message.readArmored(encrypted.data);
expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect && openpgp.config.aead_protect_version !== 4);
return openpgp.decrypt(decOpt);
}).then(function (decrypted) {
expect(decrypted.data).to.equal(plaintext);
@ -1027,6 +1100,7 @@ describe('OpenPGP.js public api tests', function() {
};
return openpgp.encrypt(encOpt).then(function (encrypted) {
decOpt.message = openpgp.message.readArmored(encrypted.data);
expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect);
return openpgp.decrypt(decOpt);
}).then(async function (decrypted) {
expect(decrypted.data).to.equal(plaintext);
@ -1037,6 +1111,63 @@ describe('OpenPGP.js public api tests', function() {
});
});
it('should encrypt/sign and decrypt/verify (no AEAD support)', function () {
const encOpt = {
data: plaintext,
publicKeys: publicKeyNoAEAD.keys,
privateKeys: privateKey.keys
};
const decOpt = {
privateKeys: privateKey.keys[0],
publicKeys: publicKeyNoAEAD.keys
};
return openpgp.encrypt(encOpt).then(function (encrypted) {
decOpt.message = openpgp.message.readArmored(encrypted.data);
expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect && openpgp.config.aead_protect_version !== 4);
return openpgp.decrypt(decOpt);
}).then(async function (decrypted) {
expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures[0].valid).to.be.true;
const keyPacket = await privateKey.keys[0].getSigningKeyPacket();
expect(decrypted.signatures[0].keyid.toHex()).to.equal(keyPacket.getKeyId().toHex());
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
});
});
it('should encrypt/sign and decrypt/verify with generated key', function () {
const genOpt = {
userIds: [{ name: 'Test User', email: 'text@example.com' }],
numBits: 512
};
if (openpgp.util.getWebCryptoAll()) { genOpt.numBits = 2048; } // webkit webcrypto accepts minimum 2048 bit keys
return openpgp.generateKey(genOpt).then(function(newKey) {
const newPublicKey = openpgp.key.readArmored(newKey.publicKeyArmored);
const newPrivateKey = openpgp.key.readArmored(newKey.privateKeyArmored);
const encOpt = {
data: plaintext,
publicKeys: newPublicKey.keys,
privateKeys: newPrivateKey.keys
};
const decOpt = {
privateKeys: newPrivateKey.keys[0],
publicKeys: newPublicKey.keys
};
return openpgp.encrypt(encOpt).then(function (encrypted) {
decOpt.message = openpgp.message.readArmored(encrypted.data);
expect(!!decOpt.message.packets.findPacket(openpgp.enums.packet.symEncryptedAEADProtected)).to.equal(openpgp.config.aead_protect);
return openpgp.decrypt(decOpt);
}).then(async function (decrypted) {
expect(decrypted.data).to.equal(plaintext);
expect(decrypted.signatures[0].valid).to.be.true;
const keyPacket = await newPrivateKey.keys[0].getSigningKeyPacket();
expect(decrypted.signatures[0].keyid.toHex()).to.equal(keyPacket.getKeyId().toHex());
expect(decrypted.signatures[0].signature.packets.length).to.equal(1);
});
});
});
it('should encrypt/sign and decrypt/verify with null string input', function () {
const encOpt = {
data: '',
@ -1667,6 +1798,37 @@ describe('OpenPGP.js public api tests', function() {
}).then(function (packets) {
const literals = packets.packets.filterByTag(openpgp.enums.packet.literal);
expect(literals.length).to.equal(1);
expect(literals[0].format).to.equal('binary');
expect(+literals[0].date).to.equal(+future);
expect(packets.getLiteralData()).to.deep.equal(data);
return packets.verify(encryptOpt.publicKeys, future);
}).then(function (signatures) {
expect(+signatures[0].signature.packets[0].created).to.equal(+future);
expect(signatures[0].valid).to.be.true;
expect(encryptOpt.privateKeys[0].getSigningKeyPacket(signatures[0].keyid, future))
.to.be.not.null;
expect(signatures[0].signature.packets.length).to.equal(1);
});
});
it('should sign, encrypt and decrypt, verify mime data with a date in the future', function () {
const future = new Date(2040, 5, 5, 5, 5, 5, 0);
const data = new Uint8Array([3, 14, 15, 92, 65, 35, 59]);
const encryptOpt = {
data,
dataType: 'mime',
publicKeys: publicKey_2038_2045.keys,
privateKeys: privateKey_2038_2045.keys,
date: future,
armor: false
};
return openpgp.encrypt(encryptOpt).then(function (encrypted) {
return encrypted.message.decrypt(encryptOpt.privateKeys);
}).then(function (packets) {
const literals = packets.packets.filterByTag(openpgp.enums.packet.literal);
expect(literals.length).to.equal(1);
expect(literals[0].format).to.equal('mime');
expect(+literals[0].date).to.equal(+future);
expect(packets.getLiteralData()).to.deep.equal(data);
return packets.verify(encryptOpt.publicKeys, future);
@ -1686,6 +1848,7 @@ describe('OpenPGP.js public api tests', function() {
const pubKeyDE = openpgp.key.readArmored(pub_key_de).keys[0];
const privKeyDE = openpgp.key.readArmored(priv_key_de).keys[0];
await privKeyDE.decrypt(passphrase);
pubKeyDE.users[0].selfCertifications[0].features = [7]; // Monkey-patch AEAD feature flag
return openpgp.encrypt({
publicKeys: pubKeyDE,
privateKeys: privKeyDE,

View File

@ -1,5 +1,6 @@
const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp');
const stub = require('sinon/lib/sinon/stub');
const chai = require('chai');
chai.use(require('chai-as-promised'));
@ -88,7 +89,7 @@ describe("Packet", function() {
message.push(enc);
await enc.packets.push(literal);
const key = '12345678901234567890123456789012';
const key = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]);
const algo = 'aes256';
await enc.encrypt(algo, key);
@ -141,6 +142,87 @@ describe("Packet", function() {
});
});
it('Sym. encrypted AEAD protected packet (draft04)', function() {
let aead_protectVal = openpgp.config.aead_protect;
let aead_protect_versionVal = openpgp.config.aead_protect_version;
openpgp.config.aead_protect = true;
openpgp.config.aead_protect_version = 4;
const key = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]);
const algo = 'aes256';
const literal = new openpgp.packet.Literal();
const enc = new openpgp.packet.SymEncryptedAEADProtected();
const msg = new openpgp.packet.List();
msg.push(enc);
literal.setText('Hello world!');
enc.packets.push(literal);
const msg2 = new openpgp.packet.List();
return enc.encrypt(algo, key).then(function() {
msg2.read(msg.write());
return msg2[0].decrypt(algo, key);
}).then(function() {
expect(msg2[0].packets[0].data).to.deep.equal(literal.data);
}).finally(function() {
openpgp.config.aead_protect = aead_protectVal;
openpgp.config.aead_protect_version = aead_protect_versionVal;
});
});
it('Sym. encrypted AEAD protected packet test vector (draft04)', function() {
// From https://gitlab.com/openpgp-wg/rfc4880bis/commit/00b20923e6233fb6ff1666ecd5acfefceb32907d
let packetBytes = openpgp.util.hex_to_Uint8Array(`
d4 4a 01 07 01 0e b7 32 37 9f 73 c4 92 8d e2 5f
ac fe 65 17 ec 10 5d c1 1a 81 dc 0c b8 a2 f6 f3
d9 00 16 38 4a 56 fc 82 1a e1 1a e8 db cb 49 86
26 55 de a8 8d 06 a8 14 86 80 1b 0f f3 87 bd 2e
ab 01 3d e1 25 95 86 90 6e ab 24 76
`.replace(/\s+/g, ''));
let aead_protectVal = openpgp.config.aead_protect;
let aead_protect_versionVal = openpgp.config.aead_protect_version;
let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte;
openpgp.config.aead_protect = true;
openpgp.config.aead_protect_version = 4;
openpgp.config.aead_chunk_size_byte = 14;
const iv = openpgp.util.hex_to_Uint8Array('b7 32 37 9f 73 c4 92 8d e2 5f ac fe 65 17 ec 10'.replace(/\s+/g, ''));
const key = openpgp.util.hex_to_Uint8Array('86 f1 ef b8 69 52 32 9f 24 ac d3 bf d0 e5 34 6d'.replace(/\s+/g, ''));
const algo = 'aes128';
const literal = new openpgp.packet.Literal(0);
const enc = new openpgp.packet.SymEncryptedAEADProtected();
const msg = new openpgp.packet.List();
msg.push(enc);
literal.setBytes(openpgp.util.str_to_Uint8Array('Hello, world!\n'), openpgp.enums.literal.binary);
literal.filename = '';
enc.packets.push(literal);
const msg2 = new openpgp.packet.List();
let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes');
randomBytesStub.returns(resolves(iv));
return enc.encrypt(algo, key).then(function() {
const data = msg.write();
expect(data).to.deep.equal(packetBytes);
msg2.read(data);
return msg2[0].decrypt(algo, key);
}).then(function() {
expect(msg2[0].packets[0].data).to.deep.equal(literal.data);
}).finally(function() {
openpgp.config.aead_protect = aead_protectVal;
openpgp.config.aead_protect_version = aead_protect_versionVal;
openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal;
randomBytesStub.restore();
});
});
it('Sym encrypted session key with a compressed packet', async function() {
const msg =
'-----BEGIN PGP MESSAGE-----\n' +
@ -187,13 +269,13 @@ describe("Packet", function() {
enc.publicKeyAlgorithm = 'rsa_encrypt';
enc.sessionKeyAlgorithm = 'aes256';
enc.publicKeyId.bytes = '12345678';
return enc.encrypt({ params: mpi }).then(() => {
return enc.encrypt({ params: mpi, getFingerprintBytes() {} }).then(() => {
msg.push(enc);
msg2.read(msg.write());
return msg2[0].decrypt({ params: mpi }).then(() => {
return msg2[0].decrypt({ params: mpi, getFingerprintBytes() {} }).then(() => {
expect(stringify(msg2[0].sessionKey)).to.equal(stringify(enc.sessionKey));
expect(msg2[0].sessionKeyAlgorithm).to.equal(enc.sessionKeyAlgorithm);
@ -339,6 +421,204 @@ describe("Packet", function() {
expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data));
});
it('Sym. encrypted session key reading/writing (draft04)', async function() {
let aead_protectVal = openpgp.config.aead_protect;
let aead_protect_versionVal = openpgp.config.aead_protect_version;
openpgp.config.aead_protect = true;
openpgp.config.aead_protect_version = 4;
try {
const passphrase = 'hello';
const algo = 'aes256';
const literal = new openpgp.packet.Literal();
const key_enc = new openpgp.packet.SymEncryptedSessionKey();
const enc = new openpgp.packet.SymEncryptedAEADProtected();
const msg = new openpgp.packet.List();
msg.push(key_enc);
msg.push(enc);
key_enc.sessionKeyAlgorithm = algo;
await key_enc.encrypt(passphrase);
const key = key_enc.sessionKey;
literal.setText('Hello world!');
enc.packets.push(literal);
await enc.encrypt(algo, key);
const msg2 = new openpgp.packet.List();
msg2.read(msg.write());
await msg2[0].decrypt(passphrase);
const key2 = msg2[0].sessionKey;
await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2);
expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data));
} finally {
openpgp.config.aead_protect = aead_protectVal;
openpgp.config.aead_protect_version = aead_protect_versionVal;
}
});
it('Sym. encrypted session key reading/writing test vector (EAX, draft04)', async function() {
// From https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b20923/back.mkd#sample-aead-eax-encryption-and-decryption
let aead_protectVal = openpgp.config.aead_protect;
let aead_protect_versionVal = openpgp.config.aead_protect_version;
let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte;
let s2k_iteration_count_byteVal = openpgp.config.s2k_iteration_count_byte;
openpgp.config.aead_protect = true;
openpgp.config.aead_protect_version = 4;
openpgp.config.aead_chunk_size_byte = 14;
openpgp.config.s2k_iteration_count_byte = 0x90;
let salt = openpgp.util.hex_to_Uint8Array(`cd5a9f70fbe0bc65`);
let sessionKey = openpgp.util.hex_to_Uint8Array(`86 f1 ef b8 69 52 32 9f 24 ac d3 bf d0 e5 34 6d`.replace(/\s+/g, ''));
let sessionIV = openpgp.util.hex_to_Uint8Array(`bc 66 9e 34 e5 00 dc ae dc 5b 32 aa 2d ab 02 35`.replace(/\s+/g, ''));
let dataIV = openpgp.util.hex_to_Uint8Array(`b7 32 37 9f 73 c4 92 8d e2 5f ac fe 65 17 ec 10`.replace(/\s+/g, ''));
let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes');
randomBytesStub.onCall(0).returns(resolves(salt));
randomBytesStub.onCall(1).returns(resolves(sessionKey));
randomBytesStub.onCall(2).returns(resolves(sessionIV));
randomBytesStub.onCall(3).returns(resolves(dataIV));
let packetBytes = openpgp.util.hex_to_Uint8Array(`
c3 3e 05 07 01 03 08 cd 5a 9f 70 fb e0 bc 65 90
bc 66 9e 34 e5 00 dc ae dc 5b 32 aa 2d ab 02 35
9d ee 19 d0 7c 34 46 c4 31 2a 34 ae 19 67 a2 fb
7e 92 8e a5 b4 fa 80 12 bd 45 6d 17 38 c6 3c 36
d4 4a 01 07 01 0e b7 32 37 9f 73 c4 92 8d e2 5f
ac fe 65 17 ec 10 5d c1 1a 81 dc 0c b8 a2 f6 f3
d9 00 16 38 4a 56 fc 82 1a e1 1a e8 db cb 49 86
26 55 de a8 8d 06 a8 14 86 80 1b 0f f3 87 bd 2e
ab 01 3d e1 25 95 86 90 6e ab 24 76
`.replace(/\s+/g, ''));
try {
const passphrase = 'password';
const algo = 'aes128';
const literal = new openpgp.packet.Literal(0);
const key_enc = new openpgp.packet.SymEncryptedSessionKey();
const enc = new openpgp.packet.SymEncryptedAEADProtected();
const msg = new openpgp.packet.List();
msg.push(key_enc);
msg.push(enc);
key_enc.sessionKeyAlgorithm = algo;
await key_enc.encrypt(passphrase);
const key = key_enc.sessionKey;
literal.setBytes(openpgp.util.str_to_Uint8Array('Hello, world!\n'), openpgp.enums.literal.binary);
literal.filename = '';
enc.packets.push(literal);
await enc.encrypt(algo, key);
const data = msg.write();
expect(data).to.deep.equal(packetBytes);
const msg2 = new openpgp.packet.List();
msg2.read(data);
await msg2[0].decrypt(passphrase);
const key2 = msg2[0].sessionKey;
await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2);
expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data));
} finally {
openpgp.config.aead_protect = aead_protectVal;
openpgp.config.aead_protect_version = aead_protect_versionVal;
openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal;
openpgp.config.s2k_iteration_count_byte = s2k_iteration_count_byteVal;
randomBytesStub.restore();
}
});
it('Sym. encrypted session key reading/writing test vector (OCB, draft04)', async function() {
// From https://gitlab.com/openpgp-wg/rfc4880bis/blob/00b20923/back.mkd#sample-aead-ocb-encryption-and-decryption
let aead_protectVal = openpgp.config.aead_protect;
let aead_protect_versionVal = openpgp.config.aead_protect_version;
let aead_chunk_size_byteVal = openpgp.config.aead_chunk_size_byte;
let s2k_iteration_count_byteVal = openpgp.config.s2k_iteration_count_byte;
openpgp.config.aead_protect = true;
openpgp.config.aead_protect_version = 4;
openpgp.config.aead_chunk_size_byte = 14;
openpgp.config.s2k_iteration_count_byte = 0x90;
let salt = openpgp.util.hex_to_Uint8Array(`9f0b7da3e5ea6477`);
let sessionKey = openpgp.util.hex_to_Uint8Array(`d1 f0 1b a3 0e 13 0a a7 d2 58 2c 16 e0 50 ae 44`.replace(/\s+/g, ''));
let sessionIV = openpgp.util.hex_to_Uint8Array(`99 e3 26 e5 40 0a 90 93 6c ef b4 e8 eb a0 8c`.replace(/\s+/g, ''));
let dataIV = openpgp.util.hex_to_Uint8Array(`5e d2 bc 1e 47 0a be 8f 1d 64 4c 7a 6c 8a 56`.replace(/\s+/g, ''));
let randomBytesStub = stub(openpgp.crypto.random, 'getRandomBytes');
randomBytesStub.onCall(0).returns(resolves(salt));
randomBytesStub.onCall(1).returns(resolves(sessionKey));
randomBytesStub.onCall(2).returns(resolves(sessionIV));
randomBytesStub.onCall(3).returns(resolves(dataIV));
let packetBytes = openpgp.util.hex_to_Uint8Array(`
c3 3d 05 07 02 03 08 9f 0b 7d a3 e5 ea 64 77 90
99 e3 26 e5 40 0a 90 93 6c ef b4 e8 eb a0 8c 67
73 71 6d 1f 27 14 54 0a 38 fc ac 52 99 49 da c5
29 d3 de 31 e1 5b 4a eb 72 9e 33 00 33 db ed
d4 49 01 07 02 0e 5e d2 bc 1e 47 0a be 8f 1d 64
4c 7a 6c 8a 56 7b 0f 77 01 19 66 11 a1 54 ba 9c
25 74 cd 05 62 84 a8 ef 68 03 5c 62 3d 93 cc 70
8a 43 21 1b b6 ea f2 b2 7f 7c 18 d5 71 bc d8 3b
20 ad d3 a0 8b 73 af 15 b9 a0 98
`.replace(/\s+/g, ''));
try {
const passphrase = 'password';
const algo = 'aes128';
const literal = new openpgp.packet.Literal(0);
const key_enc = new openpgp.packet.SymEncryptedSessionKey();
const enc = new openpgp.packet.SymEncryptedAEADProtected();
const msg = new openpgp.packet.List();
enc.aeadAlgorithm = key_enc.aeadAlgorithm = 'ocb';
msg.push(key_enc);
msg.push(enc);
key_enc.sessionKeyAlgorithm = algo;
await key_enc.encrypt(passphrase);
const key = key_enc.sessionKey;
literal.setBytes(openpgp.util.str_to_Uint8Array('Hello, world!\n'), openpgp.enums.literal.binary);
literal.filename = '';
enc.packets.push(literal);
await enc.encrypt(algo, key);
const data = msg.write();
expect(data).to.deep.equal(packetBytes);
const msg2 = new openpgp.packet.List();
msg2.read(data);
await msg2[0].decrypt(passphrase);
const key2 = msg2[0].sessionKey;
await msg2[1].decrypt(msg2[0].sessionKeyAlgorithm, key2);
expect(stringify(msg2[1].packets[0].data)).to.equal(stringify(literal.data));
} finally {
openpgp.config.aead_protect = aead_protectVal;
openpgp.config.aead_protect_version = aead_protect_versionVal;
openpgp.config.aead_chunk_size_byte = aead_chunk_size_byteVal;
openpgp.config.s2k_iteration_count_byte = s2k_iteration_count_byteVal;
randomBytesStub.restore();
}
});
it('Secret key encryption/decryption test', async function() {
const armored_msg =
'-----BEGIN PGP MESSAGE-----\n' +
@ -448,6 +728,41 @@ describe("Packet", function() {
});
});
it('Writing and encryption of a secret key packet. (draft04)', function() {
let aead_protectVal = openpgp.config.aead_protect;
let aead_protect_versionVal = openpgp.config.aead_protect_version;
openpgp.config.aead_protect = true;
openpgp.config.aead_protect_version = 4;
const key = new openpgp.packet.List();
key.push(new openpgp.packet.SecretKey());
const rsa = openpgp.crypto.publicKey.rsa;
const keySize = openpgp.util.getWebCryptoAll() ? 2048 : 512; // webkit webcrypto accepts minimum 2048 bit keys
return rsa.generate(keySize, "10001").then(async function(mpiGen) {
let mpi = [mpiGen.n, mpiGen.e, mpiGen.d, mpiGen.p, mpiGen.q, mpiGen.u];
mpi = mpi.map(function(k) {
return new openpgp.MPI(k);
});
key[0].params = mpi;
key[0].algorithm = "rsa_sign";
await key[0].encrypt('hello');
const raw = key.write();
const key2 = new openpgp.packet.List();
key2.read(raw);
await key2[0].decrypt('hello');
expect(key[0].params.toString()).to.equal(key2[0].params.toString());
}).finally(function() {
openpgp.config.aead_protect = aead_protectVal;
openpgp.config.aead_protect_version = aead_protect_versionVal;
});
});
it('Writing and verification of a signature packet.', function() {
const key = new openpgp.packet.SecretKey();

View File

@ -580,6 +580,75 @@ describe("Signature", function() {
});
});
it('Verify cleartext signed message with trailing spaces from GPG', function() {
const msg_armor =
`-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
space:
space and tab: \t
no trailing space
tab:\t
tab and space:\t
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1
iJwEAQECAAYFAlrZzCQACgkQ4IT3RGwgLJeWggP+Pb33ubbELIzg9/imM+zlR063
g0FbG4B+RGZNFSbaDArUgh9fdVqBy8M9vvbbDMBalSiQxY09Lrasfb+tsomrygbN
NisuPRa5phPhn1bB4hZDb2ed/iK41CNyU7QHuv4AAvLC0mMamRnEg0FW2M2jZLGh
zmuVOdNuWQqxT9Sqa84=
=bqAR
-----END PGP SIGNATURE-----`;
const plaintext = 'space: \nspace and tab: \t\nno trailing space\n \ntab:\t\ntab and space:\t ';
const csMsg = openpgp.cleartext.readArmored(msg_armor);
const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0];
const keyids = csMsg.getSigningKeyIds();
expect(pubKey.getKeyPackets(keyids[0])).to.not.be.empty;
return openpgp.verify({ publicKeys:[pubKey], message:csMsg }).then(function(cleartextSig) {
expect(cleartextSig).to.exist;
expect(cleartextSig.data).to.equal(openpgp.util.removeTrailingSpaces(plaintext));
expect(cleartextSig.signatures).to.have.length(1);
expect(cleartextSig.signatures[0].valid).to.be.true;
expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1);
});
});
it('Verify signed message with trailing spaces from GPG', function() {
const msg_armor =
`-----BEGIN PGP MESSAGE-----
Version: GnuPG v1
owGbwMvMyMT4oOW7S46CznTG01El3MUFicmpxbolqcUlUTev14K5Vgq8XGCGQmJe
ikJJYpKVAicvV16+QklRYmZOZl66AliWl0sBqBAkzQmmwKohBnAqdMxhYWRkYmBj
ZQIZy8DFKQCztusM8z+Vt/svG80IS/etn90utv/T16jquk69zPvp6t9F16ryrwpb
kfVlS5Xl38KnVYxWvIor0nao6WUczA4vvZX9TXPWnnW3tt1vbZoiqWUjYjjjhuKG
4DtmMTuL3TW6/zNzVfWp/Q11+71O8RGnXMsBvWM6mSqX75uLiPo6HRaUDHnvrfCP
yYDnCgA=
=15ki
-----END PGP MESSAGE-----`;
const plaintext = 'space: \nspace and tab: \t\nno trailing space\n \ntab:\t\ntab and space:\t ';
const sMsg = openpgp.message.readArmored(msg_armor);
const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0];
const keyids = sMsg.getSigningKeyIds();
expect(pubKey.getKeyPackets(keyids[0])).to.not.be.empty;
return openpgp.verify({ publicKeys:[pubKey], message:sMsg }).then(function(cleartextSig) {
expect(cleartextSig).to.exist;
expect(openpgp.util.nativeEOL(openpgp.util.Uint8Array_to_str(cleartextSig.data))).to.equal(plaintext);
expect(cleartextSig.signatures).to.have.length(1);
expect(cleartextSig.signatures[0].valid).to.be.true;
expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1);
});
});
it('Sign text with openpgp.sign and verify with openpgp.verify leads to same string cleartext and valid signatures', async function() {
const plaintext = 'short message\nnext line\n한국어/조선말';
const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0];
@ -620,6 +689,26 @@ describe("Signature", function() {
});
});
it('Sign text with openpgp.sign and verify with openpgp.verify leads to same string cleartext and valid signatures -- trailing spaces', async function() {
const plaintext = 'space: \nspace and tab: \t\nno trailing space\n \ntab:\t\ntab and space:\t ';
const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0];
const privKey = openpgp.key.readArmored(priv_key_arm2).keys[0];
await privKey.primaryKey.decrypt('hello world');
return openpgp.sign({ privateKeys:[privKey], data:plaintext }).then(function(signed) {
const csMsg = openpgp.cleartext.readArmored(signed.data);
return openpgp.verify({ publicKeys:[pubKey], message:csMsg });
}).then(function(cleartextSig) {
expect(cleartextSig).to.exist;
expect(cleartextSig.data).to.equal(openpgp.util.removeTrailingSpaces(plaintext));
expect(cleartextSig.signatures).to.have.length(1);
expect(cleartextSig.signatures[0].valid).to.be.true;
expect(cleartextSig.signatures[0].signature.packets.length).to.equal(1);
});
});
it('Sign text with openpgp.sign and verify with openpgp.verify leads to same bytes cleartext and valid signatures - armored', async function() {
const plaintext = openpgp.util.str_to_Uint8Array('short message\nnext line\n한국어/조선말');
const pubKey = openpgp.key.readArmored(pub_key_arm2).keys[0];

View File

@ -32,7 +32,20 @@ if (typeof Promise === 'undefined') {
describe('Unit Tests', function () {
if (typeof window !== 'undefined') { afterEach(function () { window.scrollTo(0, document.body.scrollHeight); }); }
if (typeof window !== 'undefined') {
afterEach(function () {
if (window.scrollY >= document.body.scrollHeight - window.innerHeight - 100) {
window.scrollTo(0, document.body.scrollHeight);
}
});
window.location.search.substr(1).split('&').forEach(param => {
const [key, value] = param.split('=');
if (key && key !== 'grep') {
openpgp.config[key] = JSON.parse(value);
}
});
}
require('./crypto');
require('./general');

View File

@ -17,7 +17,7 @@ elif [[ $OPENPGPJSTEST =~ ^end2end-.* ]]; then
declare -a capabilities=(
"export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Firefox\", \"version\":\"34\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\"}'"
"export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Firefox\", \"version\":\"54\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\"}'"
"export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Chrome\", \"version\":\"38\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\", \"extendedDebugging\":true}'"
"export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Chrome\", \"version\":\"41\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\", \"extendedDebugging\":true}'"
"export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Chrome\", \"version\":\"59\", \"platform\":\"OS X 10.12\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\", \"extendedDebugging\":true}'"
"export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"Internet Explorer\", \"version\":\"11.103\", \"platform\":\"Windows 10\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\"}'"
"export SELENIUM_BROWSER_CAPABILITIES='{\"browserName\":\"MicrosoftEdge\", \"version\":\"15.15063\", \"platform\":\"Windows 10\", \"maxDuration\":\"7200\", \"commandTimeout\":\"600\", \"idleTimeout\":\"270\"}'"