Skip to content

Commit

Permalink
crypto: decode missing passphrase errors
Browse files Browse the repository at this point in the history
When a user attempts to load an encrypted key without supplying a
passphrase, a cryptic OpenSSL error is thrown. This change intercepts
the OpenSSL error and throws a nice error code instead.
  • Loading branch information
tniessen committed Mar 29, 2019
1 parent 8df9fdc commit 36de9dc
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 79 deletions.
6 changes: 6 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,12 @@ instance, calling [`cipher.getAuthTag()`][] before calling `cipher.final()`.
The PBKDF2 algorithm failed for unspecified reasons. OpenSSL does not provide
more details and therefore neither does Node.js.

<a id="ERR_CRYPTO_READ_KEY"></a>
### ERR_CRYPTO_READ_KEY

An error occurred while parsing a cryptographic key, e.g., the key is encrypted
but no decryption passphrase was specified.

<a id="ERR_CRYPTO_SCRYPT_INVALID_PARAMETER"></a>
### ERR_CRYPTO_SCRYPT_INVALID_PARAMETER

Expand Down
177 changes: 112 additions & 65 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,33 @@ template int SSLWrap<TLSWrap>::SelectALPNCallback(
unsigned int inlen,
void* arg);

class PasswordCallbackInfo {
public:
explicit PasswordCallbackInfo(const char* passphrase)
: passphrase_(passphrase) {}

inline const char* GetPassword() {
needs_passphrase_ = true;
return passphrase_;
}

inline bool CalledButEmpty() {
return needs_passphrase_ && passphrase_ == nullptr;
}

private:
const char* passphrase_;
bool needs_passphrase_ = false;
};

static int PasswordCallback(char* buf, int size, int rwflag, void* u) {
if (u) {
PasswordCallbackInfo* info = static_cast<PasswordCallbackInfo*>(u);
const char* passphrase = info->GetPassword();
if (passphrase != nullptr) {
size_t buflen = static_cast<size_t>(size);
size_t len = strlen(static_cast<const char*>(u));
size_t len = strlen(passphrase);
len = len > buflen ? buflen : len;
memcpy(buf, u, len);
memcpy(buf, passphrase, len);
return len;
}

Expand Down Expand Up @@ -698,11 +718,12 @@ void SecureContext::SetKey(const FunctionCallbackInfo<Value>& args) {

node::Utf8Value passphrase(env->isolate(), args[1]);

PasswordCallbackInfo cb_info(len == 1 ? nullptr : *passphrase);
EVPKeyPointer key(
PEM_read_bio_PrivateKey(bio.get(),
nullptr,
PasswordCallback,
len == 1 ? nullptr : *passphrase));
&cb_info));

if (!key) {
unsigned long err = ERR_get_error(); // NOLINT(runtime/int)
Expand Down Expand Up @@ -2899,13 +2920,14 @@ static bool IsSupportedAuthenticatedMode(const EVP_CIPHER_CTX* ctx) {
return IsSupportedAuthenticatedMode(cipher);
}

enum class ParsePublicKeyResult {
kParsePublicOk,
kParsePublicNotRecognized,
kParsePublicFailed
enum class ParseKeyResult {
kParseKeyOk,
kParseKeyNotRecognized,
kParseKeyNeedPassphrase,
kParseKeyFailed
};

static ParsePublicKeyResult TryParsePublicKey(
static ParseKeyResult TryParsePublicKey(
EVPKeyPointer* pkey,
const BIOPointer& bp,
const char* name,
Expand All @@ -2919,33 +2941,33 @@ static ParsePublicKeyResult TryParsePublicKey(
MarkPopErrorOnReturn mark_pop_error_on_return;
if (PEM_bytes_read_bio(&der_data, &der_len, nullptr, name,
bp.get(), nullptr, nullptr) != 1)
return ParsePublicKeyResult::kParsePublicNotRecognized;
return ParseKeyResult::kParseKeyNotRecognized;
}

// OpenSSL might modify the pointer, so we need to make a copy before parsing.
const unsigned char* p = der_data;
pkey->reset(parse(&p, der_len));
OPENSSL_clear_free(der_data, der_len);

return *pkey ? ParsePublicKeyResult::kParsePublicOk :
ParsePublicKeyResult::kParsePublicFailed;
return *pkey ? ParseKeyResult::kParseKeyOk :
ParseKeyResult::kParseKeyFailed;
}

static ParsePublicKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey,
const char* key_pem,
int key_pem_len) {
static ParseKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey,
const char* key_pem,
int key_pem_len) {
BIOPointer bp(BIO_new_mem_buf(const_cast<char*>(key_pem), key_pem_len));
if (!bp)
return ParsePublicKeyResult::kParsePublicFailed;
return ParseKeyResult::kParseKeyFailed;

ParsePublicKeyResult ret;
ParseKeyResult ret;

// Try parsing as a SubjectPublicKeyInfo first.
ret = TryParsePublicKey(pkey, bp, "PUBLIC KEY",
[](const unsigned char** p, long l) { // NOLINT(runtime/int)
return d2i_PUBKEY(nullptr, p, l);
});
if (ret != ParsePublicKeyResult::kParsePublicNotRecognized)
if (ret != ParseKeyResult::kParseKeyNotRecognized)
return ret;

// Maybe it is PKCS#1.
Expand All @@ -2954,7 +2976,7 @@ static ParsePublicKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey,
[](const unsigned char** p, long l) { // NOLINT(runtime/int)
return d2i_PublicKey(EVP_PKEY_RSA, nullptr, p, l);
});
if (ret != ParsePublicKeyResult::kParsePublicNotRecognized)
if (ret != ParseKeyResult::kParseKeyNotRecognized)
return ret;

// X.509 fallback.
Expand All @@ -2966,25 +2988,25 @@ static ParsePublicKeyResult ParsePublicKeyPEM(EVPKeyPointer* pkey,
});
}

static bool ParsePublicKey(EVPKeyPointer* pkey,
const PublicKeyEncodingConfig& config,
const char* key,
size_t key_len) {
static ParseKeyResult ParsePublicKey(EVPKeyPointer* pkey,
const PublicKeyEncodingConfig& config,
const char* key,
size_t key_len) {
if (config.format_ == kKeyFormatPEM) {
ParsePublicKeyResult r =
ParsePublicKeyPEM(pkey, key, key_len);
return r == ParsePublicKeyResult::kParsePublicOk;
return ParsePublicKeyPEM(pkey, key, key_len);
} else {
CHECK_EQ(config.format_, kKeyFormatDER);

const unsigned char* p = reinterpret_cast<const unsigned char*>(key);
if (config.type_.ToChecked() == kKeyEncodingPKCS1) {
pkey->reset(d2i_PublicKey(EVP_PKEY_RSA, nullptr, &p, key_len));
return pkey;
} else {
CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSPKI);
pkey->reset(d2i_PUBKEY(nullptr, &p, key_len));
return pkey;
}

return *pkey ? ParseKeyResult::kParseKeyOk :
ParseKeyResult::kParseKeyFailed;
}
}

Expand Down Expand Up @@ -3099,56 +3121,59 @@ static bool IsEncryptedPrivateKeyInfo(const unsigned char* data, size_t size) {
data[offset] != 2;
}

static EVPKeyPointer ParsePrivateKey(const PrivateKeyEncodingConfig& config,
const char* key,
size_t key_len) {
EVPKeyPointer pkey;
static ParseKeyResult ParsePrivateKey(EVPKeyPointer* pkey,
const PrivateKeyEncodingConfig& config,
const char* key,
size_t key_len) {
PasswordCallbackInfo pc_info(config.passphrase_.get());

if (config.format_ == kKeyFormatPEM) {
BIOPointer bio(BIO_new_mem_buf(key, key_len));
if (!bio)
return pkey;
return ParseKeyResult::kParseKeyFailed;

char* pass = const_cast<char*>(config.passphrase_.get());
pkey.reset(PEM_read_bio_PrivateKey(bio.get(),
nullptr,
PasswordCallback,
pass));
pkey->reset(PEM_read_bio_PrivateKey(bio.get(),
nullptr,
PasswordCallback,
&pc_info));
} else {
CHECK_EQ(config.format_, kKeyFormatDER);

if (config.type_.ToChecked() == kKeyEncodingPKCS1) {
const unsigned char* p = reinterpret_cast<const unsigned char*>(key);
pkey.reset(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, key_len));
pkey->reset(d2i_PrivateKey(EVP_PKEY_RSA, nullptr, &p, key_len));
} else if (config.type_.ToChecked() == kKeyEncodingPKCS8) {
BIOPointer bio(BIO_new_mem_buf(key, key_len));
if (!bio)
return pkey;
return ParseKeyResult::kParseKeyFailed;

if (IsEncryptedPrivateKeyInfo(
reinterpret_cast<const unsigned char*>(key), key_len)) {
char* pass = const_cast<char*>(config.passphrase_.get());
pkey.reset(d2i_PKCS8PrivateKey_bio(bio.get(),
nullptr,
PasswordCallback,
pass));
pkey->reset(d2i_PKCS8PrivateKey_bio(bio.get(),
nullptr,
PasswordCallback,
&pc_info));
} else {
PKCS8Pointer p8inf(d2i_PKCS8_PRIV_KEY_INFO_bio(bio.get(), nullptr));
if (p8inf)
pkey.reset(EVP_PKCS82PKEY(p8inf.get()));
pkey->reset(EVP_PKCS82PKEY(p8inf.get()));
}
} else {
CHECK_EQ(config.type_.ToChecked(), kKeyEncodingSEC1);
const unsigned char* p = reinterpret_cast<const unsigned char*>(key);
pkey.reset(d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, key_len));
pkey->reset(d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, key_len));
}
}

// OpenSSL can fail to parse the key but still return a non-null pointer.
if (ERR_peek_error() != 0)
pkey.reset();
pkey->reset();

return pkey;
if (*pkey)
return ParseKeyResult::kParseKeyOk;
if (pc_info.CalledButEmpty())
return ParseKeyResult::kParseKeyNeedPassphrase;
return ParseKeyResult::kParseKeyFailed;
}

ByteSource::ByteSource(ByteSource&& other)
Expand Down Expand Up @@ -3284,6 +3309,26 @@ static PublicKeyEncodingConfig GetPublicKeyEncodingFromJs(
return result;
}

#define READ_FAILURE_MSG(type) ("Failed to read " type " key")

static inline ManagedEVPPKey GetParsedKey(Environment* env,
EVPKeyPointer&& pkey,
ParseKeyResult ret,
const char* default_msg) {
switch (ret) {
case ParseKeyResult::kParseKeyOk:
CHECK(pkey);
break;
case ParseKeyResult::kParseKeyNeedPassphrase:
THROW_ERR_CRYPTO_READ_KEY(env, "Passphrase required for encrypted key");
break;
default:
ThrowCryptoError(env, ERR_get_error(), default_msg);
}

return ManagedEVPPKey(std::move(pkey));
}

static NonCopyableMaybe<PrivateKeyEncodingConfig> GetPrivateKeyEncodingFromJs(
const FunctionCallbackInfo<Value>& args,
unsigned int* offset,
Expand Down Expand Up @@ -3339,11 +3384,11 @@ static ManagedEVPPKey GetPrivateKeyFromJs(
GetPrivateKeyEncodingFromJs(args, offset, kKeyContextInput);
if (config.IsEmpty())
return ManagedEVPPKey();
EVPKeyPointer pkey =
ParsePrivateKey(config.Release(), key.get(), key.size());
if (!pkey)
ThrowCryptoError(env, ERR_get_error(), "Failed to read private key");
return ManagedEVPPKey(std::move(pkey));

EVPKeyPointer pkey;
ParseKeyResult ret =
ParsePrivateKey(&pkey, config.Release(), key.get(), key.size());
return GetParsedKey(env, std::move(pkey), ret, READ_FAILURE_MSG("private"));
} else {
CHECK(args[*offset]->IsObject() && allow_key_object);
KeyObject* key;
Expand All @@ -3364,15 +3409,16 @@ static ManagedEVPPKey GetPublicOrPrivateKeyFromJs(
GetPrivateKeyEncodingFromJs(args, offset, kKeyContextInput);
if (config_.IsEmpty())
return ManagedEVPPKey();

ParseKeyResult ret;
PrivateKeyEncodingConfig config = config_.Release();
EVPKeyPointer pkey;
if (config.format_ == kKeyFormatPEM) {
// For PEM, we can easily determine whether it is a public or private key
// by looking for the respective PEM tags.
ParsePublicKeyResult ret = ParsePublicKeyPEM(&pkey, data.get(),
data.size());
if (ret == ParsePublicKeyResult::kParsePublicNotRecognized) {
pkey = ParsePrivateKey(config, data.get(), data.size());
ret = ParsePublicKeyPEM(&pkey, data.get(), data.size());
if (ret == ParseKeyResult::kParseKeyNotRecognized) {
ret = ParsePrivateKey(&pkey, config, data.get(), data.size());
}
} else {
// For DER, the type determines how to parse it. SPKI, PKCS#8 and SEC1 are
Expand All @@ -3395,14 +3441,14 @@ static ManagedEVPPKey GetPublicOrPrivateKeyFromJs(
}

if (is_public) {
ParsePublicKey(&pkey, config, data.get(), data.size());
ret = ParsePublicKey(&pkey, config, data.get(), data.size());
} else {
pkey = ParsePrivateKey(config, data.get(), data.size());
ret = ParsePrivateKey(&pkey, config, data.get(), data.size());
}
}
if (!pkey)
ThrowCryptoError(env, ERR_get_error(), "Failed to read asymmetric key");
return ManagedEVPPKey(std::move(pkey));

return GetParsedKey(env, std::move(pkey), ret,
READ_FAILURE_MSG("asymmetric"));
} else {
CHECK(args[*offset]->IsObject());
KeyObject* key = Unwrap<KeyObject>(args[*offset].As<Object>());
Expand Down Expand Up @@ -3585,6 +3631,7 @@ KeyType KeyObject::GetKeyType() const {
void KeyObject::Init(const FunctionCallbackInfo<Value>& args) {
KeyObject* key;
ASSIGN_OR_RETURN_UNWRAP(&key, args.Holder());
MarkPopErrorOnReturn mark_pop_error_on_return;

unsigned int offset;
ManagedEVPPKey pkey;
Expand Down Expand Up @@ -4780,6 +4827,8 @@ void Sign::SignFinal(const FunctionCallbackInfo<Value>& args) {
Sign* sign;
ASSIGN_OR_RETURN_UNWRAP(&sign, args.Holder());

ClearErrorOnReturn clear_error_on_return;

unsigned int offset = 0;
ManagedEVPPKey key = GetPrivateKeyFromJs(args, &offset, true);
if (!key)
Expand All @@ -4791,8 +4840,6 @@ void Sign::SignFinal(const FunctionCallbackInfo<Value>& args) {
CHECK(args[offset + 1]->IsInt32());
int salt_len = args[offset + 1].As<Int32>()->Value();

ClearErrorOnReturn clear_error_on_return;

SignResult ret = sign->SignFinal(
key,
padding,
Expand Down
1 change: 1 addition & 0 deletions src/node_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ void FatalException(v8::Isolate* isolate,
V(ERR_BUFFER_TOO_LARGE, Error) \
V(ERR_CANNOT_TRANSFER_OBJECT, TypeError) \
V(ERR_CONSTRUCT_CALL_REQUIRED, Error) \
V(ERR_CRYPTO_READ_KEY, TypeError) \
V(ERR_INVALID_ARG_VALUE, TypeError) \
V(ERR_INVALID_ARG_TYPE, TypeError) \
V(ERR_INVALID_MODULE_SPECIFIER, TypeError) \
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/keys/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ all: \
dh2048.pem \
dsa1025.pem \
dsa_private_1025.pem \
dsa_private_encrypted_1025.pem \
dsa_public_1025.pem \
ec-cert.pem \
ec.pfx \
Expand Down Expand Up @@ -549,6 +550,9 @@ dsa1025.pem:
dsa_private_1025.pem:
openssl gendsa -out dsa_private_1025.pem dsa1025.pem

dsa_private_encrypted_1025.pem:
openssl pkcs8 -in dsa_private_1025.pem -topk8 -passout 'pass:secret' -out dsa_private_encrypted_1025.pem

dsa_public_1025.pem:
openssl dsa -in dsa_private_1025.pem -pubout -out dsa_public_1025.pem

Expand Down
Loading

0 comments on commit 36de9dc

Please sign in to comment.