Skip to content
Prev Previous commit
Next Next commit
crypto: update tests for createCipher
  • Loading branch information
Ian Carroll committed Jul 9, 2017
commit 7f913ec6506aa7dbbff6d074b26fa76292ce07cd
7 changes: 5 additions & 2 deletions benchmark/crypto/cipher-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ function main(conf) {
// alice_secret and bob_secret should be the same
assert(alice_secret === bob_secret);

var alice_cipher = crypto.createCipher(conf.cipher, alice_secret);
var bob_cipher = crypto.createDecipher(conf.cipher, bob_secret);
const key = crypto.generateLegacyKey(conf.cipher, alice_secret);
const iv = crypto.generateLegacyIV(conf.cipher, alice_secret);

var alice_cipher = crypto.createCipheriv(conf.cipher, key, iv);
var bob_cipher = crypto.createDecipheriv(conf.cipher, key, iv);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use const instead of var.


var message;
var encoding;
Expand Down
2 changes: 1 addition & 1 deletion lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Object.defineProperty(exports, 'Cipher', {
function Cipher(cipher, password, options) {
if (!(this instanceof Cipher))
return new Cipher(cipher, password, options);

this._handle = new binding.CipherBase(true);

this._handle.init(cipher, toBuf(password));
Expand Down
27 changes: 19 additions & 8 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3301,7 +3301,7 @@ void GenerateLegacyKey(const FunctionCallbackInfo<Value>& args) {
}

THROW_AND_RETURN_IF_NOT_STRING(args[0], "Cipher");
THROW_AND_RETURN_IF_NOT_STRING(args[1], "Key");
THROW_AND_RETURN_IF_NOT_STRING_OR_BUFFER(args[1], "Key");

const node::Utf8Value cipher_type(env->isolate(), args[0]);
const node::Utf8Value key_value(env->isolate(), args[1]);
Expand All @@ -3324,8 +3324,12 @@ void GenerateLegacyKey(const FunctionCallbackInfo<Value>& args) {
1,
key,
nullptr);

Local<Object> buf = Buffer::New(env, (char *) key, (unsigned) key_len).ToLocalChecked();

Local<Object> buf = Buffer::New(env,
reinterpret_cast<char *>(key),
(unsigned) key_len)
.ToLocalChecked();

args.GetReturnValue().Set(buf);
}

Expand All @@ -3337,7 +3341,7 @@ void GenerateLegacyIV(const FunctionCallbackInfo<Value>& args) {
}

THROW_AND_RETURN_IF_NOT_STRING(args[0], "Cipher");
THROW_AND_RETURN_IF_NOT_STRING(args[1], "Key");
THROW_AND_RETURN_IF_NOT_STRING_OR_BUFFER(args[1], "Key");

const node::Utf8Value cipher_type(env->isolate(), args[0]);
const node::Utf8Value key_value(env->isolate(), args[1]);
Expand Down Expand Up @@ -3366,7 +3370,11 @@ void GenerateLegacyIV(const FunctionCallbackInfo<Value>& args) {
nullptr,
iv);

Local<Object> buf = Buffer::New(env, (char *) iv, (unsigned) expected_iv_len).ToLocalChecked();
Local<Object> buf = Buffer::New(env,
reinterpret_cast<char *>(iv),
(unsigned) expected_iv_len)
.ToLocalChecked();

args.GetReturnValue().Set(buf);
}

Expand Down Expand Up @@ -3416,6 +3424,7 @@ void CipherBase::Init(const char* cipher_type,
}

unsigned char key[EVP_MAX_KEY_LENGTH];
unsigned char iv[EVP_MAX_KEY_LENGTH];

int key_len = EVP_BytesToKey(cipher_,
EVP_md5(),
Expand All @@ -3424,7 +3433,7 @@ void CipherBase::Init(const char* cipher_type,
key_buf_len,
1,
key,
nullptr);
iv);

EVP_CIPHER_CTX_init(&ctx_);
const bool encrypt = (kind_ == kCipher);
Expand All @@ -3434,7 +3443,9 @@ void CipherBase::Init(const char* cipher_type,
const int iv_len = EVP_CIPHER_iv_length(cipher_);

if (encrypt && iv_len != 0) {
return env()->ThrowError("crypto.createCipher() is no longer supported with ciphers that require initialization vectors. Generate a random IV and pass it to crypto.createCipheriv().");
return env()->ThrowError("crypto.createCipher() is no longer supported"
" with ciphers that require initialization vectors. "
" Generate a random IV and pass it to crypto.createCipheriv().");
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No comments on whether this is something we should do (ping @nodejs/crypto) ... but this is not the correct way of doing it. There are two kinds of deprecations we use: documentation and runtime. A runtime deprecation uses either the util.deprecate() or process.emitWarning('...', 'DeprecationWarning') API to emit a warning at runtime, but the code still continues to operate as it did in every other respect.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My PR intends to immediately prohibit the use of this API for ciphers that require an IV. The deprecation of this API entirely hasn’t been done yet. I’m guessing that would go in lib/crypto.js?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, and that's the point: Our deprecation policy requires that APIs go through a proper deprecation cycle so this PR would not be able to land as is. The first step is to do a docs or runtime deprecation as a semver-major, which would go into Node.js 9.0.0.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have provisions for making exceptions for security-related issues but as this is more about protecting users against themselves, it's up for debate whether those provisions apply.

I'm personally leaning towards 'acceptable in node 9' (raising an exception, that is) but I won't hold it against anyone who feels differently.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prohibits users from using not only counter modes but also cbc modes. I'm not sure if the cbc mode has a security issue in this API.


if (!EVP_CIPHER_CTX_set_key_length(&ctx_, key_len)) {
Expand All @@ -3446,7 +3457,7 @@ void CipherBase::Init(const char* cipher_type,
nullptr,
nullptr,
reinterpret_cast<unsigned char*>(key),
nullptr,
reinterpret_cast<unsigned char*>(iv),
kind_ == kCipher);
initialised_ = true;
}
Expand Down
5 changes: 3 additions & 2 deletions test/parallel/test-crypto-authenticated.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ const errMessages = {
auth: / auth/,
state: / state/,
FIPS: /not supported in FIPS mode/,
IV: /no longer supported with ciphers that require initialization vectors/,
length: /Invalid IV length/,
};

Expand Down Expand Up @@ -390,9 +391,9 @@ for (const i in TEST_CASES) {
}

if (test.password) {
if (common.hasFipsCrypto) {
if (!test.algo.endsWith('ecb')) {
assert.throws(() => { crypto.createCipher(test.algo, test.password); },
errMessages.FIPS);
errMessages.IV);
} else {
const encrypt = crypto.createCipher(test.algo, test.password);
if (test.aad)
Expand Down
16 changes: 12 additions & 4 deletions test/parallel/test-crypto-binary-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -456,17 +456,21 @@ assert.strictEqual(s3Verified, true, 'sign and verify (buffer)');


function testCipher1(key) {
const derived_key = crypto.generateLegacyKey('aes-192-cbc', key);
const iv = crypto.generateLegacyIV('aes-192-cbc', key);

// Test encryption and decryption
const plaintext = 'Keep this a secret? No! Tell everyone about node.js!';
const cipher = crypto.createCipher('aes192', key);
const cipher = crypto.createCipheriv('aes-192-cbc', derived_key, iv);

// encrypt plaintext which is in utf8 format
// to a ciphertext which will be in hex
let ciph = cipher.update(plaintext, 'utf8', 'hex');
// Only use binary or hex, not base64.
ciph += cipher.final('hex');

const decipher = crypto.createDecipher('aes192', key);
const decipher = crypto.createDecipheriv('aes-192-cbc', derived_key, iv);

let txt = decipher.update(ciph, 'hex', 'utf8');
txt += decipher.final('utf8');

Expand All @@ -481,14 +485,18 @@ function testCipher2(key) {
'32|RmVZZkFUVmpRRkp0TmJaUm56ZU9qcnJkaXNNWVNpTTU*|iXmckfRWZBGWWELw' +
'eCBsThSsfUHLeRe0KCsK8ooHgxie0zOINpXxfZi/oNG7uq9JWFVCk70gfzQH8ZUJ' +
'jAfaFg**';
const cipher = crypto.createCipher('aes256', key);

const derived_key = crypto.generateLegacyKey('aes-256-cbc', key);
const iv = crypto.generateLegacyIV('aes-256-cbc', key);

const cipher = crypto.createCipheriv('aes-256-cbc', derived_key, iv);

// encrypt plaintext which is in utf8 format
// to a ciphertext which will be in Base64
let ciph = cipher.update(plaintext, 'utf8', 'base64');
ciph += cipher.final('base64');

const decipher = crypto.createDecipher('aes256', key);
const decipher = crypto.createDecipheriv('aes-256-cbc', derived_key, iv);
let txt = decipher.update(ciph, 'base64', 'utf8');
txt += decipher.final('utf8');

Expand Down
65 changes: 44 additions & 21 deletions test/parallel/test-crypto-cipher-decipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@ const crypto = require('crypto');
const assert = require('assert');

function testCipher1(key) {
key = crypto.generateLegacyKey('aes-192-cbc', key);
const iv = crypto.generateLegacyIV('aes-192-cbc', key);

// Test encryption and decryption
const plaintext = 'Keep this a secret? No! Tell everyone about node.js!';
const cipher = crypto.createCipher('aes192', key);

const cipher = crypto.createCipheriv('aes-192-cbc', key, iv);

// encrypt plaintext which is in utf8 format
// to a ciphertext which will be in hex
let ciph = cipher.update(plaintext, 'utf8', 'hex');
// Only use binary or hex, not base64.
ciph += cipher.final('hex');

const decipher = crypto.createDecipher('aes192', key);
const decipher = crypto.createDecipheriv('aes-192-cbc', key, iv);
let txt = decipher.update(ciph, 'hex', 'utf8');
txt += decipher.final('utf8');

Expand All @@ -33,11 +37,11 @@ function testCipher1(key) {
// NB: In real life, it's not guaranteed that you can get all of it
// in a single read() like this. But in this case, we know it's
// quite small, so there's no harm.
const cStream = crypto.createCipher('aes192', key);
const cStream = crypto.createCipheriv('aes-192-cbc', key, iv);
cStream.end(plaintext);
ciph = cStream.read();

const dStream = crypto.createDecipher('aes192', key);
const dStream = crypto.createDecipheriv('aes-192-cbc', key, iv);
dStream.end(ciph);
txt = dStream.read().toString('utf8');

Expand All @@ -48,18 +52,21 @@ function testCipher1(key) {
function testCipher2(key) {
// encryption and decryption with Base64
// reported in https://github.com/joyent/node/issues/738
key = crypto.generateLegacyKey('aes-256-cbc', key);
const iv = crypto.generateLegacyIV('aes-256-cbc', key);

const plaintext =
'32|RmVZZkFUVmpRRkp0TmJaUm56ZU9qcnJkaXNNWVNpTTU*|iXmckfRWZBGWWELw' +
'eCBsThSsfUHLeRe0KCsK8ooHgxie0zOINpXxfZi/oNG7uq9JWFVCk70gfzQH8ZUJ' +
'jAfaFg**';
const cipher = crypto.createCipher('aes256', key);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);

// encrypt plaintext which is in utf8 format
// to a ciphertext which will be in Base64
let ciph = cipher.update(plaintext, 'utf8', 'base64');
ciph += cipher.final('base64');

const decipher = crypto.createDecipher('aes256', key);
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
let txt = decipher.update(ciph, 'base64', 'utf8');
txt += decipher.final('utf8');

Expand All @@ -74,67 +81,81 @@ testCipher2(Buffer.from('0123456789abcdef'));

// Base64 padding regression test, see #4837.
{
const c = crypto.createCipher('aes-256-cbc', 'secret');
const key = crypto.generateLegacyKey('aes-256-cbc', 'secret');
const iv = crypto.generateLegacyIV('aes-256-cbc', 'secret');

const c = crypto.createCipheriv('aes-256-cbc', key, iv);
const s = c.update('test', 'utf8', 'base64') + c.final('base64');
assert.strictEqual(s, '375oxUQCIocvxmC5At+rvA==');
}

// Calling Cipher.final() or Decipher.final() twice should error but
// not assert. See #4886.
{
const c = crypto.createCipher('aes-256-cbc', 'secret');
const key = crypto.generateLegacyKey('aes-256-cbc', 'secret');
const iv = crypto.generateLegacyIV('aes-256-cbc', 'secret');

const c = crypto.createCipheriv('aes-256-cbc', key, iv);
try { c.final('xxx'); } catch (e) { /* Ignore. */ }
try { c.final('xxx'); } catch (e) { /* Ignore. */ }
try { c.final('xxx'); } catch (e) { /* Ignore. */ }
const d = crypto.createDecipher('aes-256-cbc', 'secret');
const d = crypto.createDecipheriv('aes-256-cbc', key, iv);
try { d.final('xxx'); } catch (e) { /* Ignore. */ }
try { d.final('xxx'); } catch (e) { /* Ignore. */ }
try { d.final('xxx'); } catch (e) { /* Ignore. */ }
}

// Regression test for #5482: string to Cipher#update() should not assert.
{
const c = crypto.createCipher('aes192', '0123456789abcdef');
const key = crypto.generateLegacyKey('aes-192-cbc', '0123456789abcdef');
const iv = crypto.generateLegacyIV('aes-192-cbc', '0123456789abcdef');

const c = crypto.createCipheriv('aes-192-cbc', key, iv);
c.update('update');
c.final();
}

// #5655 regression tests, 'utf-8' and 'utf8' are identical.
{
let c = crypto.createCipher('aes192', '0123456789abcdef');
const key = crypto.generateLegacyKey('aes-192-cbc', '0123456789abcdef');
const iv = crypto.generateLegacyIV('aes-192-cbc', '0123456789abcdef');

let c = crypto.createCipheriv('aes-192-cbc', key, iv);
c.update('update', ''); // Defaults to "utf8".
c.final('utf-8'); // Should not throw.

c = crypto.createCipher('aes192', '0123456789abcdef');
c = crypto.createCipheriv('aes-192-cbc', key, iv);
c.update('update', 'utf8');
c.final('utf-8'); // Should not throw.

c = crypto.createCipher('aes192', '0123456789abcdef');
c = crypto.createCipheriv('aes-192-cbc', key, iv);
c.update('update', 'utf-8');
c.final('utf8'); // Should not throw.
}

// Regression tests for https://github.com/nodejs/node/issues/8236.
{
const key = '0123456789abcdef';
const key = crypto.generateLegacyKey('aes-192-cbc', '0123456789abcdef');
const iv = crypto.generateLegacyIV('aes-192-cbc', '0123456789abcdef');

const plaintext = 'Top secret!!!';
const c = crypto.createCipher('aes192', key);
const c = crypto.createCipheriv('aes-192-cbc', key, iv);
let ciph = c.update(plaintext, 'utf16le', 'base64');
ciph += c.final('base64');

let decipher = crypto.createDecipher('aes192', key);
let decipher = crypto.createDecipheriv('aes-192-cbc', key, iv);

let txt;
assert.doesNotThrow(() => txt = decipher.update(ciph, 'base64', 'ucs2'));
assert.doesNotThrow(() => txt += decipher.final('ucs2'));
assert.strictEqual(txt, plaintext, 'decrypted result in ucs2');

decipher = crypto.createDecipher('aes192', key);
decipher = crypto.createDecipheriv('aes-192-cbc', key, iv);
assert.doesNotThrow(() => txt = decipher.update(ciph, 'base64', 'ucs-2'));
assert.doesNotThrow(() => txt += decipher.final('ucs-2'));
assert.strictEqual(txt, plaintext, 'decrypted result in ucs-2');

decipher = crypto.createDecipher('aes192', key);
decipher = crypto.createDecipheriv('aes-192-cbc', key, iv);
assert.doesNotThrow(() => txt = decipher.update(ciph, 'base64', 'utf-16le'));
assert.doesNotThrow(() => txt += decipher.final('utf-16le'));
assert.strictEqual(txt, plaintext, 'decrypted result in utf-16le');
Expand All @@ -153,11 +174,13 @@ testCipher2(Buffer.from('0123456789abcdef'));

// error throwing in setAAD/setAuthTag/getAuthTag/setAutoPadding
{
const key = '0123456789';
const key = crypto.generateLegacyKey('aes-256-gcm', '0123456789');
const iv = crypto.generateLegacyIV('aes-256-gcm', '0123456789');

const aadbuf = Buffer.from('aadbuf');
const data = Buffer.from('test-crypto-cipher-decipher');

const cipher = crypto.createCipher('aes-256-gcm', key);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
cipher.setAAD(aadbuf);
cipher.setAutoPadding();

Expand All @@ -167,7 +190,7 @@ testCipher2(Buffer.from('0123456789abcdef'));

const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);

const decipher = crypto.createDecipher('aes-256-gcm', key);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAAD(aadbuf);
decipher.setAuthTag(cipher.getAuthTag());
decipher.setAutoPadding();
Expand Down
16 changes: 15 additions & 1 deletion test/parallel/test-crypto-deprecated.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ const tls = require('tls');

common.expectWarning('DeprecationWarning', [
'crypto.Credentials is deprecated. Use tls.SecureContext instead.',
'crypto.createCredentials is deprecated. Use tls.createSecureContext instead.'
'crypto.createCredentials is deprecated. ' +
'Use tls.createSecureContext instead.',
'crypto.createCipher is deprecated. Use crypto.createCipheriv instead.',
'crypto.Cipher is deprecated. Use crypto.createCipheriv instead.',
]);

// Accessing the deprecated function is enough to trigger the warning event.
Expand All @@ -20,3 +23,14 @@ common.expectWarning('DeprecationWarning', [
// mapped to the correct non-deprecated function.
assert.strictEqual(crypto.Credentials, tls.SecureContext);
assert.strictEqual(crypto.createCredentials, tls.createSecureContext);

crypto.createCipher;
crypto.Cipher;

assert.throws(() => {
crypto.createCipher('aes-128-cbc', 'this-should-throw');
}, /no longer supported with ciphers that require initialization vectors/);

assert.doesNotThrow(() => {
crypto.createCipher('aes-128-ecb', 'this-should-warn');
});
2 changes: 1 addition & 1 deletion test/parallel/test-crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ testImmutability(crypto.getCurves);
// throw, not assert in C++ land.
assert.throws(function() {
crypto.createCipher('aes192', 'test').update('0', 'hex');
}, common.hasFipsCrypto ? /not supported in FIPS mode/ : /Bad input string/);
}, /no longer supported with ciphers that require initialization vectors/);

assert.throws(function() {
crypto.createDecipher('aes192', 'test').update('0', 'hex');
Expand Down