crypto: support GCM authenticated encryption mode.
authorIngmar Runge <ingmar@irsoft.de>
Tue, 19 Nov 2013 21:38:15 +0000 (22:38 +0100)
committerFedor Indutny <fedor.indutny@gmail.com>
Sat, 7 Dec 2013 20:00:02 +0000 (00:00 +0400)
This adds two new member functions getAuthTag and setAuthTag that
are useful for AES-GCM encryption modes. Use getAuthTag after
Cipheriv.final, transmit the tag along with the data and use
Decipheriv.setAuthTag to have the encrypted data verified.

doc/api/crypto.markdown
lib/crypto.js
src/node_crypto.cc
src/node_crypto.h
test/simple/test-crypto-authenticated.js [new file with mode: 0644]

index c0227c8..dc8f9b5 100644 (file)
@@ -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
index db3ce1c..add6a79 100644 (file)
@@ -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;
 
 
 
index beb6bd9..1e7bf36 100644 (file)
@@ -2122,6 +2122,8 @@ void CipherBase::Initialize(Environment* env, Handle<Object> 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<Value>& 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<Value>& args) {
+  Environment* env = Environment::GetCurrent(args.GetIsolate());
+  HandleScope handle_scope(args.GetIsolate());
+  CipherBase* cipher = Unwrap<CipherBase>(args.This());
+
+  char* out = NULL;
+  unsigned int out_len = 0;
+
+  if (cipher->GetAuthTag(&out, &out_len)) {
+    Local<Object> 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<Value>& args) {
+  HandleScope handle_scope(args.GetIsolate());
+
+  Local<Object> buf = args[0].As<Object>();
+  if (!buf->IsObject() || !Buffer::HasInstance(buf))
+    return ThrowTypeError("Argument must be a Buffer");
+
+  CipherBase* cipher = Unwrap<CipherBase>(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<unsigned char*>(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<unsigned char*>(auth_tag_));
+    }
+  }
+
   EVP_CIPHER_CTX_cleanup(&ctx_);
   initialised_ = false;
 
index 05f5e36..f11f2a0 100644 (file)
@@ -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<v8::Value>& args);
   static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void InitIv(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -346,13 +351,18 @@ class CipherBase : public BaseObject {
   static void Final(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void SetAutoPadding(const v8::FunctionCallbackInfo<v8::Value>& args);
 
+  static void GetAuthTag(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void SetAuthTag(const v8::FunctionCallbackInfo<v8::Value>& args);
+
   CipherBase(Environment* env,
              v8::Local<v8::Object> wrap,
              CipherKind kind)
       : BaseObject(env, wrap),
         cipher_(NULL),
         initialised_(false),
-        kind_(kind) {
+        kind_(kind),
+        auth_tag_(NULL),
+        auth_tag_len_(0) {
     MakeWeak<CipherBase>(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 (file)
index 0000000..ff9eeda
--- /dev/null
@@ -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(); });
+  })();
+}