Skip to content

Commit

Permalink
crypto: support GCM authenticated encryption mode.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
KiNgMaR authored and indutny committed Dec 7, 2013
1 parent f9f9239 commit e0d31ea
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 1 deletion.
16 changes: 16 additions & 0 deletions doc/api/crypto.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;



Expand Down
90 changes: 90 additions & 0 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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_,
Expand Down Expand Up @@ -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;

Expand Down
14 changes: 13 additions & 1 deletion src/node_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ class CipherBase : public BaseObject {
~CipherBase() {
if (!initialised_)
return;
delete[] auth_tag_;
EVP_CIPHER_CTX_cleanup(&ctx_);
}

Expand All @@ -339,20 +340,29 @@ 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);
static void Update(const v8::FunctionCallbackInfo<v8::Value>& args);
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);
}

Expand All @@ -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 {
Expand Down
130 changes: 130 additions & 0 deletions test/simple/test-crypto-authenticated.js
Original file line number Diff line number Diff line change
@@ -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(); });
})();
}

0 comments on commit e0d31ea

Please sign in to comment.