Lukas Nowy fd947bd852 https
express server läuft jetzt mit https
2018-12-16 19:08:08 +01:00

753 lines
20 KiB
JavaScript

// Copyright 2012 The Obvious Corporation.
/*
* "ursa": RSA crypto, with an emphasis on Buffer objects
*/
/*
* Modules used
*/
"use strict";
// Note: This also forces OpenSSL to be initialized, which is important!
var crypto = require("crypto");
var assert = require("assert");
var ursaNative = require("bindings")("ursaNative");
var RsaWrap = ursaNative.RsaWrap;
var textToNid = ursaNative.textToNid;
/*
* Variable definitions
*/
/** encoding constant */
var BASE64 = "base64";
/** encoding constant */
var BINARY = "binary";
/** encoding constant */
var HEX = "hex";
/** type name */
var STRING = "string";
/** encoding constant */
var UTF8 = "utf8";
/** encoding constant */
var UTF16 = "utf16le";
/** hash algorithm constant */
var MD5 = "md5";
/** regex that matches PEM files, capturing the file type */
var PEM_REGEX =
/^(-----BEGIN (.*) KEY-----\r?\n[:\s,-\/+=a-zA-Z0-9\r\n]*\r?\n-----END \2 KEY-----\r?\n)/m;
/** "unsealer" key object to authenticate objects */
var theUnsealer = [ "ursa unsealer" ];
/*
* Helper functions
*/
/**
* Return true iff x is either a string or a Buffer.
*/
function isStringOrBuffer(x) {
return (typeof x === STRING) || Buffer.isBuffer(x);
}
/**
* Extract and identify the PEM file type represented in the given
* buffer. Returns the extracted type string or undefined if the
* buffer doesn't seem to be any sort of PEM format file.
*/
function identifyPemType(buf) {
var str = encodeBuffer(buf, UTF8);
var match = PEM_REGEX.exec(str);
if (!match) {
return undefined;
}
return match[2];
}
/**
* Return whether the given buffer or string appears (trivially) to be a
* valid public key file in PEM format.
*/
function isPublicKeyPem(buf) {
var kind = identifyPemType(buf);
return (kind == "PUBLIC");
}
/**
* Return whether the given buffer or string appears (trivially) to be a
* valid private key file in PEM format.
*/
function isPrivateKeyPem(buf) {
var kind = identifyPemType(buf);
return (kind == "RSA PRIVATE");
}
/**
* Return a buffer containing the encoding of the given bigint for use
* as part of an SSH-style public key file. The input value must be a
* buffer representing an unsigned bigint in big-endian order.
*/
function toSshBigint(value) {
// The output is signed, so we need to add an extra 00 byte at the
// head if the high-order bit is set.
var prefix00 = ((value[0] & 0x80) !== 0);
var length = value.length + (prefix00 ? 1 : 0);
var result = new Buffer(length + 4);
var offset = 0;
result.writeUInt32BE(length, offset);
offset += 4;
if (prefix00) {
result[offset] = 0;
offset++;
}
value.copy(result, offset);
return result;
}
/**
* Create and return a buffer containing an SSH-style public key file for
* the given RsaWrap object.
*
* For the record, an SSH-style public key file consists of three
* concatenated values, each one length-prefixed:
*
* literal string "ssh-rsa"
* exponent
* modulus
*
* The literal string header is length-prefixed. The two numbers are
* represented as signed big-int values in big-endian order, also
* length-prefixed.
*/
function createSshPublicKey(rsa) {
var e = toSshBigint(rsa.getExponent());
var m = toSshBigint(rsa.getModulus());
var header = toSshBigint(new Buffer("ssh-rsa", UTF8));
var result = new Buffer(header.length + m.length + e.length);
var offset = 0;
header.copy(result, offset);
offset += header.length;
e.copy(result, offset);
offset += e.length;
m.copy(result, offset);
return result;
}
/**
* Validate the given encoding name. Throws an exception if invalid.
*/
function validateEncoding(encoding) {
switch (encoding) {
case BASE64:
case BINARY:
case HEX:
case UTF16:
case UTF8: {
// These are all valid.
break;
}
default: {
throw new Error("Invalid encoding: " + encoding);
}
}
}
/**
* Convert a buffer into an appropriately-encoded string, or return it
* unmodified if the encoding is undefined.
*/
function encodeBuffer(buf, encoding) {
if (encoding === undefined) {
return buf;
}
validateEncoding(encoding);
return buf.toString(encoding);
}
/**
* Return a buffer or undefined argument as-is, or convert a given
* string into a buffer by using the indicated encoding. An undefined
* encoding is interpreted to mean UTF8.
*/
function decodeString(str, encoding) {
if ((str === undefined) || Buffer.isBuffer(str)) {
return str;
}
if (encoding === undefined) {
encoding = UTF8;
}
validateEncoding(encoding);
return new Buffer(str, encoding);
}
/**
* OpenSSH Public key to RSA
* @param {String|Object} key OpenSSH Public Key
* @param <String> key encoding, default 'base64'
* @returns {PublicKey}
*/
function openSshPublicKey(key, encoding) {
if (!Buffer.isBuffer(key)) {
key = key.substr(0, 3) === 'ssh' ? key.split(' ')[1] : key;
key = new Buffer(key, encoding || 'base64');
}
function parsePublicKey(key) {
var parts = [],
partsLength = 3;
while(key.length) {
var dLen = key.readInt32BE(0);
var data = key.slice(4, dLen+4);
key = key.slice(4+dLen);
parts.push(data);
if (!(--partsLength)) break;
}
return {
modulus : parts[2],
exponent: parts[1],
type : parts[0]
};
}
var pubKey = parsePublicKey(key);
var rsa = new RsaWrap();
if (pubKey.type != 'ssh-rsa') {
throw new TypeError('Only "ssh-rsa" format supported');
}
rsa.openPublicSshKey(pubKey.modulus, pubKey.exponent);
return PublicKey(rsa);
}
/**
* Public Key object. This is the externally-visible object that one gets
* when constructing an instance from a public key. The constructor takes
* a native RsaWrap object.
*/
function PublicKey(rsa) {
var self;
function getExponent(encoding) {
return encodeBuffer(rsa.getExponent(), encoding);
}
function getModulus(encoding) {
return encodeBuffer(rsa.getModulus(), encoding);
}
function toPublicPem(encoding) {
return encodeBuffer(rsa.getPublicKeyPem(), encoding);
}
function toPublicSsh(encoding) {
return encodeBuffer(createSshPublicKey(rsa), encoding);
}
function toPublicSshFingerprint(encoding) {
return sshFingerprint(createSshPublicKey(rsa), undefined, encoding);
}
function encrypt(buf, bufEncoding, outEncoding, padding) {
buf = decodeString(buf, bufEncoding);
padding = (padding === undefined) ?
ursaNative.RSA_PKCS1_OAEP_PADDING : padding;
return encodeBuffer(rsa.publicEncrypt(buf, padding), outEncoding);
}
function publicDecrypt(buf, bufEncoding, outEncoding, padding) {
buf = decodeString(buf, bufEncoding);
padding = (padding === undefined) ?
ursaNative.RSA_PKCS1_PADDING : padding;
return encodeBuffer(rsa.publicDecrypt(buf, padding), outEncoding);
}
function verify(algorithm, hash, sig, encoding) {
algorithm = textToNid(algorithm);
hash = decodeString(hash, encoding);
sig = decodeString(sig, encoding);
return rsa.verify(algorithm, hash, sig);
}
function hashAndVerify(algorithm, buf, sig, encoding,
use_pss_padding, salt_len) {
if (use_pss_padding) {
sig = publicDecrypt(sig, encoding, undefined,
ursaNative.RSA_NO_PADDING);
var hash = crypto.createHash(algorithm);
hash.update(decodeString(buf, encoding));
buf = new Buffer(hash.digest(BINARY), BINARY);
return rsa.verifyPSSPadding(textToNid(algorithm), buf, sig,
(salt_len === undefined) ? ursaNative.RSA_PKCS1_SALT_LEN_HLEN : salt_len);
} else {
var verifier = createVerifier(algorithm);
verifier.update(buf, encoding);
return verifier.verify(self, sig, encoding);
}
}
function unseal(unsealer) {
return (unsealer === theUnsealer) ? self : undefined;
}
self = {
encrypt: encrypt,
getExponent: getExponent,
getModulus: getModulus,
hashAndVerify: hashAndVerify,
publicDecrypt: publicDecrypt,
toPublicPem: toPublicPem,
toPublicSsh: toPublicSsh,
toPublicSshFingerprint: toPublicSshFingerprint,
verify: verify,
unseal: unseal
};
return self;
}
/**
* Private Key object. This is the externally-visible object that one
* gets when constructing an instance from a private key (aka a
* keypair). The constructor takes a native RsaWrap object.
*/
function PrivateKey(rsa) {
var self;
function getPrivateExponent(encoding) {
return encodeBuffer(rsa.getPrivateExponent(), encoding);
}
function toPrivatePem(encoding) {
return encodeBuffer(rsa.getPrivateKeyPem(), encoding);
}
function toEncryptedPrivatePem(passPhrase, cipher, encoding) {
if(!passPhrase) return toPrivatePem(encoding);
return encodeBuffer(rsa.getPrivateKeyPem(passPhrase, cipher));
}
function decrypt(buf, bufEncoding, outEncoding, padding) {
buf = decodeString(buf, bufEncoding);
padding = (padding === undefined) ? ursaNative.RSA_PKCS1_OAEP_PADDING : padding;
return encodeBuffer(rsa.privateDecrypt(buf, padding), outEncoding);
}
function privateEncrypt(buf, bufEncoding, outEncoding, padding) {
buf = decodeString(buf, bufEncoding);
padding = (padding === undefined) ? ursaNative.RSA_PKCS1_PADDING : padding;
return encodeBuffer(rsa.privateEncrypt(buf, padding), outEncoding);
}
function sign(algorithm, hash, hashEncoding, outEncoding) {
algorithm = textToNid(algorithm);
hash = decodeString(hash, hashEncoding);
return encodeBuffer(rsa.sign(algorithm, hash), outEncoding);
}
function hashAndSign(algorithm, buf, bufEncoding, outEncoding,
use_pss_padding, salt_len) {
if (use_pss_padding) {
var hash = crypto.createHash(algorithm);
hash.update(decodeString(buf, bufEncoding));
buf = new Buffer(hash.digest(BINARY), BINARY);
buf = rsa.addPSSPadding(textToNid(algorithm), buf,
(salt_len === undefined) ? ursaNative.RSA_PKCS1_SALT_LEN_HLEN : salt_len);
return privateEncrypt(buf, undefined, outEncoding,
ursaNative.RSA_NO_PADDING);
} else {
var signer = createSigner(algorithm);
signer.update(buf, bufEncoding);
return signer.sign(self, outEncoding);
}
}
self = PublicKey(rsa);
self.decrypt = decrypt;
self.getPrivateExponent = getPrivateExponent;
self.hashAndSign = hashAndSign;
self.privateEncrypt = privateEncrypt;
self.sign = sign;
self.toPrivatePem = toPrivatePem;
self.toEncryptedPrivatePem = toEncryptedPrivatePem;
return self;
}
/*
* Exported bindings
*/
/**
* Create a new public key object, from the given PEM-encoded file.
*/
function createPublicKey(pem, encoding) {
var rsa = new RsaWrap();
pem = decodeString(pem, encoding);
try {
rsa.setPublicKeyPem(pem);
} catch (ex) {
if (!isPublicKeyPem(pem)) {
throw new Error("Not a public key.");
}
throw ex;
}
return PublicKey(rsa);
}
/**
* Create a new private key object, from the given PEM-encoded file,
* optionally decrypting the file with a password.
*/
function createPrivateKey(pem, password, encoding) {
var rsa = new RsaWrap();
pem = decodeString(pem, encoding);
password = decodeString(password, encoding);
try {
// Note: The native code is sensitive to the actual number of
// arguments. It's *not* okay to pass undefined as a password.
if (password) {
rsa.setPrivateKeyPem(pem, password);
} else {
rsa.setPrivateKeyPem(pem);
}
} catch (ex) {
if (!isPrivateKeyPem(pem)) {
throw new Error("Not a private key.");
}
throw ex;
}
return PrivateKey(rsa);
}
/**
* Create public key from components
*/
function createPublicKeyFromComponents(modulus, exponent) {
var rsa = new RsaWrap();
rsa.createPublicKeyFromComponents(modulus, exponent);
return PublicKey(rsa);
}
/**
* Create private key from components
*/
function createPrivateKeyFromComponents(modulus, exponent, p, q, dp, dq, inverseQ, d) {
var rsa = new RsaWrap();
rsa.createPrivateKeyFromComponents(modulus, exponent, p, q, dp, dq, inverseQ, d);
return PrivateKey(rsa);
}
/**
* Generate a new private key object (aka a keypair).
*/
function generatePrivateKey(modulusBits, exponent) {
if (modulusBits === undefined) {
modulusBits = 2048;
}
if (exponent === undefined) {
exponent = 65537;
}
var rsa = new RsaWrap();
rsa.generatePrivateKey(modulusBits, exponent);
return PrivateKey(rsa);
}
/**
* Create a key object from a PEM format file, either a private or
* public key depending on what kind of file is passed in. If given
* a private key file, it must not be encrypted.
*/
function createKey(pem, encoding) {
pem = decodeString(pem, encoding);
if (isPublicKeyPem(pem)) {
return createPublicKey(pem);
} else if (isPrivateKeyPem(pem)) {
return createPrivateKey(pem);
} else {
throw new Error("Not a key.");
}
}
/**
* Return the SSH-style public key fingerprint of the given SSH-format
* public key.
*/
function sshFingerprint(sshKey, sshEncoding, outEncoding) {
var hash = crypto.createHash(MD5);
hash.update(decodeString(sshKey, sshEncoding));
var result = new Buffer(hash.digest(BINARY), BINARY);
return encodeBuffer(result, outEncoding);
}
/**
* Return whether the given object is a key object (either public or
* private), as constructed by this module.
*/
function isKey(obj) {
var obj2;
try {
var unseal = obj.unseal;
if (typeof unseal !== "function") {
return false;
}
obj2 = unseal(theUnsealer);
} catch (ex) {
// Ignore; can't assume that other objects obey any particular
// unsealing protocol.
// TODO: Log?
return false;
}
return obj2 !== undefined;
}
/**
* Return whether the given object is a private key object, as
* constructed by this module.
*/
function isPrivateKey(obj) {
return isKey(obj) && (obj.decrypt !== undefined);
}
/**
* Return whether the given object is a public key object (per se), as
* constructed by this module.
*/
function isPublicKey(obj) {
return isKey(obj) && !isPrivateKey(obj);
}
/**
* Assert wrapper for isKey().
*/
function assertKey(obj) {
assert(isKey(obj));
}
/**
* Assert wrapper for isPrivateKey().
*/
function assertPrivateKey(obj) {
assert(isPrivateKey(obj));
}
/**
* Assert wrapper for isPublicKey().
*/
function assertPublicKey(obj) {
assert(isPublicKey(obj));
}
/**
* Coerce the given key value into an private key object, returning
* it. If given a private key object, this just returns it as-is. If
* given a string or Buffer, it tries to parse it as PEM. Anything
* else is an error.
*/
function coercePrivateKey(orig) {
if (isPrivateKey(orig)) {
return orig;
} else if (isStringOrBuffer(orig)) {
return createPrivateKey(orig);
}
throw new Error("Not a private key: " + orig);
}
/**
* Coerce the given key value into a public key object, returning
* it. If given a private key object, this just returns it as-is. If
* given a string or Buffer, it tries to parse it as PEM. Anything
* else is an error.
*/
function coercePublicKey(orig) {
if (isPublicKey(orig)) {
return orig;
} else if (isStringOrBuffer(orig)) {
return createPublicKey(orig);
}
throw new Error("Not a public key: " + orig);
}
/**
* Coerce the given key value into a key object (either public or
* private), returning it. If given a private key object, this just
* returns it as-is. If given a string or Buffer, it tries to parse it
* as PEM. Anything else is an error.
*/
function coerceKey(orig) {
if (isKey(orig)) {
return orig;
} else if (isStringOrBuffer(orig)) {
return createKey(orig);
}
throw new Error("Not a key: " + orig);
}
/**
* Check whether the two objects are both keys of some sort and
* have the same public part.
*/
function matchingPublicKeys(key1, key2) {
if (!(isKey(key1) && isKey(key2))) {
return false;
}
// This isn't the most efficient implementation, but it will suffice:
// We convert both to ssh form, which has very little leeway for
// variation, and compare bytes.
var ssh1 = key1.toPublicSsh(UTF8);
var ssh2 = key2.toPublicSsh(UTF8);
return ssh1 === ssh2;
}
/**
* Check whether the two objects are both keys of some sort, are
* both public or both private, and have the same contents.
*/
function equalKeys(key1, key2) {
// See above for rationale. In this case, there's no ssh form for
// private keys, so we just use PEM for that.
if (isPrivateKey(key1) && isPrivateKey(key2)) {
var pem1 = key1.toPrivatePem(UTF8);
var pem2 = key2.toPrivatePem(UTF8);
return pem1 === pem2;
}
if (isPublicKey(key1) && isPublicKey(key2)) {
return matchingPublicKeys(key1, key2);
}
return false;
}
/**
* Create a signer object.
*/
function createSigner(algorithm) {
var hash = crypto.createHash(algorithm);
var self = {};
function update(buf, bufEncoding) {
buf = decodeString(buf, bufEncoding);
hash.update(buf);
return self;
}
function sign(privateKey, outEncoding) {
var hashBuf = new Buffer(hash.digest(BINARY), BINARY);
return privateKey.sign(algorithm, hashBuf, undefined, outEncoding);
}
self.sign = sign;
self.update = update;
return self;
}
/**
* Create a verifier object.
*/
function createVerifier(algorithm) {
var hash = crypto.createHash(algorithm);
var self = {};
function update(buf, bufEncoding) {
buf = decodeString(buf, bufEncoding);
hash.update(buf);
return self;
}
function verify(publicKey, sig, sigEncoding) {
var hashBuf = new Buffer(hash.digest(BINARY), BINARY);
sig = decodeString(sig, sigEncoding);
return publicKey.verify(algorithm, hashBuf, sig);
}
self.update = update;
self.verify = verify;
return self;
}
/*
* Initialization
*/
module.exports = {
assertKey: assertKey,
assertPrivateKey: assertPrivateKey,
assertPublicKey: assertPublicKey,
coerceKey: coerceKey,
coercePrivateKey: coercePrivateKey,
coercePublicKey: coercePublicKey,
createKey: createKey,
createPrivateKey: createPrivateKey,
createPrivateKeyFromComponents: createPrivateKeyFromComponents,
openSshPublicKey: openSshPublicKey,
createPublicKey: createPublicKey,
createPublicKeyFromComponents: createPublicKeyFromComponents,
createSigner: createSigner,
createVerifier: createVerifier,
equalKeys: equalKeys,
generatePrivateKey: generatePrivateKey,
isKey: isKey,
isPrivateKey: isPrivateKey,
isPublicKey: isPublicKey,
matchingPublicKeys: matchingPublicKeys,
sshFingerprint: sshFingerprint,
RSA_NO_PADDING: ursaNative.RSA_NO_PADDING,
RSA_PKCS1_PADDING: ursaNative.RSA_PKCS1_PADDING,
RSA_PKCS1_OAEP_PADDING: ursaNative.RSA_PKCS1_OAEP_PADDING,
RSA_PKCS1_SALT_LEN_HLEN: ursaNative.RSA_PKCS1_SALT_LEN_HLEN,
RSA_PKCS1_SALT_LEN_MAX: ursaNative.RSA_PKCS1_SALT_LEN_MAX,
RSA_PKCS1_SALT_LEN_RECOVER: ursaNative.RSA_PKCS1_SALT_LEN_RECOVER
};