Browse Source

crypto: support GCM authenticated encryption mode.

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.
v0.11.10-release
Ingmar Runge 11 years ago
committed by Fedor Indutny
parent
commit
e0d31ea2db
  1. 16
      doc/api/crypto.markdown
  2. 11
      lib/crypto.js
  3. 90
      src/node_crypto.cc
  4. 14
      src/node_crypto.h
  5. 130
      test/simple/test-crypto-authenticated.js

16
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

11
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;

90
src/node_crypto.cc

@ -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;

14
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<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 {

130
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(); });
})();
}
Loading…
Cancel
Save