From 42bda05af8d628db90f3ee64fa2dd89973cb1e03 Mon Sep 17 00:00:00 2001 From: seishun Date: Mon, 5 May 2014 17:35:28 +0300 Subject: [PATCH] crypto: add RSA encryption Reviewed-By: Fedor Indutny --- doc/api/crypto.markdown | 16 ++++ lib/crypto.js | 10 +++ src/node_crypto.cc | 146 +++++++++++++++++++++++++++++++++++++ src/node_crypto.h | 29 ++++++++ test/simple/test-crypto.js | 34 +++++++++ 5 files changed, 235 insertions(+) diff --git a/doc/api/crypto.markdown b/doc/api/crypto.markdown index 0c324c543a..d1ed683006 100644 --- a/doc/api/crypto.markdown +++ b/doc/api/crypto.markdown @@ -593,6 +593,22 @@ Exports the encoded public key from the supplied SPKAC. Exports the encoded challenge associated with the SPKAC. +## crypto.publicEncrypt(public_key, buffer) + +Encrypts `buffer` with `public_key`. Only RSA is currently supported. + +## crypto.privateDecrypt(private_key, buffer) + +Decrypts `buffer` with `private_key`. + +`private_key` can be an object or a string. If `private_key` is a string, it is +treated as the key with no passphrase. + +`private_key`: + +* `key` : A string holding the PEM encoded private key +* `passphrase` : A string of passphrase for the private key + ## crypto.DEFAULT_ENCODING The default encoding to use for functions that can take either strings diff --git a/lib/crypto.js b/lib/crypto.js index c4256609bf..16a3e38e25 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -355,6 +355,16 @@ Verify.prototype.verify = function(object, signature, sigEncoding) { return this._handle.verify(toBuf(object), toBuf(signature, sigEncoding)); }; +exports.publicEncrypt = function(object, buffer) { + return binding.publicEncrypt(toBuf(object), buffer); +}; + +exports.privateDecrypt = function(options, buffer) { + var key = options.key || options; + var passphrase = options.passphrase || null; + return binding.privateDecrypt(toBuf(key), buffer, passphrase); +}; + exports.createDiffieHellman = exports.DiffieHellman = DiffieHellman; diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 4abc513dce..03c2d25b25 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -65,6 +65,9 @@ static const char PUBLIC_KEY_PFX[] = "-----BEGIN PUBLIC KEY-----"; static const int PUBLIC_KEY_PFX_LEN = sizeof(PUBLIC_KEY_PFX) - 1; static const char PUBRSA_KEY_PFX[] = "-----BEGIN RSA PUBLIC KEY-----"; static const int PUBRSA_KEY_PFX_LEN = sizeof(PUBRSA_KEY_PFX) - 1; +static const char CERTIFICATE_PFX[] = "-----BEGIN CERTIFICATE-----"; +static const int CERTIFICATE_PFX_LEN = sizeof(CERTIFICATE_PFX) - 1; + static const int X509_NAME_FLAGS = ASN1_STRFLGS_ESC_CTRL | ASN1_STRFLGS_ESC_MSB | XN_FLAG_SEP_MULTILINE @@ -3544,6 +3547,139 @@ void Verify::VerifyFinal(const FunctionCallbackInfo& args) { } +template +bool PublicKeyCipher::Cipher(const char* key_pem, + int key_pem_len, + const char* passphrase, + const unsigned char* data, + int len, + unsigned char** out, + size_t* out_len) { + EVP_PKEY* pkey = NULL; + EVP_PKEY_CTX* ctx = NULL; + BIO* bp = NULL; + X509* x509 = NULL; + bool fatal = true; + + bp = BIO_new(BIO_s_mem()); + if (bp == NULL) + goto exit; + + if (!BIO_write(bp, key_pem, key_pem_len)) + goto exit; + + // Check if this is a PKCS#8 or RSA public key before trying as X.509 and + // private key. + if (operation == kEncrypt && + strncmp(key_pem, PUBLIC_KEY_PFX, PUBLIC_KEY_PFX_LEN) == 0) { + pkey = PEM_read_bio_PUBKEY(bp, NULL, NULL, NULL); + if (pkey == NULL) + goto exit; + } else if (operation == kEncrypt && + strncmp(key_pem, PUBRSA_KEY_PFX, PUBRSA_KEY_PFX_LEN) == 0) { + RSA* rsa = PEM_read_bio_RSAPublicKey(bp, NULL, NULL, NULL); + if (rsa) { + pkey = EVP_PKEY_new(); + if (pkey) + EVP_PKEY_set1_RSA(pkey, rsa); + RSA_free(rsa); + } + if (pkey == NULL) + goto exit; + } else if (operation == kEncrypt && + strncmp(key_pem, CERTIFICATE_PFX, CERTIFICATE_PFX_LEN) == 0) { + x509 = PEM_read_bio_X509(bp, NULL, CryptoPemCallback, NULL); + if (x509 == NULL) + goto exit; + + pkey = X509_get_pubkey(x509); + if (pkey == NULL) + goto exit; + } else { + pkey = PEM_read_bio_PrivateKey(bp, + NULL, + CryptoPemCallback, + const_cast(passphrase)); + if (pkey == NULL) + goto exit; + } + + ctx = EVP_PKEY_CTX_new(pkey, NULL); + if (!ctx) + goto exit; + if (EVP_PKEY_cipher_init(ctx) <= 0) + goto exit; + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) + goto exit; + if (EVP_PKEY_cipher(ctx, NULL, out_len, data, len) <= 0) + goto exit; + + *out = new unsigned char[*out_len]; + + if (EVP_PKEY_cipher(ctx, *out, out_len, data, len) <= 0) + goto exit; + + fatal = false; + + exit: + if (pkey != NULL) + EVP_PKEY_free(pkey); + if (bp != NULL) + BIO_free_all(bp); + if (ctx != NULL) + EVP_PKEY_CTX_free(ctx); + + return !fatal; +} + + +template +void PublicKeyCipher::Cipher(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args.GetIsolate()); + HandleScope scope(env->isolate()); + + ASSERT_IS_BUFFER(args[0]); + char* kbuf = Buffer::Data(args[0]); + ssize_t klen = Buffer::Length(args[0]); + + ASSERT_IS_BUFFER(args[1]); + char* buf = Buffer::Data(args[1]); + ssize_t len = Buffer::Length(args[1]); + + String::Utf8Value passphrase(args[2]); + + unsigned char* out_value = NULL; + size_t out_len = -1; + + bool r = Cipher( + kbuf, + klen, + args.Length() >= 3 && !args[2]->IsNull() ? *passphrase : NULL, + reinterpret_cast(buf), + len, + &out_value, + &out_len); + + if (out_len <= 0 || !r) { + delete[] out_value; + out_value = NULL; + out_len = 0; + if (!r) { + return ThrowCryptoError(env, + ERR_get_error()); + } + } + + args.GetReturnValue().Set( + Buffer::New(env, reinterpret_cast(out_value), out_len)); + delete[] out_value; +} + + void DiffieHellman::Initialize(Environment* env, Handle target) { Local t = FunctionTemplate::New(env->isolate(), New); @@ -4730,6 +4866,16 @@ void InitCrypto(Handle target, NODE_SET_METHOD(target, "getSSLCiphers", GetSSLCiphers); NODE_SET_METHOD(target, "getCiphers", GetCiphers); NODE_SET_METHOD(target, "getHashes", GetHashes); + NODE_SET_METHOD(target, + "publicEncrypt", + PublicKeyCipher::Cipher); + NODE_SET_METHOD(target, + "privateDecrypt", + PublicKeyCipher::Cipher); } } // namespace crypto diff --git a/src/node_crypto.h b/src/node_crypto.h index 530f8e8848..9531df0cad 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -559,6 +559,35 @@ class Verify : public SignBase { } }; +class PublicKeyCipher { + public: + typedef int (*EVP_PKEY_cipher_init_t)(EVP_PKEY_CTX *ctx); + typedef int (*EVP_PKEY_cipher_t)(EVP_PKEY_CTX *ctx, + unsigned char *out, size_t *outlen, + const unsigned char *in, size_t inlen); + + enum Operation { + kEncrypt, + kDecrypt + }; + + template + static bool Cipher(const char* key_pem, + int key_pem_len, + const char* passphrase, + const unsigned char* data, + int len, + unsigned char** out, + size_t* out_len); + + template + static void Cipher(const v8::FunctionCallbackInfo& args); +}; + class DiffieHellman : public BaseObject { public: ~DiffieHellman() { diff --git a/test/simple/test-crypto.js b/test/simple/test-crypto.js index 4965b627f6..fd9896663a 100644 --- a/test/simple/test-crypto.js +++ b/test/simple/test-crypto.js @@ -823,6 +823,40 @@ var p = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' + var bad_dh = crypto.createDiffieHellman(p, 'hex'); assert.equal(bad_dh.verifyError, constants.DH_NOT_SUITABLE_GENERATOR); +// Test RSA encryption/decryption +(function() { + var input = 'I AM THE WALRUS'; + var bufferToEncrypt = new Buffer(input); + + var encryptedBuffer = crypto.publicEncrypt(rsaPubPem, bufferToEncrypt); + + var decryptedBuffer = crypto.privateDecrypt(rsaKeyPem, encryptedBuffer); + assert.equal(input, decryptedBuffer.toString()); + + var decryptedBufferWithPassword = crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'password' + }, encryptedBuffer); + assert.equal(input, decryptedBufferWithPassword.toString()); + + encryptedBuffer = crypto.publicEncrypt(certPem, bufferToEncrypt); + + decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer); + assert.equal(input, decryptedBuffer.toString()); + + encryptedBuffer = crypto.publicEncrypt(keyPem, bufferToEncrypt); + + decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer); + assert.equal(input, decryptedBuffer.toString()); + + assert.throws(function() { + crypto.privateDecrypt({ + key: rsaKeyPemEncrypted, + passphrase: 'wrong' + }, encryptedBuffer); + }); +})(); + // Test RSA key signing/verification var rsaSign = crypto.createSign('RSA-SHA1'); var rsaVerify = crypto.createVerify('RSA-SHA1');