diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index d0c0fa7a..490e34f9 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -28,7 +28,11 @@ import mode from './mode'; import { getRandomBytes } from './random'; import { getCipherParams } from './cipher'; import ECDHSymkey from '../type/ecdh_symkey'; +import ShortByteString from '../type/short_byte_string'; +import hash from './hash'; +import config from '../config'; import KDFParams from '../type/kdf_params'; +import { SymAlgoEnum, AEADEnum, HashEnum } from '../type/enum'; import enums from '../enums'; import util from '../util'; import OID from '../type/oid'; @@ -41,12 +45,13 @@ import ECDHXSymmetricKey from '../type/ecdh_x_symkey'; * @param {module:enums.publicKey} keyAlgo - Public key algorithm * @param {module:enums.symmetric|null} symmetricAlgo - Cipher algorithm (v3 only) * @param {Object} publicParams - Algorithm-specific public key parameters - * @param {Uint8Array} data - Session key data to be encrypted + * @param {Object} privateParams - Algorithm-specific private key parameters + * @param {Uint8Array} data - Data to be encrypted * @param {Uint8Array} fingerprint - Recipient fingerprint * @returns {Promise} Encrypted session key parameters. * @async */ -export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, data, fingerprint) { +export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, privateParams, data, fingerprint) { switch (keyAlgo) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: { @@ -76,6 +81,21 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, dat const C = ECDHXSymmetricKey.fromObject({ algorithm: symmetricAlgo, wrappedKey }); return { ephemeralPublicKey, C }; } + case enums.publicKey.aead: { + if (!privateParams) { + throw new Error('Cannot encrypt with symmetric key missing private parameters'); + } + const { cipher: algo } = publicParams; + const algoValue = algo.getValue(); + const { keyMaterial } = privateParams; + const aeadMode = config.preferredAEADAlgorithm; + const mode = getAEADMode(config.preferredAEADAlgorithm); + const { ivLength } = mode; + const iv = getRandomBytes(ivLength); + const modeInstance = await mode(algoValue, keyMaterial); + const c = await modeInstance.encrypt(data, iv, new Uint8Array()); + return { aeadMode: new AEADEnum(aeadMode), iv, c: new ShortByteString(c) }; + } default: return []; } @@ -128,6 +148,17 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams, return publicKey.elliptic.ecdhX.decrypt( algo, ephemeralPublicKey, C.wrappedKey, A, k); } + case enums.publicKey.aead: { + const { cipher: algo } = publicKeyParams; + const algoValue = algo.getValue(); + const { keyMaterial } = privateKeyParams; + + const { aeadMode, iv, c } = sessionKeyParams; + + const mode = getAEADMode(aeadMode.getValue()); + const modeInstance = await mode(algoValue, keyMaterial); + return modeInstance.decrypt(c.data, iv, new Uint8Array()); + } default: throw new Error('Unknown public key encryption algorithm.'); } @@ -192,6 +223,13 @@ export function parsePublicKeyParams(algo, bytes) { const A = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(algo)); read += A.length; return { read, publicParams: { A } }; } + case enums.publicKey.hmac: + case enums.publicKey.aead: { + const algo = new SymAlgoEnum(); read += algo.read(bytes); + const digestLength = hash.getHashByteLength(enums.hash.sha256); + const digest = bytes.subarray(read, read + digestLength); read += digestLength; + return { read: read, publicParams: { cipher: algo, digest } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -201,7 +239,7 @@ export function parsePublicKeyParams(algo, bytes) { * Parse private key material in binary form to get the key parameters * @param {module:enums.publicKey} algo - The key algorithm * @param {Uint8Array} bytes - The key material to parse - * @param {Object} publicParams - (ECC only) public params, needed to format some private params + * @param {Object} publicParams - (ECC and symmetric only) public params, needed to format some private params * @returns {{ read: Number, privateParams: Object }} Number of read bytes plus the key parameters referenced by name. */ export function parsePrivateKeyParams(algo, bytes, publicParams) { @@ -249,6 +287,20 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) { const k = util.readExactSubarray(bytes, read, read + payloadSize); read += k.length; return { read, privateParams: { k } }; } + case enums.publicKey.hmac: { + const { cipher: algo } = publicParams; + const keySize = hash.getHashByteLength(algo.getValue()); + const hashSeed = bytes.subarray(read, read + 32); read += 32; + const keyMaterial = bytes.subarray(read, read + keySize); read += keySize; + return { read, privateParams: { hashSeed, keyMaterial } }; + } + case enums.publicKey.aead: { + const { cipher: algo } = publicParams; + const hashSeed = bytes.subarray(read, read + 32); read += 32; + const { keySize } = getCipherParams(algo.getValue()); + const keyMaterial = bytes.subarray(read, read + keySize); read += keySize; + return { read, privateParams: { hashSeed, keyMaterial } }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -298,6 +350,20 @@ export function parseEncSessionKeyParams(algo, bytes) { const C = new ECDHXSymmetricKey(); C.read(bytes.subarray(read)); return { ephemeralPublicKey, C }; } + // Algorithm-Specific Fields for symmetric AEAD encryption: + // - AEAD algorithm + // - Starting initialization vector + // - Symmetric key encryption of "m" dependent on cipher and AEAD mode prefixed with a one-octet length + // - An authentication tag generated by the AEAD mode. + case enums.publicKey.aead: { + const aeadMode = new AEADEnum(); read += aeadMode.read(bytes.subarray(read)); + const { ivLength } = getAEADMode(aeadMode.getValue()); + + const iv = bytes.subarray(read, read + ivLength); read += ivLength; + const c = new ShortByteString(); read += c.read(bytes.subarray(read)); + + return { aeadMode, iv, c }; + } default: throw new UnsupportedError('Unknown public key encryption algorithm.'); } @@ -315,7 +381,9 @@ export function serializeParams(algo, params) { enums.publicKey.ed25519, enums.publicKey.x25519, enums.publicKey.ed448, - enums.publicKey.x448 + enums.publicKey.x448, + enums.publicKey.aead, + enums.publicKey.hmac ]); const orderedParams = Object.keys(params).map(name => { const param = params[name]; @@ -330,10 +398,11 @@ export function serializeParams(algo, params) { * @param {module:enums.publicKey} algo - The public key algorithm * @param {Integer} bits - Bit length for RSA keys * @param {module:type/oid} oid - Object identifier for ECC keys + * @param {module:enums.symmetric|enums.hash} symmetric - Hash or cipher algorithm for symmetric keys * @returns {Promise<{ publicParams: {Object}, privateParams: {Object} }>} The parameters referenced by name. * @async */ -export function generateParams(algo, bits, oid) { +export async function generateParams(algo, bits, oid, symmetric) { switch (algo) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: @@ -373,6 +442,14 @@ export function generateParams(algo, bits, oid) { privateParams: { k }, publicParams: { A } })); + case enums.publicKey.hmac: { + const keyMaterial = await publicKey.hmac.generate(symmetric); + return createSymmetricParams(keyMaterial, new HashEnum(symmetric)); + } + case enums.publicKey.aead: { + const keyMaterial = generateSessionKey(symmetric); + return createSymmetricParams(keyMaterial, new SymAlgoEnum(symmetric)); + } case enums.publicKey.dsa: case enums.publicKey.elgamal: throw new Error('Unsupported algorithm for key generation.'); @@ -381,6 +458,21 @@ export function generateParams(algo, bits, oid) { } } +async function createSymmetricParams(key, algo) { + const seed = getRandomBytes(32); + const bindingHash = await hash.sha256(seed); + return { + privateParams: { + hashSeed: seed, + keyMaterial: key + }, + publicParams: { + cipher: algo, + digest: bindingHash + } + }; +} + /** * Validate algorithm-specific key parameters * @param {module:enums.publicKey} algo - The public key algorithm @@ -435,6 +527,20 @@ export async function validateParams(algo, publicParams, privateParams) { const { k } = privateParams; return publicKey.elliptic.ecdhX.validateParams(algo, A, k); } + case enums.publicKey.hmac: { + const { cipher: algo, digest } = publicParams; + const { hashSeed, keyMaterial } = privateParams; + const keySize = hash.getHashByteLength(algo.getValue()); + return keySize === keyMaterial.length && + util.equalsUint8Array(digest, await hash.sha256(hashSeed)); + } + case enums.publicKey.aead: { + const { cipher: algo, digest } = publicParams; + const { hashSeed, keyMaterial } = privateParams; + const { keySize } = getCipherParams(algo.getValue()); + return keySize === keyMaterial.length && + util.equalsUint8Array(digest, await hash.sha256(hashSeed)); + } default: throw new Error('Unknown public key algorithm.'); } diff --git a/src/crypto/hash/index.js b/src/crypto/hash/index.js index 3332a1ce..9035a177 100644 --- a/src/crypto/hash/index.js +++ b/src/crypto/hash/index.js @@ -126,5 +126,26 @@ export default { default: throw new Error('Invalid hash algorithm.'); } + }, + + /** + * Returns the internal block size in bytes of the specified hash algorithm type + * @param {module:enums.hash} algo - Hash algorithm type (See {@link https://tools.ietf.org/html/rfc4880#section-9.4|RFC 4880 9.4}) + * @returns {Integer} Underlying block size in bytes. + */ + getBlockSize: function(algo) { + switch (algo) { + case enums.hash.md5: + case enums.hash.sha1: + case enums.hash.ripemd: + case enums.hash.sha224: + case enums.hash.sha256: + return 64; + case enums.hash.sha384: + case enums.hash.sha512: + return 128; + default: + throw new Error('Invalid hash algorithm.'); + } } }; diff --git a/src/crypto/public_key/hmac.js b/src/crypto/public_key/hmac.js new file mode 100644 index 00000000..f74f20d7 --- /dev/null +++ b/src/crypto/public_key/hmac.js @@ -0,0 +1,67 @@ +import enums from '../../enums'; +import util from '../../util'; + +const supportedHashAlgos = new Set([enums.hash.sha1, enums.hash.sha256, enums.hash.sha512]); + +const webCrypto = util.getWebCrypto(); +const nodeCrypto = util.getNodeCrypto(); + +export async function generate(hashAlgo) { + if (!supportedHashAlgos.has(hashAlgo)) { + throw new Error('Unsupported hash algorithm.'); + } + const hashName = enums.read(enums.webHash, hashAlgo); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const key = await crypto.generateKey( + { + name: 'HMAC', + hash: { name: hashName } + }, + true, + ['sign', 'verify'] + ); + const exportedKey = await crypto.exportKey('raw', key); + return new Uint8Array(exportedKey); +} + +export async function sign(hashAlgo, key, data) { + if (!supportedHashAlgos.has(hashAlgo)) { + throw new Error('Unsupported hash algorithm.'); + } + const hashName = enums.read(enums.webHash, hashAlgo); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const importedKey = await crypto.importKey( + 'raw', + key, + { + name: 'HMAC', + hash: { name: hashName } + }, + false, + ['sign'] + ); + const mac = await crypto.sign('HMAC', importedKey, data); + return new Uint8Array(mac); +} + +export async function verify(hashAlgo, key, mac, data) { + if (!supportedHashAlgos.has(hashAlgo)) { + throw new Error('Unsupported hash algorithm.'); + } + const hashName = enums.read(enums.webHash, hashAlgo); + + const crypto = webCrypto || nodeCrypto.webcrypto.subtle; + const importedKey = await crypto.importKey( + 'raw', + key, + { + name: 'HMAC', + hash: { name: hashName } + }, + false, + ['verify'] + ); + return crypto.verify('HMAC', importedKey, mac, data); +} diff --git a/src/crypto/public_key/index.js b/src/crypto/public_key/index.js index ffcf73cc..b20774ab 100644 --- a/src/crypto/public_key/index.js +++ b/src/crypto/public_key/index.js @@ -7,6 +7,7 @@ import * as rsa from './rsa'; import * as elgamal from './elgamal'; import * as elliptic from './elliptic'; import * as dsa from './dsa'; +import * as hmac from './hmac'; export default { /** @see module:crypto/public_key/rsa */ @@ -16,5 +17,7 @@ export default { /** @see module:crypto/public_key/elliptic */ elliptic: elliptic, /** @see module:crypto/public_key/dsa */ - dsa: dsa + dsa: dsa, + /** @see module:crypto/public_key/hmac */ + hmac: hmac }; diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 4f78a1ad..e8c04ba7 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -6,6 +6,7 @@ import publicKey from './public_key'; import enums from '../enums'; import util from '../util'; +import ShortByteString from '../type/short_byte_string'; import { UnsupportedError } from '../packet/packet'; /** @@ -65,7 +66,10 @@ export function parseSignatureParams(algo, signature) { const RS = util.readExactSubarray(signature, read, read + rsSize); read += RS.length; return { read, signatureParams: { RS } }; } - + case enums.publicKey.hmac: { + const mac = new ShortByteString(); read += mac.read(signature.subarray(read)); + return { read, signatureParams: { mac } }; + } default: throw new UnsupportedError('Unknown signature algorithm.'); } @@ -80,12 +84,13 @@ export function parseSignatureParams(algo, signature) { * @param {module:enums.hash} hashAlgo - Hash algorithm * @param {Object} signature - Named algorithm-specific signature parameters * @param {Object} publicParams - Algorithm-specific public key parameters + * @param {Object} privateParams - Algorithm-specific private key parameters * @param {Uint8Array} data - Data for which the signature was created * @param {Uint8Array} hashed - The hashed data * @returns {Promise} True if signature is valid. * @async */ -export async function verify(algo, hashAlgo, signature, publicParams, data, hashed) { +export async function verify(algo, hashAlgo, signature, publicParams, privateParams, data, hashed) { switch (algo) { case enums.publicKey.rsaEncryptSign: case enums.publicKey.rsaEncrypt: @@ -121,6 +126,14 @@ export async function verify(algo, hashAlgo, signature, publicParams, data, hash const { A } = publicParams; return publicKey.elliptic.eddsa.verify(algo, hashAlgo, signature, data, A, hashed); } + case enums.publicKey.hmac: { + if (!privateParams) { + throw new Error('Cannot verify HMAC signature with symmetric key missing private parameters'); + } + const { cipher: algo } = publicParams; + const { keyMaterial } = privateParams; + return publicKey.hmac.verify(algo.getValue(), keyMaterial, signature.mac.data, hashed); + } default: throw new Error('Unknown signature algorithm.'); } @@ -176,6 +189,12 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da const { seed } = privateKeyParams; return publicKey.elliptic.eddsa.sign(algo, hashAlgo, data, A, seed, hashed); } + case enums.publicKey.hmac: { + const { cipher: algo } = publicKeyParams; + const { keyMaterial } = privateKeyParams; + const mac = await publicKey.hmac.sign(algo.getValue(), keyMaterial, hashed); + return { mac: new ShortByteString(mac) }; + } default: throw new Error('Unknown signature algorithm.'); } diff --git a/src/enums.js b/src/enums.js index b9ea1e2b..14fd2f6b 100644 --- a/src/enums.js +++ b/src/enums.js @@ -95,7 +95,11 @@ export default { /** Ed25519 (Sign only) */ ed25519: 27, /** Ed448 (Sign only) */ - ed448: 28 + ed448: 28, + /** Persistent symmetric keys: encryption algorithm */ + aead: 100, + /** Persistent symmetric keys: authentication algorithm */ + hmac: 101 }, /** {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC4880, section 9.2} diff --git a/src/key/factory.js b/src/key/factory.js index 68cdfb95..2eebf2ec 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -75,6 +75,8 @@ function createKey(packetlist) { * @param {Date} options.date Creation date of the key and the key signatures * @param {Object} config - Full configuration * @param {Array} options.subkeys (optional) options for each subkey, default to main key options. e.g. [{sign: true, passphrase: '123'}] + * @param {String} options.symmetricHash (optional) hash algorithm for symmetric keys + * @param {String} options.symmetricCipher (optional) cipher algorithm for symmetric keys * sign parameter defaults to false, and indicates whether the subkey should sign rather than encrypt * @returns {Promise<{{ key: PrivateKey, revocationCertificate: String }}>} * @async diff --git a/src/key/helper.js b/src/key/helper.js index cd1643d5..f2c26ccd 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -17,7 +17,7 @@ export async function generateSecretSubkey(options, config) { const secretSubkeyPacket = new SecretSubkeyPacket(options.date, config); secretSubkeyPacket.packets = null; secretSubkeyPacket.algorithm = enums.write(enums.publicKey, options.algorithm); - await secretSubkeyPacket.generate(options.rsaBits, options.curve); + await secretSubkeyPacket.generate(options.rsaBits, options.curve, options.symmetric); await secretSubkeyPacket.computeFingerprintAndKeyID(); return secretSubkeyPacket; } @@ -26,7 +26,7 @@ export async function generateSecretKey(options, config) { const secretKeyPacket = new SecretKeyPacket(options.date, config); secretKeyPacket.packets = null; secretKeyPacket.algorithm = enums.write(enums.publicKey, options.algorithm); - await secretKeyPacket.generate(options.rsaBits, options.curve, options.config); + await secretKeyPacket.generate(options.rsaBits, options.curve, options.symmetric); await secretKeyPacket.computeFingerprintAndKeyID(); return secretKeyPacket; } @@ -388,6 +388,8 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { options.type = options.type || subkeyDefaults.type; options.curve = options.curve || subkeyDefaults.curve; options.rsaBits = options.rsaBits || subkeyDefaults.rsaBits; + options.symmetricHash = options.symmetricHash || subkeyDefaults.symmetricHash; + options.symmetricCipher = options.symmetricCipher || subkeyDefaults.symmetricCipher; options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime; options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase; options.date = options.date || subkeyDefaults.date; @@ -420,6 +422,23 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { case 'rsa': options.algorithm = enums.publicKey.rsaEncryptSign; break; + case 'symmetric': + if (options.sign) { + options.algorithm = enums.publicKey.hmac; + try { + options.symmetric = enums.write(enums.hash, options.symmetricHash); + } catch (e) { + throw new Error('Unknown hash algorithm'); + } + } else { + options.algorithm = enums.publicKey.aead; + try { + options.symmetric = enums.write(enums.symmetric, options.symmetricCipher); + } catch (e) { + throw new Error('Unknown symmetric algorithm'); + } + } + break; default: throw new Error(`Unsupported key type ${options.type}`); } @@ -435,6 +454,7 @@ export function validateSigningKeyPacket(keyPacket, signature, config) { case enums.publicKey.eddsaLegacy: case enums.publicKey.ed25519: case enums.publicKey.ed448: + case enums.publicKey.hmac: if (!signature.keyFlags && !config.allowMissingKeyFlags) { throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); } @@ -453,6 +473,7 @@ export function validateEncryptionKeyPacket(keyPacket, signature, config) { case enums.publicKey.ecdh: case enums.publicKey.x25519: case enums.publicKey.x448: + case enums.publicKey.aead: if (!signature.keyFlags && !config.allowMissingKeyFlags) { throw new Error('None of the key flags is set: consider passing `config.allowMissingKeyFlags`'); } diff --git a/src/key/private_key.js b/src/key/private_key.js index e82fa118..2a9b2f29 100644 --- a/src/key/private_key.js +++ b/src/key/private_key.js @@ -39,14 +39,28 @@ class PrivateKey extends PublicKey { toPublic() { const packetlist = new PacketList(); const keyPackets = this.toPacketList(); + let symmetricFound = false; for (const keyPacket of keyPackets) { + if (symmetricFound && + keyPacket.constructor.tag === enums.packet.Signature) { + continue; + } else if (symmetricFound) { + symmetricFound = false; + } switch (keyPacket.constructor.tag) { case enums.packet.secretKey: { + if (keyPacket.algorithm === enums.publicKey.aead || keyPacket.algorithm === enums.publicKey.hmac) { + throw new Error('Cannot create public key from symmetric private'); + } const pubKeyPacket = PublicKeyPacket.fromSecretKeyPacket(keyPacket); packetlist.push(pubKeyPacket); break; } case enums.packet.secretSubkey: { + if (keyPacket.algorithm === enums.publicKey.aead || keyPacket.algorithm === enums.publicKey.hmac) { + symmetricFound = true; + break; + } const pubSubkeyPacket = PublicSubkeyPacket.fromSecretSubkeyPacket(keyPacket); packetlist.push(pubSubkeyPacket); break; @@ -219,10 +233,12 @@ class PrivateKey extends PublicKey { * Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added. * Supports RSA and ECC keys, as well as the newer Curve448 and Curve25519. * Defaults to the algorithm and bit size/curve of the primary key. DSA primary keys default to RSA subkeys. - * @param {ecc|rsa|curve25519|curve448} options.type The subkey algorithm: ECC, RSA, Curve448 or Curve25519 (new format). + * @param {ecc|rsa|curve25519|curve448|symmetric} options.type The subkey algorithm: ECC, RSA, Curve448 or Curve25519 (new format). * Note: Curve448 and Curve25519 are not widely supported yet. * @param {String} options.curve (optional) Elliptic curve for ECC keys * @param {Integer} options.rsaBits (optional) Number of bits for RSA subkeys + * @param {String} options.symmetricCipher (optional) Symmetric algorithm for persistent symmetric aead keys + * @param {String} options.symmetricHash (optional)Hash lgorithm for persistent symmetric hmac keys * @param {Number} options.keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires * @param {Date} options.date (optional) Override the creation date of the key and the key signatures * @param {Boolean} options.sign (optional) Indicates whether the subkey should sign rather than encrypt. Defaults to false diff --git a/src/openpgp.js b/src/openpgp.js index 22ac9077..7073166c 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -37,13 +37,15 @@ import { checkKeyRequirements } from './key/helper'; * The generated primary key will have signing capabilities. By default, one subkey with encryption capabilities is also generated. * @param {Object} options * @param {Object|Array} options.userIDs - User IDs as objects: `{ name: 'Jo Doe', email: 'info@jo.com' }` - * @param {'ecc'|'rsa'|'curve448'|'curve25519'} [options.type='ecc'] - The primary key algorithm type: ECC (default for v4 keys), RSA, Curve448 or Curve25519 (new format, default for v6 keys). + * @param {'ecc'|'rsa'|'curve448'|'curve25519'|'symmetric'} [options.type='ecc'] - The primary key algorithm type: ECC (default for v4 keys), RSA, Curve448, Curve25519 (new format, default for v6 keys) or symmetric. * Note: Curve448 and Curve25519 (new format) are not widely supported yet. * @param {String} [options.passphrase=(not protected)] - The passphrase used to encrypt the generated private key. If omitted or empty, the key won't be encrypted. * @param {Number} [options.rsaBits=4096] - Number of bits for RSA keys * @param {String} [options.curve='curve25519Legacy'] - Elliptic curve for ECC keys: * curve25519Legacy (default), nistP256, nistP384, nistP521, secp256k1, * brainpoolP256r1, brainpoolP384r1, or brainpoolP512r1 +* @param {String} options.symmetricCipher (optional) Symmetric algorithm for persistent symmetric aead keys +* @param {String} options.symmetricHash (optional)Hash lgorithm for persistent symmetric hmac keys * @param {Date} [options.date=current date] - Override the creation date of the key and the key signatures * @param {Number} [options.keyExpirationTime=0 (never expires)] - Number of seconds from the key creation time after which the key expires * @param {Array} [options.subkeys=a single encryption subkey] - Options for each subkey e.g. `[{sign: true, passphrase: '123'}]` @@ -55,7 +57,7 @@ import { checkKeyRequirements } from './key/helper'; * @async * @static */ -export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBits = 4096, keyExpirationTime = 0, date = new Date(), subkeys = [{}], format = 'armored', config, ...rest }) { +export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBits = 4096, symmetricHash = 'sha256', symmetricCipher = 'aes256', keyExpirationTime = 0, date = new Date(), subkeys = [{}], format = 'armored', config, ...rest }) { config = { ...defaultConfig, ...config }; checkConfig(config); if (!type && !curve) { type = config.v6Keys ? 'curve25519' : 'ecc'; // default to new curve25519 for v6 keys (legacy curve25519 cannot be used with them) @@ -74,7 +76,7 @@ export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBi throw new Error(`rsaBits should be at least ${config.minRSABits}, got: ${rsaBits}`); } - const options = { userIDs, passphrase, type, rsaBits, curve, keyExpirationTime, date, subkeys }; + const options = { userIDs, passphrase, type, rsaBits, curve, keyExpirationTime, date, subkeys, symmetricHash, symmetricCipher }; try { const { key, revocationCertificate } = await generate(options, config); @@ -82,7 +84,7 @@ export async function generateKey({ userIDs = [], passphrase, type, curve, rsaBi return { privateKey: formatObject(key, format, config), - publicKey: formatObject(key.toPublic(), format, config), + publicKey: type !== 'symmetric' ? formatObject(key.toPublic(), format, config) : null, revocationCertificate }; } catch (err) { diff --git a/src/packet/public_key.js b/src/packet/public_key.js index c3b13071..a7de9be9 100644 --- a/src/packet/public_key.js +++ b/src/packet/public_key.js @@ -264,7 +264,7 @@ class PublicKeyPacket { /** * Returns algorithm information - * @returns {Object} An object of the form {algorithm: String, bits:int, curve:String}. + * @returns {Object} An object of the form {algorithm: String, bits:int, curve:String, symmetric:String}. */ getAlgorithmInfo() { const result = {}; @@ -275,6 +275,8 @@ class PublicKeyPacket { result.bits = util.uint8ArrayBitLength(modulo); } else if (this.publicParams.oid) { result.curve = this.publicParams.oid.getName(); + } else if (this.publicParams.cipher) { + result.symmetric = this.publicParams.cipher.getName(); } return result; } diff --git a/src/packet/public_key_encrypted_session_key.js b/src/packet/public_key_encrypted_session_key.js index eaa5dacd..00013d7b 100644 --- a/src/packet/public_key_encrypted_session_key.js +++ b/src/packet/public_key_encrypted_session_key.js @@ -182,8 +182,9 @@ class PublicKeyEncryptedSessionKeyPacket { const sessionKeyAlgorithm = this.version === 3 ? this.sessionKeyAlgorithm : null; const fingerprint = key.version === 5 ? key.getFingerprintBytes().subarray(0, 20) : key.getFingerprintBytes(); const encoded = encodeSessionKey(this.version, algo, sessionKeyAlgorithm, this.sessionKey); + const privateParams = algo === enums.publicKey.aead ? key.privateParams : null; this.encrypted = await crypto.publicKeyEncrypt( - algo, sessionKeyAlgorithm, key.publicParams, encoded, fingerprint); + algo, sessionKeyAlgorithm, key.publicParams, privateParams, encoded, fingerprint); } /** @@ -230,6 +231,7 @@ function encodeSessionKey(version, keyAlgo, cipherAlgo, sessionKeyData) { case enums.publicKey.rsaEncryptSign: case enums.publicKey.elgamal: case enums.publicKey.ecdh: + case enums.publicKey.aead: // add checksum return util.concatUint8Array([ new Uint8Array(version === 6 ? [] : [cipherAlgo]), @@ -250,7 +252,8 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) { case enums.publicKey.rsaEncrypt: case enums.publicKey.rsaEncryptSign: case enums.publicKey.elgamal: - case enums.publicKey.ecdh: { + case enums.publicKey.ecdh: + case enums.publicKey.aead: { // verify checksum in constant time const result = decryptedData.subarray(0, decryptedData.length - 2); const checksum = decryptedData.subarray(decryptedData.length - 2); diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index ddddae5f..70a16b8e 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -523,7 +523,7 @@ class SecretKeyPacket extends PublicKeyPacket { } } - async generate(bits, curve) { + async generate(bits, curve, symmetric) { // The deprecated OIDs for Ed25519Legacy and Curve25519Legacy are used in legacy version 4 keys and signatures. // Implementations MUST NOT accept or generate v6 key material using the deprecated OIDs. if (this.version === 6 && ( @@ -532,7 +532,7 @@ class SecretKeyPacket extends PublicKeyPacket { )) { throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${curve}. Generate a key of type 'curve25519' instead`); } - const { privateParams, publicParams } = await crypto.generateParams(this.algorithm, bits, curve); + const { privateParams, publicParams } = await crypto.generateParams(this.algorithm, bits, curve, symmetric); this.privateParams = privateParams; this.publicParams = publicParams; this.isEncrypted = false; diff --git a/src/packet/signature.js b/src/packet/signature.js index 87e0d304..61965ba7 100644 --- a/src/packet/signature.js +++ b/src/packet/signature.js @@ -781,9 +781,10 @@ class SignaturePacket { } this.params = await this.params; + const privateParams = this.publicKeyAlgorithm === enums.publicKey.hmac ? key.privateParams : null; this[verified] = await crypto.signature.verify( - this.publicKeyAlgorithm, this.hashAlgorithm, this.params, key.publicParams, + this.publicKeyAlgorithm, this.hashAlgorithm, this.params, key.publicParams, privateParams, toHash, hash ); diff --git a/src/type/enum.js b/src/type/enum.js new file mode 100644 index 00000000..e697f30c --- /dev/null +++ b/src/type/enum.js @@ -0,0 +1,78 @@ +// OpenPGP.js - An OpenPGP implementation in javascript +// Copyright (C) 2015-2016 Decentral +// +// 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 + + +/** + * Wrapper for enums + * + * @requires enums + * @module type/enum.js + */ + + +import enums from '../enums'; + +const type_enum = type => class EnumType { + constructor(data) { + if (typeof data === 'undefined') { + this.data = null; + } else { + this.data = data; + } + } + + /** + * Read an enum entry + * @param {Uint8Array} input Where to read the symmetric algo from + */ + read(input) { + const data = input[0]; + this.data = enums.write(type, data); + return 1; + } + + /** + * Write an enum as an integer + * @returns {Uint8Array} An integer representing the algorithm + */ + write() { + return new Uint8Array([this.data]); + } + + /** + * Get the name of the enum entry + * @returns {string} The name string + */ + getName() { + return enums.read(type, this.data); + } + + /** + * Get the integer value of the enum entry + * @returns {Number} The integer value + */ + getValue() { + return this.data; + } +}; +const AEADEnum = type_enum(enums.aead); +const SymAlgoEnum = type_enum(enums.symmetric); +const HashEnum = type_enum(enums.hash); + +export { AEADEnum, SymAlgoEnum, HashEnum }; + +export default type_enum; diff --git a/src/type/short_byte_string.js b/src/type/short_byte_string.js new file mode 100644 index 00000000..1611e16f --- /dev/null +++ b/src/type/short_byte_string.js @@ -0,0 +1,32 @@ +import util from '../util'; + +class ShortByteString { + constructor(data) { + if (typeof data === 'undefined') { + data = new Uint8Array([]); + } + if (!util.isUint8Array(data)) { + throw new Error('data must be in the form of a Uint8Array'); + } + this.data = data; + this.length = this.data.byteLength; + } + + write() { + return util.concatUint8Array([new Uint8Array([this.length]), this.data]); + } + + read(input) { + if (input.length >= 1) { + const length = input[0]; + if (input.length >= length + 1) { + this.data = input.subarray(1, 1 + length); + this.length = length; + return 1 + length; + } + } + throw new Error('Invalid octet string'); + } +} + +export default ShortByteString; diff --git a/test/crypto/crypto.js b/test/crypto/crypto.js index c6bd5828..20a46b8e 100644 --- a/test/crypto/crypto.js +++ b/test/crypto/crypto.js @@ -214,7 +214,7 @@ export default () => describe('API functional testing', function() { openpgp.enums.publicKey.rsaEncryptSign, openpgp.enums.hash.sha1, RSAPublicParams, RSAPrivateParams, data, await crypto.hash.digest(2, data) ); const success = await crypto.signature.verify( - openpgp.enums.publicKey.rsaEncryptSign, openpgp.enums.hash.sha1, RSAsignedData, RSAPublicParams, data, await crypto.hash.digest(2, data) + openpgp.enums.publicKey.rsaEncryptSign, openpgp.enums.hash.sha1, RSAsignedData, RSAPublicParams, null, data, await crypto.hash.digest(2, data) ); return expect(success).to.be.true; }); @@ -224,7 +224,7 @@ export default () => describe('API functional testing', function() { openpgp.enums.publicKey.dsa, openpgp.enums.hash.sha1, DSAPublicParams, DSAPrivateParams, data, await crypto.hash.digest(2, data) ); const success = await crypto.signature.verify( - openpgp.enums.publicKey.dsa, openpgp.enums.hash.sha1, DSAsignedData, DSAPublicParams, data, await crypto.hash.digest(2, data) + openpgp.enums.publicKey.dsa, openpgp.enums.hash.sha1, DSAsignedData, DSAPublicParams, null, data, await crypto.hash.digest(2, data) ); return expect(success).to.be.true; @@ -239,7 +239,7 @@ export default () => describe('API functional testing', function() { openpgp.enums.publicKey.ed448, openpgp.enums.hash.sha512, { A }, { seed }, data, toSign ); const success = await crypto.signature.verify( - openpgp.enums.publicKey.ed448, openpgp.enums.hash.sha512, signedData, { A }, data, toSign + openpgp.enums.publicKey.ed448, openpgp.enums.hash.sha512, signedData, { A }, null, data, toSign ); return expect(success).to.be.true; @@ -271,7 +271,7 @@ export default () => describe('API functional testing', function() { it('Asymmetric using RSA with eme_pkcs1 padding', function () { const symmKey = crypto.generateSessionKey(openpgp.enums.symmetric.aes256); - return crypto.publicKeyEncrypt(algoRSA, openpgp.enums.symmetric.aes256, RSAPublicParams, symmKey).then(RSAEncryptedData => { + return crypto.publicKeyEncrypt(algoRSA, openpgp.enums.symmetric.aes256, RSAPublicParams, null, symmKey).then(RSAEncryptedData => { return crypto.publicKeyDecrypt( algoRSA, RSAPublicParams, RSAPrivateParams, RSAEncryptedData ).then(data => { @@ -282,7 +282,7 @@ export default () => describe('API functional testing', function() { it('Asymmetric using Elgamal with eme_pkcs1 padding', function () { const symmKey = crypto.generateSessionKey(openpgp.enums.symmetric.aes256); - return crypto.publicKeyEncrypt(algoElGamal, openpgp.enums.symmetric.aes256, elGamalPublicParams, symmKey).then(ElgamalEncryptedData => { + return crypto.publicKeyEncrypt(algoElGamal, openpgp.enums.symmetric.aes256, elGamalPublicParams, null, symmKey).then(ElgamalEncryptedData => { return crypto.publicKeyDecrypt( algoElGamal, elGamalPublicParams, elGamalPrivateParams, ElgamalEncryptedData ).then(data => { diff --git a/test/crypto/hmac.js b/test/crypto/hmac.js new file mode 100644 index 00000000..61b62e14 --- /dev/null +++ b/test/crypto/hmac.js @@ -0,0 +1,34 @@ +import { use as chaiUse, expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/newline-after-import +chaiUse(chaiAsPromised); + +import { sign, verify } from '../../src/crypto/public_key/hmac'; +import enums from '../../src/enums'; +import util from '../../src/util'; + +export default () => describe('HMAC', function () { + it('Test vectors', async function() { + const vectors = [ + { + algo: enums.hash.sha256, + key: util.hexToUint8Array('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), + data: util.hexToUint8Array('4869205468657265'), + expected: util.hexToUint8Array('b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7') + }, + { + algo: enums.hash.sha512, + key: util.hexToUint8Array('4a656665'), + data: util.hexToUint8Array('7768617420646f2079612077616e7420666f72206e6f7468696e673f'), + expected: util.hexToUint8Array('164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737') + } + ]; + + await Promise.all(vectors.map(async vec => { + const res = await sign(vec.algo, vec.key, vec.data); + const verified = await verify(vec.algo, vec.key, vec.expected, vec.data); + expect(util.equalsUint8Array(res, vec.expected)).to.be.true; + expect(verified).to.be.true; + })); + }); +}); + diff --git a/test/crypto/index.js b/test/crypto/index.js index a4c73ae5..ac315a06 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -8,6 +8,7 @@ import testECDH from './ecdh'; import testPKCS5 from './pkcs5'; import testAESKW from './aes_kw'; import testHKDF from './hkdf'; +import testHMAC from './hmac'; import testGCM from './gcm'; import testEAX from './eax'; import testOCB from './ocb'; @@ -25,6 +26,7 @@ export default () => describe('Crypto', function () { testPKCS5(); testAESKW(); testHKDF(); + testHMAC(); testGCM(); testEAX(); testOCB(); diff --git a/test/general/key.js b/test/general/key.js index ff94ecab..3b51826c 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -4728,6 +4728,71 @@ I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg== await subkey.verify(); }); + it('create and add a new symmetric subkey to a rsa key', async function() { + const privateKey = await openpgp.decryptKey({ + privateKey: await openpgp.readKey({ armoredKey: priv_key_rsa }), + passphrase: 'hello world' + }); + const total = privateKey.getSubkeys().length; + const opt2 = { type: 'symmetric', symmetricCipher: 'aes256' }; + let newPrivateKey = await privateKey.addSubkey(opt2); + const armoredKey = await newPrivateKey.armor(); + newPrivateKey = await openpgp.readKey({ armoredKey: armoredKey }); + const subKey = newPrivateKey.getSubkeys()[total]; + expect(subKey).to.exist; + expect(newPrivateKey.getSubkeys().length).to.be.equal(total + 1); + expect(subKey.getAlgorithmInfo().symmetric).to.be.equal('aes256'); + expect(subKey.keyPacket.publicParams.digest).to.exist.and.to.have.length(32); + expect(subKey.keyPacket.privateParams.keyMaterial).to.exist.and.to.have.length(32); + await subKey.verify(privateKey.primaryKey); + }); + + it('create and add a new rsa key with a symmetric encryption subkey', async function() { + const userId = { name: 'test', email: 'a@b.com' }; + const opt = { rsaBits: 512, userIDs: [userId], subkeys:[{ type: 'symmetric', symmetricCipher: 'aes256' }], format: 'object' }; + + const { privateKey } = await openpgp.generateKey(opt); + const armoredKey = await privateKey.armor(); + const newKey = await openpgp.readKey({ armoredKey: armoredKey }); + const subKey = newKey.getSubkeys()[0]; + expect(newKey.getSubkeys().length).to.be.equal(1); + expect(subKey).to.exist; + expect(subKey.getAlgorithmInfo().symmetric).to.be.equal('aes256'); + expect(subKey.keyPacket.publicParams.cipher).to.exist; + expect(subKey.keyPacket.publicParams.cipher.getValue()).to.be.equal(openpgp.enums.symmetric.aes256); + expect(subKey.keyPacket.publicParams.digest).to.exist.and.to.have.length(32); + expect(subKey.keyPacket.privateParams.keyMaterial).to.exist.and.to.have.length(32); + await subKey.verify(newKey.primaryKey); + }); + + it('create and add a new encrypted rsa key with a symmetric encryption subkey', async function() { + const userId = { name: 'test', email: 'a@b.com' }; + const opt = { rsaBits: 512, userIDs: [userId], subkeys:[{ type: 'symmetric', symmetricCipher: 'aes256' }], format: 'object' }; + + const { privateKey } = await openpgp.generateKey(opt); + const subKey = privateKey.getSubkeys()[0]; + expect(privateKey.getSubkeys().length).to.be.equal(1); + expect(subKey).to.exist; + expect(subKey.getAlgorithmInfo().symmetric).to.be.equal('aes256'); + expect(subKey.keyPacket.publicParams.cipher).to.exist; + expect(subKey.keyPacket.publicParams.cipher.getValue()).to.be.equal(openpgp.enums.symmetric.aes256); + expect(subKey.keyPacket.publicParams.digest).to.exist.and.to.have.length(32); + expect(subKey.keyPacket.privateParams.keyMaterial).to.exist.and.to.have.length(32); + await subKey.verify(privateKey.primaryKey); + }); + + it('create and add a symmetric signing key', async function() { + const opt = { userIDs: [{ name: 'test', email: 'a@b.com' }], type: 'symmetric', symmetricHash: 'sha256', format: 'object' }; + const { privateKey } = await openpgp.generateKey(opt); + const signed = await openpgp.sign({ message: await openpgp.createMessage({ text: 'the data to be signed' }), signingKeys: privateKey, format: 'binary' }); + const message = await openpgp.readMessage({ binaryMessage: signed }); + const { signatures } = await openpgp.verify({ message, verificationKeys: [privateKey], expectSigned: true }); + expect(signatures).to.exist; + expect(signatures.length).to.be.equal(1); + expect(signatures[0].keyID.toHex()).to.be.equal(privateKey.getKeyID().toHex()); + expect(await signatures[0].verified).to.be.true; + }); + it('sign/verify data with the new subkey correctly using curve25519 (legacy format)', async function() { const userID = { name: 'test', email: 'a@b.com' }; const opt = { curve: 'curve25519', userIDs: [userID], format: 'object', subkeys:[] }; diff --git a/test/general/packet.js b/test/general/packet.js index e977ebab..a85ab8a1 100644 --- a/test/general/packet.js +++ b/test/general/packet.js @@ -1225,6 +1225,119 @@ V+HOQJQxXJkVRYa3QrFUehiMzTeqqMdgC6ZqJy7+ }); }); + describe('Persistent symmetric keys', function() { + it('Parse persistent key encrypted session key packet (PKESK encrypted with persistent symmetric key)', () => { + const serializedPacket = new Uint8Array([ + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x64, 0x01, 0x4c, 0x80, 0x3d, 0x9b, 0xb6, + 0x44, 0x13, 0x25, 0x90, 0x24, 0x6e, 0x59, 0x0d, + 0x68, 0xee, 0x1e, 0x23, 0x75, 0x8a, 0x90, 0x85, + 0x78, 0x1b, 0xf7, 0xc4, 0x4d, 0x58, 0xc7, 0x64, + 0xd2, 0xe5, 0xb3, 0x4f, 0xf6, 0x6e, 0xef, 0x53, + 0xc4, 0xc3, 0x76, 0x7b, 0xba, 0xf9, 0x03, 0x86, + 0xee, 0xc0, 0x9d, 0x60, 0x23, 0xa6, 0x8a + ]); + const expectedIV = new Uint8Array([ + 0x4c, 0x80, 0x3d, 0x9b, 0xb6, 0x44, 0x13, 0x25, + 0x90, 0x24, 0x6e, 0x59, 0x0d, 0x68, 0xee, 0x1e + ]); + const expectedCiphertext = new Uint8Array([ + 0x75, 0x8a, 0x90, 0x85, 0x78, 0x1b, 0xf7, 0xc4, + 0x4d, 0x58, 0xc7, 0x64, 0xd2, 0xe5, 0xb3, 0x4f, + 0xf6, 0x6e, 0xef, 0x53, 0xc4, 0xc3, 0x76, 0x7b, + 0xba, 0xf9, 0x03, 0x86, 0xee, 0xc0, 0x9d, 0x60, + 0x23, 0xa6, 0x8a + ]); + const packet = new openpgp.PublicKeyEncryptedSessionKeyPacket(); + packet.read(serializedPacket); + + expect(packet.publicKeyAlgorithm).to.equal(openpgp.enums.publicKey.aead); + expect(packet.encrypted.aeadMode.data).to.equal(openpgp.enums.aead.eax); + expect(util.equalsUint8Array(packet.encrypted.iv, expectedIV)).to.be.true; + expect(util.equalsUint8Array(packet.encrypted.c.data, expectedCiphertext)).to.be.true; + }); + + it('Parse signature packet from persistent symmetric key', () => { + const serializedPacket = new Uint8Array([ + 0x04, 0x00, 0x65, 0x08, 0x00, 0x10, 0x05, 0x02, + 0x60, 0x64, 0x52, 0x28, 0x09, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x6b, 0x1d, 0x40, 0x76, 0xf9, 0x35, 0xf8, + 0xe4, 0xc5, 0x2f, 0x49, 0xce, 0xf7, 0x91, 0x23, + 0xb4, 0x00, 0x3b, 0x77, 0x92, 0x60, 0x2a, 0xfe, + 0x2e, 0x4c, 0xf5, 0x5f, 0x6c, 0x75, 0x80, 0x5a, + 0x3a, 0xf4, 0x38, 0x09, 0x22, 0x97, 0x1a, 0x01, + 0x2e, 0x8a, 0x59, 0x86, 0x26, 0x77, 0x79, 0xe1, + 0xb7, 0xd7, 0x4d, 0x7c, 0xa6, 0x61, 0xe7, 0x00, + 0xf1, 0x4b, 0xa4, 0x5b, 0xc8, 0x3a, 0xe2, 0x35, + 0x9e, 0x67, 0x4a, 0x44 + ]); + + const expectedMAC = new Uint8Array([ + 0x76, 0xf9, 0x35, 0xf8, 0xe4, 0xc5, 0x2f, 0x49, + 0xce, 0xf7, 0x91, 0x23, 0xb4, 0x00, 0x3b, 0x77, + 0x92, 0x60, 0x2a, 0xfe, 0x2e, 0x4c, 0xf5, 0x5f, + 0x6c, 0x75, 0x80, 0x5a, 0x3a, 0xf4, 0x38, 0x09, + 0x22, 0x97, 0x1a, 0x01, 0x2e, 0x8a, 0x59, 0x86, + 0x26, 0x77, 0x79, 0xe1, 0xb7, 0xd7, 0x4d, 0x7c, + 0xa6, 0x61, 0xe7, 0x00, 0xf1, 0x4b, 0xa4, 0x5b, + 0xc8, 0x3a, 0xe2, 0x35, 0x9e, 0x67, 0x4a, 0x44 + ]); + + const packet = new openpgp.SignaturePacket(); + packet.read(serializedPacket); + + const readMAC = packet.params.mac.data; + + expect(packet.publicKeyAlgorithm).to.equal(openpgp.enums.publicKey.hmac); + expect(util.equalsUint8Array(readMAC, expectedMAC)).to.be.true; + }); + + it('encrypt/decrypt using persistent symmetric key', async () => { + const persistentSymmetricKeyPacket = new openpgp.SecretSubkeyPacket(); + persistentSymmetricKeyPacket.version = 4; + persistentSymmetricKeyPacket.algorithm = openpgp.enums.publicKey.aead; + await persistentSymmetricKeyPacket.generate(null, null, openpgp.enums.symmetric.aes256); + await persistentSymmetricKeyPacket.computeFingerprintAndKeyID(); + + const sessionKey = 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 pkesk = openpgp.PublicKeyEncryptedSessionKeyPacket.fromObject({ + version: 3, + encryptionKeyPacket: persistentSymmetricKeyPacket, + sessionKey, + sessionKeyAlgorithm: openpgp.enums.symmetric.aes256 + }); + await pkesk.encrypt(persistentSymmetricKeyPacket); + + const parsedPKESK = new openpgp.PublicKeyEncryptedSessionKeyPacket(); + parsedPKESK.read(pkesk.write()); + await parsedPKESK.decrypt(persistentSymmetricKeyPacket); + expect(util.equalsUint8Array(sessionKey, parsedPKESK.sessionKey)).to.be.true; + }); + + it('throws when encrypting with encrypted symmetric key', async () => { + const persistentSymmetricKeyPacket = new openpgp.SecretSubkeyPacket(); + persistentSymmetricKeyPacket.version = 4; + persistentSymmetricKeyPacket.algorithm = openpgp.enums.publicKey.aead; + await persistentSymmetricKeyPacket.generate(null, null, openpgp.enums.symmetric.aes256); + await persistentSymmetricKeyPacket.computeFingerprintAndKeyID(); + await persistentSymmetricKeyPacket.encrypt('passphrase'); + await persistentSymmetricKeyPacket.clearPrivateParams(); + + const sessionKey = 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 pkesk = openpgp.PublicKeyEncryptedSessionKeyPacket.fromObject({ + version: 3, + encryptionKeyPacket: persistentSymmetricKeyPacket, + sessionKey, + sessionKeyAlgorithm: openpgp.enums.symmetric.aes256 + }); + + await expect(pkesk.encrypt(persistentSymmetricKeyPacket)).to.be.rejectedWith('Cannot encrypt with symmetric key missing private parameters'); + }); + }); + describe('PacketList parsing', function () { it('Ignores unknown packet version with `config.ignoreUnsupportedPackets` enabled', async function() { const armoredSignature = `-----BEGIN PGP SIGNATURE----- diff --git a/test/general/x25519.js b/test/general/x25519.js index e0cc8628..788f5481 100644 --- a/test/general/x25519.js +++ b/test/general/x25519.js @@ -241,7 +241,7 @@ export default () => describe('X25519 Cryptography (legacy format)', function () expect(R).to.deep.eq(r); expect(S).to.deep.eq(s); }), - signature.verify(22, openpgp.enums.hash.sha256, { r: R, s: S }, publicParams, undefined, data).then(result => { + signature.verify(22, openpgp.enums.hash.sha256, { r: R, s: S }, publicParams, null, undefined, data).then(result => { expect(result).to.be.true; }) ]);