Add verify method to message class and other improvements. Implement openpgp.decryptAndVerifyMessage. Allow parsing of unhashed signature subpackets.

This commit is contained in:
Thomas Oberndörfer 2013-11-30 17:29:20 +01:00
parent b0ea97ec28
commit c2a79368dc
8 changed files with 729 additions and 134 deletions

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,7 @@ var enums = require('./enums.js');
var armor = require('./encoding/armor.js');
var config = require('./config');
var crypto = require('./crypto');
var util = require('./util');
/**
* @class
@ -31,7 +32,7 @@ function message(packetlist) {
this.packets = packetlist || new packet.list();
/**
* Returns the key IDs of the public keys to which the session key is encrypted
* Returns the key IDs of the keys to which the session key is encrypted
* @return {[keyId]} array of keyid objects
*/
this.getEncryptionKeyIds = function() {
@ -43,6 +44,28 @@ function message(packetlist) {
return keyIds;
}
/**
* Returns the key IDs of the keys that signed the message
* @return {[keyId]} array of keyid objects
*/
this.getSigningKeyIds = function() {
var keyIds = [];
var msg = this.unwrapCompressed();
// search for one pass signatures
var onePassSigList = msg.packets.filterByTag(enums.packet.one_pass_signature);
onePassSigList.forEach(function(packet) {
keyIds.push(packet.signingKeyId);
});
// if nothing found look for signature packets
if (!keyIds.length) {
var signatureList = msg.packets.filterByTag(enums.packet.signature);
signatureList.forEach(function(packet) {
keyIds.push(packet.issuerKeyId);
});
}
return keyIds;
}
/**
* Decrypt the message
* @param {key} privateKey private key with decrypted secret data
@ -79,11 +102,30 @@ function message(packetlist) {
* Get literal data that is the body of the message
* @return {String|null} literal body of the message as string
*/
this.getLiteral = function() {
this.getLiteralData = function() {
var literal = this.packets.findPacket(enums.packet.literal);
return literal && literal.data || null;
}
/**
* Get literal data as text
* @return {String|null} literal body of the message interpreted as text
*/
this.getText = function() {
var literal = this.packets.findPacket(enums.packet.literal);
if (literal) {
var data = literal.data;
if (literal.format == enums.read(enums.literal, enums.literal.binary)
|| literal.format == enums.read(enums.literal, enums.literal.text)) {
// text in a literal packet with format 'binary' or 'text' could be utf8, therefore decode
data = util.decode_utf8(data);
}
return data;
} else {
return null;
}
}
/**
* Encrypt the message
* @param {[key]} keys array of keys, used to encrypt the message
@ -121,37 +163,93 @@ function message(packetlist) {
/**
* Sign the message (the literal data packet of the message)
* @param {key} privateKey private key with decrypted secret key data for signing
* @return {[message]} new message with encrypted content
* @param {[key]} privateKey private keys with decrypted secret key data for signing
* @return {message} new message with signed content
*/
this.sign = function(privateKey) {
this.sign = function(privateKeys) {
var packetlist = new packet.list();
var onePassSig = new packet.one_pass_signature();
onePassSig.type = enums.signature.text;
//TODO get preferred hashg algo from signature
onePassSig.hashAlgorithm = config.prefer_hash_algorithm;
var signingKeyPacket = privateKey.getSigningKeyPacket();
onePassSig.publicKeyAlgorithm = signingKeyPacket.algorithm;
onePassSig.signingKeyId = signingKeyPacket.getKeyId();
packetlist.push(onePassSig);
var literalDataPacket = this.packets.findPacket(enums.packet.literal);
if (!literalDataPacket) throw new Error('No literal data packet to sign.');
packetlist.push(literalDataPacket);
var literalFormat = enums.write(enums.literal, literalDataPacket.format);
var signatureType = literalFormat == enums.literal.binary
? enums.signature.binary : enums.signature.text;
for (var i = 0; i < privateKeys.length; i++) {
var onePassSig = new packet.one_pass_signature();
onePassSig.type = signatureType;
//TODO get preferred hashg algo from key signature
onePassSig.hashAlgorithm = config.prefer_hash_algorithm;
var signingKeyPacket = privateKeys[i].getSigningKeyPacket();
onePassSig.publicKeyAlgorithm = signingKeyPacket.algorithm;
onePassSig.signingKeyId = signingKeyPacket.getKeyId();
packetlist.push(onePassSig);
}
var signaturePacket = new packet.signature();
signaturePacket.signatureType = enums.signature.text;
signaturePacket.hashAlgorithm = config.prefer_hash_algorithm;
signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm;
if (!signingKeyPacket.isDecrypted) throw new Error('Private key is not decrypted.');
signaturePacket.sign(signingKeyPacket, literalDataPacket);
packetlist.push(signaturePacket);
packetlist.push(literalDataPacket);
for (var i = privateKeys.length - 1; i >= 0; i--) {
var signaturePacket = new packet.signature();
signaturePacket.signatureType = signatureType;
signaturePacket.hashAlgorithm = config.prefer_hash_algorithm;
signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm;
if (!signingKeyPacket.isDecrypted) throw new Error('Private key is not decrypted.');
signaturePacket.sign(signingKeyPacket, literalDataPacket);
packetlist.push(signaturePacket);
}
return new message(packetlist);
}
/**
* Verify message signatures
* @param {[key]} publicKeys public keys to verify signatures
* @return {[{'keyid': keyid, 'valid': Boolean}]} list of signer's keyid and validity of signature
*/
this.verify = function(publicKeys) {
var result = [];
var msg = this.unwrapCompressed();
var literalDataList = msg.packets.filterByTag(enums.packet.literal);
if (literalDataList.length !== 1) throw new Error('Can only verify message with one literal data packet.');
var signatureList = msg.packets.filterByTag(enums.packet.signature);
publicKeys.forEach(function(pubKey) {
for (var i = 0; i < signatureList.length; i++) {
var publicKeyPacket = pubKey.getPublicKeyPacket([signatureList[i].issuerKeyId]);
if (publicKeyPacket) {
var verifiedSig = {};
verifiedSig.keyid = signatureList[i].issuerKeyId;
verifiedSig.status = signatureList[i].verify(publicKeyPacket, literalDataList[0]);
result.push(verifiedSig);
break;
}
}
});
return result;
}
/**
* Unwrap compressed message
* @return {message} message Content of compressed message
*/
this.unwrapCompressed = function() {
var compressed = this.packets.filterByTag(enums.packet.compressed);
if (compressed.length) {
return new message(compressed[0].packets);
} else {
return this;
}
}
/**
* Returns ASCII armored text of message
* @return {String} ASCII armor
*/
this.armor = function() {
return armor.encode(enums.armor.message, this.packets.write());
}
/**
* Decrypts a message and generates user interface message out of the found.
* MDC will be verified as well as message signatures

View File

@ -38,7 +38,7 @@ var message = require('./message.js');
function _openpgp() {
/**
* encrypts message text with keys
* Encrypts message text with keys
* @param {[key]} keys array of keys, used to encrypt the message
* @param {String} text message as native JavaScript string
* @return {String} encrypted ASCII armored message
@ -51,7 +51,7 @@ function _openpgp() {
}
/**
* signs message text and encrypts it
* Signs message text and encrypts it
* @param {[key]} publicKeys array of keys, used to encrypt the message
* @param {key} privateKey private key with decrypted secret key data for signing
* @param {String} text message as native JavaScript string
@ -59,14 +59,14 @@ function _openpgp() {
*/
function signAndEncryptMessage(publicKeys, privateKey, text) {
var msg = message.fromText(text);
msg = msg.sign(privateKey);
msg = msg.sign([privateKey]);
msg = msg.encrypt(publicKeys);
var armored = armor.encode(enums.armor.message, msg.packets.write());
return armored;
}
/**
* decrypts message
* Decrypts message
* @param {key} privateKey private key with decrypted secret key data
* @param {message} message the message object with the encrypted data
* @return {String|null} decrypted message as as native JavaScript string
@ -74,11 +74,27 @@ function _openpgp() {
*/
function decryptMessage(privateKey, message) {
message = message.decrypt(privateKey);
return message.getLiteral();
return message.getText();
}
function decryptAndVerifyMessage(privateKey, publicKeys, messagePacketlist) {
/**
* Decrypts message and verifies signatures
* @param {key} privateKey private key with decrypted secret key data
* @param {[key]} publicKeys public keys to verify signatures
* @param {message} message the message object with signed and encrypted data
* @return {{'text': String, signatures: [{'keyid': keyid, 'status': Boolean}]}}
* decrypted message as as native JavaScript string
* with verified signatures or null if no literal data found
*/
function decryptAndVerifyMessage(privateKey, publicKeys, message) {
var result = {};
message = message.decrypt(privateKey);
result.text = message.getText();
if (result.text) {
result.signatures = message.verify(publicKeys);
return result;
}
return null;
}
function verifyMessage(publicKeys, messagePacketlist) {
@ -286,6 +302,7 @@ function _openpgp() {
this.generateKeyPair = generateKeyPair;
this.write_signed_message = write_signed_message;
this.signAndEncryptMessage = signAndEncryptMessage;
this.decryptAndVerifyMessage = decryptAndVerifyMessage
this.encryptMessage = encryptMessage;
this.decryptMessage = decryptMessage;

View File

@ -56,7 +56,7 @@ module.exports = function packetlist() {
* writing to packetlist[i] directly will result in an error.
*/
this.push = function(packet) {
packet.packets = new packetlist();
packet.packets = packet.packets || new packetlist();
this[this.length] = packet;
this.length++;

View File

@ -122,26 +122,21 @@ module.exports = function packet_signature() {
this.publicKeyAlgorithm = bytes[i++].charCodeAt();
this.hashAlgorithm = bytes[i++].charCodeAt();
function subpackets(bytes, signed) {
// Two-octet scalar octet count for following hashed subpacket
// data.
function subpackets(bytes) {
// Two-octet scalar octet count for following subpacket data.
var subpacket_length = util.readNumber(
bytes.substr(0, 2));
var i = 2;
// Hashed subpacket data set (zero or more subpackets)
// subpacket data set (zero or more subpackets)
var subpacked_read = 0;
while (i < 2 + subpacket_length) {
var len = packet.readSimpleLength(bytes.substr(i));
i += len.offset;
// Since it is trivial to add data to the unhashed portion of
// the packet we simply ignore all unauthenticated data.
if (signed)
this.read_sub_packet(bytes.substr(i, len.len));
this.read_sub_packet(bytes.substr(i, len.len));
i += len.len;
}
@ -149,6 +144,7 @@ module.exports = function packet_signature() {
return i;
}
// hashed subpackets
i += subpackets.call(this, bytes.substr(i), true);
// A V4 signature hashes the packet body
@ -159,6 +155,7 @@ module.exports = function packet_signature() {
// subpacket body.
this.signatureData = bytes.substr(0, i);
// unhashed subpackets
i += subpackets.call(this, bytes.substr(i), false);
break;
@ -582,8 +579,8 @@ module.exports = function packet_signature() {
/**
* verifys the signature packet. Note: not signature types are implemented
* @param {String} data data which on the signature applies
* @param {openpgp_msg_privatekey} key the public key to verify the signature
* @param {String|Object} data data which on the signature applies
* @param {public_subkey|packet_public_key} key the public key to verify the signature
* @return {boolean} True if message is verified, else false.
*/
this.verify = function(key, data) {

View File

@ -137,6 +137,38 @@ unit.register("Signature testing", function() {
'=h/aX',
'-----END PGP PUBLIC KEY BLOCK-----'].join('\n');
var pub_key_arm3 =
['-----BEGIN PGP PUBLIC KEY BLOCK-----',
'Version: GnuPG v2.0.19 (GNU/Linux)',
'',
'mQENBFKV0FUBCACtZliApy01KBGbGNB36YGH4lpr+5KoqF1I8A5IT0YeNjyGisOk',
'WsDsUzOqaNvgzQ82I3MY/jQV5rLBhH/6LiRmCA16WkKcqBrHfNGIxJ+Q+ofVBHUb',
'aS9ClXYI88j747QgWzirnLuEA0GfilRZcewII1pDA/G7+m1HwV4qHsPataYLeboq',
'hPA3h1EVVQFMAcwlqjOuS8+weHQRfNVRGQdRMm6H7166PseDVRUHdkJpVaKFhptg',
'rDoNI0lO+UujdqeF1o5tVZ0j/s7RbyBvdLTXNuBbcpq93ceSWuJPZmi1XztQXKYe',
'y0f+ltgVtZDEc7TGV5WDX9erRECCcA3+s7J3ABEBAAG0G0pTIENyeXB0byA8ZGlm',
'ZmllQGhvbWUub3JnPokBPwQTAQIAKQUCUpXQVQIbAwUJCWYBgAcLCQgHAwIBBhUI',
'AgkKCwQWAgMBAh4BAheAAAoJENvyI+hwU030yRAIAKX/mGEgi/miqasbbQoyK/CS',
'a7sRxgZwOWQLdi2xxpE5V4W4HJIDNLJs5vGpRN4mmcNK2fmJAh74w0PskmVgJEhP',
'dFJ14UC3fFPq5nbqkBl7hU0tDP5jZxo9ruQZfDOWpHKxOCz5guYJ0CW97bz4fChZ',
'NFDyfU7VsJQwRIoViVcMCipP0fVZQkIhhwpzQpmVmN8E0a6jWezTZv1YpMdlzbEf',
'H79l3StaOh9/Un9CkIyqEWdYiKvIYms9nENyehN7r/OKYN3SW+qlt5GaL+ws+N1w',
'6kEZjPFwnsr+Y4A3oHcAwXq7nfOz71USojSmmo8pgdN8je16CP98vw3/k6TncLS5',
'AQ0EUpXQVQEIAMEjHMeqg7B04FliUFWr/8C6sJDb492MlGAWgghIbnuJfXAnUGdN',
'oAzn0S+n93Y/qHbW6YcjHD4/G+kK3MuxthAFqcVjdHZQXK0rkhXO/u1co7v1cdtk',
'OTEcyOpyLXolM/1S2UYImhrml7YulTHMnWVja7xu6QIRso+7HBFT/u9D47L/xXrX',
'MzXFVZfBtVY+yoeTrOY3OX9cBMOAu0kuN9eT18Yv2yi6XMzP3iONVHtl6HfFrAA7',
'kAtx4ne0jgAPWZ+a8hMy59on2ZFs/AvSpJtSc1kw/vMTWkyVP1Ky20vAPHQ6Ej5q',
'1NGJ/JbcFgolvEeI/3uDueLjj4SdSIbLOXMAEQEAAYkBJQQYAQIADwUCUpXQVQIb',
'DAUJCWYBgAAKCRDb8iPocFNN9NLkB/wO4iRxia0zf4Kw2RLVZG8qcuo3Bw9UTXYY',
'lI0AutoLNnSURMLLCq6rcJ0BCXGj/2iZ0NBxZq3t5vbRh6uUv+hpiSxK1nF7AheN',
'4aAAzhbWx0UDTF04ebG/neE4uDklRIJLhif6+Bwu+EUeTlGbDj7fqGSsNe8g92w7',
'1e41rF/9CMoOswrKgIjXAou3aexogWcHvKY2D+1q9exORe1rIa1+sUGl5PG2wsEs',
'znN6qtN5gMlGY1ofWDY+I02gO4qzaZ/FxRZfittCw7v5dmQYKot9qRi2Kx3Fvw+h',
'ivFBpC4TWgppFBnJJnAsFXZJQcejMW4nEmOViRQXY8N8PepQmgsu',
'=ummy',
'-----END PGP PUBLIC KEY BLOCK-----'].join('\n');
var tests = [function() {
var priv_key = openpgp.key.readArmored(priv_key_arm1).packets;
@ -229,6 +261,103 @@ unit.register("Signature testing", function() {
var pub_key = openpgp.key.readArmored(pub_key_arm2).packets;
sMsg[0].packets[2].verify(pub_key[3], sMsg[0].packets[1]);
return new unit.result("Verify V3 signature. Hash: MD5. PK: RSA. Signature Type: 0x01 (text document)", sMsg[0].packets[2].verified);
}, function() {
var msg_armor =
['-----BEGIN PGP MESSAGE-----',
'Version: GnuPG v2.0.19 (GNU/Linux)',
'',
'hIwD4IT3RGwgLJcBBADEBdm+GEW7IV1K/Bykg0nB0WYO08ai7/8/+Y/O9xu6RiU0',
'q7/jWuKms7kSjw9gxMCjf2dGnAuT4Cg505Kj5WfeBuHh618ovO8qo4h0qHyp1/y3',
'o1P0IXPAb+LGJOeO7DyM9Xp2AOBiIKOVWzFTg+MBZOc+XZEVx3FioHfm9SSDudLA',
'WAEkDakCG6MRFj/7SmOiV8mQKH+YPMKT69eDZW7hjINabrpM2pdRU7c9lC7CMUBx',
'Vj7wZsQBMASSC8f2rhpGA2iKvYMsmW3g9R1xkvj1MXWftSPUS4jeNTAgEwvvF6Af',
'cP+OYSXKlTbwfEr73ES2O3/IFE9sHRjPqWaxWuv4DDQ5YfIxE54C1aE8Aq5/QaIH',
'v38TUSia0yEMCc/tJd58DikkT07AF162tcx9Ro0ZjhudyuvUyXIfPfxA+XWR2pdz',
'ifxyV4zia9RvaCUY8vXGM+gQJ3NNXx2LkZA3kWUEyxFVL1Vl/XUQY0M6U+uccSk4',
'eMXm6eyEWDcj0lBRckqKoKo1w/uan11jPuHsnRz6jO9DsuKEz79UDgI=',
'=cFi7',
'-----END PGP MESSAGE-----'].join('\n');
var plaintext = 'short message\nnext line\n한국어/조선말';
var esMsg = openpgp.message.readArmored(msg_armor);
var pubKey = openpgp.key.readArmored(pub_key_arm2);
var privKey = openpgp.key.readArmored(priv_key_arm2);
var keyids = esMsg.getEncryptionKeyIds();
privKey.decryptKeyPacket(keyids, 'hello world');
var decrypted = openpgp.decryptAndVerifyMessage(privKey, [pubKey], esMsg);
var verified = decrypted.text == plaintext && decrypted.signatures[0].status;
return new unit.result("Verify signature of signed and encrypted message from GPG2 with openpgp.decryptAndVerifyMessage", verified);
}, function() {
var msg_armor =
['-----BEGIN PGP MESSAGE-----',
'Version: Encryption Desktop 10.3.0 (Build 9307)',
'Charset: utf-8',
'',
'qANQR1DBjAPghPdEbCAslwED/2S4oNvCjO5TdLUMMUuVOQc8fi6c5XIBu7Y09fEX',
'Jm/UrkDHVgmPojLGBDF0CYENNZOVrNfpahY7A3r4HPzGucBzCO1uxuUIKjhtNAAM',
'mjD939ernjooOZrM6xDuRaX8adG0LSxpNaVJGxXd/EdlmKDJbYDI6aJ5INrUxzAR',
'DAqw0sBSAXgRWgiH6IIiAo5y5WFEDEN9sGStaEQT2wd32kX73M4iZuMt/GM2agiB',
'sWb7yLcNHiJ/3OnTfDg9+T543kFq9FlwFbwqygO/wm9e/kgMBq0ZsFOfV+GRtXep',
'3qNbJsmzGvdqiUHb/+hkdE191jaSVcO/zaMW4N0Vc1IwIEhZ8I9+9bKwusdVhHT5',
'MySnhIogv+0Ilag/aY+UiCt+Zcie69T7Eix48fC/VVW5w3INf1T2CMmDm5ZLZFRN',
'oyqzb9Vsgu1gS7SCb6qTbnbV9PlSyU4wJB6siX8hz/U0urokT5se3uYRjiV0KbkA',
'zl1/r/wCrmwX4Gl9VN9+33cQgYZAlJLsRw8N82GhbVweZS8qwv24GQ==',
'=nx90',
'-----END PGP MESSAGE-----'].join('\n');
var plaintext = 'short message\nnext line\n한국어/조선말\n\n';
var esMsg = openpgp.message.readArmored(msg_armor);
var pubKey = openpgp.key.readArmored(pub_key_arm2);
var privKey = openpgp.key.readArmored(priv_key_arm2);
var keyids = esMsg.getEncryptionKeyIds();
privKey.decryptKeyPacket(keyids, 'hello world');
var decrypted = openpgp.decryptAndVerifyMessage(privKey, [pubKey], esMsg);
var verified = decrypted.text == plaintext && decrypted.signatures[0].status;
return new unit.result("Verify signature of signed and encrypted message from PGP 10.3.0 with openpgp.decryptAndVerifyMessage", verified);
}, function() {
var msg_armor =
['-----BEGIN PGP MESSAGE-----',
'Version: GnuPG v2.0.19 (GNU/Linux)',
'',
'owGbwMvMwMF4+5Pyi4Jg3y8ME8DcBy3fXXIUdKYzrjFNYilJrSgJmsXDXJyRX1Si',
'kJtaXJyYnsqVBxRVyMnMS+V6O3XOq61r30zbov9m4YY3LQteL5/QMYeFgZGDgY2V',
'CaSRgYtTAGZiYxYLwySbQk07ptZel6gmjrKyBWsyWdkOG3oscLBdIpXXfDdb6fNv',
'8ULN5L1ed+xNo79P2dBotWud6vn7e9dtLJ7o12PunnvEz8gyyvP4/As/los0xsnZ',
'H+8ublrhvGtLxJUZuUKZO6QdHq2Nepuw8OrfiMXPBDQXXpV2q11Ze+rD3lndgv/C',
'bJQNOhll0J0H839jFvt/16m20h/ZmDoWqJywapnypjdIjcXr+7rJFess40yenV7Q',
'2LSu/EX6Aq29x+dv+GPUMfuhTNE3viWWUR4PD6T7XfmdViUwmSf8fkRNUn/t3a2n',
'cq46Xr36seCor/OLp0atSZwHrjx2SU5zPLheZn+zw/0d1/YZnD7AEeP9s/Cuycyv',
'CZ5HZNKufvB8fsh+dfdSXW0GfqkPfxk36Vw8ufpjaoZDyt2nxxg/6D4KS3UvZzv3',
'axdLZ9yd0OJNZv4P501If24W4vTGz6nI7Ser8Yd2PiOvE5MWMT0wLZQ+zPX1sv0/',
's8PvkyWmVM0O0fB/ZSHovHNNPffDg/rWhzOmXQ9/7vTn477F+aWm5sYzJ75/BQA=',
'=+L0S',
'-----END PGP MESSAGE-----'].join('\n');
var plaintext = 'short message\nnext line\n한국어/조선말';
var sMsg = openpgp.message.readArmored(msg_armor);
var pubKey2 = openpgp.key.readArmored(pub_key_arm2);
var pubKey3 = openpgp.key.readArmored(pub_key_arm3);
var keyids = sMsg.getSigningKeyIds();
var verified = pubKey2.getPublicKeyPacket(keyids) && pubKey3.getPublicKeyPacket(keyids);
verified = verified && sMsg.getText() == plaintext;
var verifiedSig = sMsg.verify([pubKey2, pubKey3]);
verified = verified && verifiedSig[0].status && verifiedSig[1].status;
return new unit.result("Verify signed message with two one pass signatures", verified);
}];
var results = [];

View File

@ -1,6 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta charset="utf-8">
<!-- unit test -->
<script type="text/javascript" src="test-bundle.js"></script>

File diff suppressed because one or more lines are too long