commit
d562c147f9
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
29
README.md
29
README.md
|
@ -54,17 +54,32 @@ OpenPGP.js [ | 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
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
98
src/crypto/cmac.js
Normal 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^(n−1−(|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
168
src/crypto/eax.js
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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
274
src/crypto/ocb.js
Normal 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;
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
41
src/enums.js
41
src/enums.js
|
@ -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') {
|
||||
|
|
67
src/key.js
67
src/key.js
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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)];
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
|
98
src/util.js
98
src/util.js
|
@ -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, "");
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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
150
test/crypto/eax.js
Normal 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();
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
183
test/crypto/ocb.js
Normal 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());
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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\"}'"
|
||||
|
|
Loading…
Reference in New Issue
Block a user