diff --git a/doc/api/crypto.markdown b/doc/api/crypto.markdown index c0227c8fa8..dc8f9b5292 100644 --- a/doc/api/crypto.markdown +++ b/doc/api/crypto.markdown @@ -218,6 +218,13 @@ multiple of the cipher's block size or `final` will fail. Useful for non-standard padding, e.g. using `0x0` instead of PKCS padding. You must call this before `cipher.final`. +### cipher.getAuthTag() + +For authenticated encryption modes (currently supported: GCM), this +method returns a `Buffer` that represents the _authentication tag_ that +has been computed from the given data. Should be called after +encryption has been completed using the `final` method! + ## crypto.createDecipher(algorithm, password) @@ -268,6 +275,15 @@ removing it. Can only work if the input data's length is a multiple of the ciphers block size. You must call this before streaming data to `decipher.update`. +### decipher.setAuthTag(buffer) + +For authenticated encryption modes (currently supported: GCM), this +method must be used to pass in the received _authentication tag_. +If no tag is provided or if the ciphertext has been tampered with, +`final` will throw, thus indicating that the ciphertext should +be discarded due to failed authentication. + + ## crypto.createSign(algorithm) Creates and returns a signing object, with the given algorithm. On diff --git a/lib/crypto.js b/lib/crypto.js index db3ce1c431..add6a79d78 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -322,6 +322,15 @@ Cipheriv.prototype.update = Cipher.prototype.update; Cipheriv.prototype.final = Cipher.prototype.final; Cipheriv.prototype.setAutoPadding = Cipher.prototype.setAutoPadding; +Cipheriv.prototype.getAuthTag = function() { + return this._binding.getAuthTag(); +}; + + +Cipheriv.prototype.setAuthTag = function(tagbuf) { + this._binding.setAuthTag(tagbuf); +}; + exports.createDecipher = exports.Decipher = Decipher; @@ -367,6 +376,8 @@ Decipheriv.prototype.update = Cipher.prototype.update; Decipheriv.prototype.final = Cipher.prototype.final; Decipheriv.prototype.finaltol = Cipher.prototype.final; Decipheriv.prototype.setAutoPadding = Cipher.prototype.setAutoPadding; +Decipheriv.prototype.getAuthTag = Cipheriv.prototype.getAuthTag; +Decipheriv.prototype.setAuthTag = Cipheriv.prototype.setAuthTag; diff --git a/src/node_crypto.cc b/src/node_crypto.cc index beb6bd905a..1e7bf368af 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -2122,6 +2122,8 @@ void CipherBase::Initialize(Environment* env, Handle target) { NODE_SET_PROTOTYPE_METHOD(t, "update", Update); NODE_SET_PROTOTYPE_METHOD(t, "final", Final); NODE_SET_PROTOTYPE_METHOD(t, "setAutoPadding", SetAutoPadding); + NODE_SET_PROTOTYPE_METHOD(t, "getAuthTag", GetAuthTag); + NODE_SET_PROTOTYPE_METHOD(t, "setAuthTag", SetAuthTag); target->Set(FIXED_ONE_BYTE_STRING(node_isolate, "CipherBase"), t->GetFunction()); @@ -2250,12 +2252,85 @@ void CipherBase::InitIv(const FunctionCallbackInfo& args) { } +bool CipherBase::IsAuthenticatedMode() const { + // check if this cipher operates in an AEAD mode that we support. + if (!cipher_) + return false; + int mode = EVP_CIPHER_mode(cipher_); + return mode == EVP_CIPH_GCM_MODE; +} + + +bool CipherBase::GetAuthTag(char** out, unsigned int* out_len) const { + // only callable after Final and if encrypting. + if (initialised_ || kind_ != kCipher || !auth_tag_) + return false; + *out_len = auth_tag_len_; + *out = new char[auth_tag_len_]; + memcpy(*out, auth_tag_, auth_tag_len_); + return true; +} + + +void CipherBase::GetAuthTag(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args.GetIsolate()); + HandleScope handle_scope(args.GetIsolate()); + CipherBase* cipher = Unwrap(args.This()); + + char* out = NULL; + unsigned int out_len = 0; + + if (cipher->GetAuthTag(&out, &out_len)) { + Local buf = Buffer::Use(env, out, out_len); + args.GetReturnValue().Set(buf); + } else { + ThrowError("Attempting to get auth tag in unsupported state"); + } +} + + +bool CipherBase::SetAuthTag(const char* data, unsigned int len) { + if (!initialised_ || !IsAuthenticatedMode() || kind_ != kDecipher) + return false; + delete[] auth_tag_; + auth_tag_len_ = len; + auth_tag_ = new char[len]; + memcpy(auth_tag_, data, len); + return true; +} + + +void CipherBase::SetAuthTag(const FunctionCallbackInfo& args) { + HandleScope handle_scope(args.GetIsolate()); + + Local buf = args[0].As(); + if (!buf->IsObject() || !Buffer::HasInstance(buf)) + return ThrowTypeError("Argument must be a Buffer"); + + CipherBase* cipher = Unwrap(args.This()); + + if (!cipher->SetAuthTag(Buffer::Data(buf), Buffer::Length(buf))) + ThrowError("Attempting to set auth tag in unsupported state"); +} + + bool CipherBase::Update(const char* data, int len, unsigned char** out, int* out_len) { if (!initialised_) return 0; + + // on first update: + if (kind_ == kDecipher && IsAuthenticatedMode() && auth_tag_ != NULL) { + EVP_CIPHER_CTX_ctrl(&ctx_, + EVP_CTRL_GCM_SET_TAG, + auth_tag_len_, + reinterpret_cast(auth_tag_)); + delete[] auth_tag_; + auth_tag_ = NULL; + } + *out_len = len + EVP_CIPHER_CTX_block_size(&ctx_); *out = new unsigned char[*out_len]; return EVP_CipherUpdate(&ctx_, @@ -2328,6 +2403,21 @@ bool CipherBase::Final(unsigned char** out, int *out_len) { *out = new unsigned char[EVP_CIPHER_CTX_block_size(&ctx_)]; bool r = EVP_CipherFinal_ex(&ctx_, *out, out_len); + + if (r && kind_ == kCipher) { + delete[] auth_tag_; + auth_tag_ = NULL; + if (IsAuthenticatedMode()) { + auth_tag_len_ = EVP_GCM_TLS_TAG_LEN; // use default tag length + auth_tag_ = new char[auth_tag_len_]; + memset(auth_tag_, 0, auth_tag_len_); + EVP_CIPHER_CTX_ctrl(&ctx_, + EVP_CTRL_GCM_GET_TAG, + auth_tag_len_, + reinterpret_cast(auth_tag_)); + } + } + EVP_CIPHER_CTX_cleanup(&ctx_); initialised_ = false; diff --git a/src/node_crypto.h b/src/node_crypto.h index 05f5e36231..f11f2a00ce 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -318,6 +318,7 @@ class CipherBase : public BaseObject { ~CipherBase() { if (!initialised_) return; + delete[] auth_tag_; EVP_CIPHER_CTX_cleanup(&ctx_); } @@ -339,6 +340,10 @@ class CipherBase : public BaseObject { bool Final(unsigned char** out, int *out_len); bool SetAutoPadding(bool auto_padding); + bool IsAuthenticatedMode() const; + bool GetAuthTag(char** out, unsigned int* out_len) const; + bool SetAuthTag(const char* data, unsigned int len); + static void New(const v8::FunctionCallbackInfo& args); static void Init(const v8::FunctionCallbackInfo& args); static void InitIv(const v8::FunctionCallbackInfo& args); @@ -346,13 +351,18 @@ class CipherBase : public BaseObject { static void Final(const v8::FunctionCallbackInfo& args); static void SetAutoPadding(const v8::FunctionCallbackInfo& args); + static void GetAuthTag(const v8::FunctionCallbackInfo& args); + static void SetAuthTag(const v8::FunctionCallbackInfo& args); + CipherBase(Environment* env, v8::Local wrap, CipherKind kind) : BaseObject(env, wrap), cipher_(NULL), initialised_(false), - kind_(kind) { + kind_(kind), + auth_tag_(NULL), + auth_tag_len_(0) { MakeWeak(this); } @@ -361,6 +371,8 @@ class CipherBase : public BaseObject { const EVP_CIPHER* cipher_; /* coverity[member_decl] */ bool initialised_; CipherKind kind_; + char* auth_tag_; + unsigned int auth_tag_len_; }; class Hmac : public BaseObject { diff --git a/test/simple/test-crypto-authenticated.js b/test/simple/test-crypto-authenticated.js new file mode 100644 index 0000000000..ff9eedab03 --- /dev/null +++ b/test/simple/test-crypto-authenticated.js @@ -0,0 +1,130 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + + + + +var common = require('../common'); +var assert = require('assert'); + +try { + var crypto = require('crypto'); +} catch (e) { + console.log('Not compiled with OPENSSL support.'); + process.exit(); +} + +crypto.DEFAULT_ENCODING = 'buffer'; + +// +// Test authenticated encryption modes. +// +// !NEVER USE STATIC IVs IN REAL LIFE! +// + +var TEST_CASES = [ + { algo: 'aes-128-gcm', key: 'ipxp9a6i1Mb4USb4', iv: 'X6sIq117H0vR', + plain: 'Hello World!', ct: '4BE13896F64DFA2C2D0F2C76', + tag: '272B422F62EB545EAA15B5FF84092447', tampered: false }, + { algo: 'aes-128-gcm', key: 'ipxp9a6i1Mb4USb4', iv: 'X6sIq117H0vR', + plain: 'Hello World!', ct: '4BE13596F64DFA2C2D0FAC76', + tag: '272B422F62EB545EAA15B5FF84092447', tampered: true }, + { algo: 'aes-256-gcm', key: '3zTvzr3p67VC61jmV54rIYu1545x4TlY', + iv: '60iP0h6vJoEa', plain: 'Hello node.js world!', + ct: '58E62CFE7B1D274111A82267EBB93866E72B6C2A', + tag: '9BB44F663BADABACAE9720881FB1EC7A', tampered: false }, + { algo: 'aes-256-gcm', key: '3zTvzr3p67VC61jmV54rIYu1545x4TlY', + iv: '60iP0h6vJoEa', plain: 'Hello node.js world!', + ct: '58E62CFF7B1D274011A82267EBB93866E72B6C2B', + tag: '9BB44F663BADABACAE9720881FB1EC7A', tampered: true }, +]; + +var ciphers = crypto.getCiphers(); + +for (var i in TEST_CASES) { + var test = TEST_CASES[i]; + + if (ciphers.indexOf(test.algo) == -1) { + console.log('skipping unsupported ' + test.algo + ' test'); + continue; + } + + (function() { + var encrypt = crypto.createCipheriv(test.algo, test.key, test.iv); + var hex = encrypt.update(test.plain, 'ascii', 'hex'); + hex += encrypt.final('hex'); + var auth_tag = encrypt.getAuthTag(); + // only test basic encryption run if output is marked as tampered. + if (!test.tampered) { + assert.equal(hex.toUpperCase(), test.ct); + assert.equal(auth_tag.toString('hex').toUpperCase(), test.tag); + } + })(); + + (function() { + var decrypt = crypto.createDecipheriv(test.algo, test.key, test.iv); + decrypt.setAuthTag(new Buffer(test.tag, 'hex')); + var msg = decrypt.update(test.ct, 'hex', 'ascii'); + if (!test.tampered) { + msg += decrypt.final('ascii'); + assert.equal(msg, test.plain); + } else { + // assert that final throws if input data could not be verified! + assert.throws(function() { decrypt.final('ascii'); }); + } + })(); + + // after normal operation, test some incorrect ways of calling the API: + // it's most certainly enough to run these tests with one algorithm only. + + if (i > 0) { + continue; + } + + (function() { + // non-authenticating mode: + var encrypt = crypto.createCipheriv('aes-128-cbc', + 'ipxp9a6i1Mb4USb4', '6fKjEjR3Vl30EUYC'); + encrypt.update('blah', 'ascii'); + encrypt.final(); + assert.throws(function() { encrypt.getAuthTag(); }); + })(); + + (function() { + // trying to get tag before inputting all data: + var encrypt = crypto.createCipheriv(test.algo, test.key, test.iv); + encrypt.update('blah', 'ascii'); + assert.throws(function() { encrypt.getAuthTag(); }); + })(); + + (function() { + // trying to set tag on encryption object: + var encrypt = crypto.createCipheriv(test.algo, test.key, test.iv); + assert.throws(function() { + encrypt.setAuthTag(new Buffer(test.tag, 'hex')); }); + })(); + + (function() { + // trying to read tag from decryption object: + var decrypt = crypto.createDecipheriv(test.algo, test.key, test.iv); + assert.throws(function() { decrypt.getAuthTag(); }); + })(); +}