Skip to content

Commit

Permalink
Add pk_encrypt/pk_decrypt commands
Browse files Browse the repository at this point in the history
  • Loading branch information
randombit committed Jul 4, 2018
1 parent 0e8e8f1 commit a2ce8a8
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 2 deletions.
8 changes: 8 additions & 0 deletions doc/manual/cli.rst
Expand Up @@ -91,6 +91,14 @@ Public Key Cryptography
Print raw elliptic curve domain parameters of the standarized curve *name*. If
*pem* is set, the encoded domain is printed.

``pk_encrypt --aead=AES-256/GCM rsa_pubkey datafile``
Encrypts ``datafile`` using the specified AEAD algorithm, under a key protected
by the specified RSA public key.

``pk_decrypt rsa_privkey datafile``
Decrypts a file encrypted with ``pk_encrypt``. If the key is encrypted using a
password, it will be prompted for on the terminal.

X.509
----------------------------------------------

Expand Down
38 changes: 36 additions & 2 deletions src/cli/cli.cpp
Expand Up @@ -11,6 +11,10 @@
#include <iostream>
#include <fstream>

#if defined(BOTAN_TARGET_OS_HAS_POSIX1)
#include <termios.h>
#endif

namespace Botan_CLI {

Command::Command(const std::string& cmd_spec) : m_spec(cmd_spec)
Expand Down Expand Up @@ -182,8 +186,8 @@ void Command::read_file(const std::string& input_file,
}

void Command::do_read_file(std::istream& in,
std::function<void (uint8_t[], size_t)> consumer_fn,
size_t buf_size) const
std::function<void (uint8_t[], size_t)> consumer_fn,
size_t buf_size) const
{
// Avoid an infinite loop on --buf-size=0
std::vector<uint8_t> buf(buf_size == 0 ? 4096 : buf_size);
Expand All @@ -206,6 +210,36 @@ Botan::RandomNumberGenerator& Command::rng()
return *m_rng.get();
}

std::string Command::get_passphrase(const std::string& prompt)
{
error_output() << prompt << ": " << std::flush;
std::string pass;

#if defined(BOTAN_TARGET_OS_HAS_POSIX1)

struct termios old_flags;
::tcgetattr(fileno(stdin), &old_flags);
struct termios noecho_flags = old_flags;
noecho_flags.c_lflag &= ~ECHO;
noecho_flags.c_lflag |= ECHONL;

if(::tcsetattr(fileno(stdin), TCSANOW, &noecho_flags) != 0)
throw CLI_Error("Clearing terminal echo bit failed");

std::getline(std::cin, pass);

if(::tcsetattr(::fileno(stdin), TCSANOW, &old_flags) != 0)
throw CLI_Error("Restoring terminal echo bit failed");
#else

// TODO equivalent for Windows ...
std::getline(std::cin, pass);

#endif

return pass;
}

// Registration code

Command::Registration::Registration(const std::string& name, Command::cmd_maker_fn maker_fn)
Expand Down
2 changes: 2 additions & 0 deletions src/cli/cli.h
Expand Up @@ -118,6 +118,8 @@ class Command
return flag_set("verbose");
}

std::string get_passphrase(const std::string& prompt);

bool flag_set(const std::string& flag_name) const;

std::string get_arg(const std::string& opt_name) const;
Expand Down
212 changes: 212 additions & 0 deletions src/cli/pk_crypt.cpp
@@ -0,0 +1,212 @@
/*
* (C) 2018 Jack Lloyd
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#include "cli.h"

#if defined(BOTAN_HAS_RSA) && defined(BOTAN_HAS_AEAD_MODES) && defined(BOTAN_HAS_EME_OAEP) && defined(BOTAN_HAS_SHA2_32) && defined(BOTAN_HAS_PEM_CODEC)

#include <botan/pubkey.h>
#include <botan/x509_key.h>
#include <botan/pkcs8.h>
#include <botan/der_enc.h>
#include <botan/ber_dec.h>
#include <botan/oids.h>
#include <botan/aead.h>
#include <botan/pem.h>

namespace Botan_CLI {

namespace {

class PK_Encrypt final : public Command
{
public:
PK_Encrypt() : Command("pk_encrypt --aead=AES-256/GCM pubkey datafile") {}

std::string group() const override
{
return "pubkey";
}

std::string description() const override
{
return "Encrypt a file using a RSA public key";
}

void go() override
{
std::unique_ptr<Botan::Public_Key> key(Botan::X509::load_key(get_arg("pubkey")));
if(!key)
{
throw CLI_Error("Unable to load public key");
}

if(key->algo_name() != "RSA")
{
throw CLI_Usage_Error("This function requires an RSA key");
}

const std::string OAEP_HASH = "SHA-256";
const std::string aead_algo = get_arg("aead");

std::unique_ptr<Botan::AEAD_Mode> aead =
Botan::AEAD_Mode::create(aead_algo, Botan::ENCRYPTION);

if(!aead)
throw CLI_Usage_Error("The AEAD '" + aead_algo + "' is not available");

const Botan::OID aead_oid = Botan::OIDS::lookup(aead_algo);
if(aead_oid.empty())
throw CLI_Usage_Error("No OID defined for AEAD '" + aead_algo + "'");

Botan::secure_vector<uint8_t> data;
auto insert_fn = [&](const uint8_t b[], size_t l)
{
data.insert(data.end(), b, b + l);
};
this->read_file(get_arg("datafile"), insert_fn);

const Botan::AlgorithmIdentifier hash_id(OAEP_HASH, Botan::AlgorithmIdentifier::USE_NULL_PARAM);
const Botan::AlgorithmIdentifier pk_alg_id("RSA/OAEP", hash_id.BER_encode());

Botan::PK_Encryptor_EME enc(*key, rng(), "OAEP(" + OAEP_HASH + ")");

const Botan::secure_vector<uint8_t> file_key = rng().random_vec(aead->key_spec().maximum_keylength());

const std::vector<uint8_t> encrypted_key = enc.encrypt(file_key, rng());

const Botan::secure_vector<uint8_t> nonce = rng().random_vec(aead->default_nonce_length());
aead->set_key(file_key);
aead->set_associated_data_vec(encrypted_key);
aead->start(nonce);

aead->finish(data);

std::vector<uint8_t> buf;
Botan::DER_Encoder der(buf);

der.start_cons(Botan::SEQUENCE)
.encode(pk_alg_id)
.encode(encrypted_key, Botan::OCTET_STRING)
.encode(aead_oid)
.encode(nonce, Botan::OCTET_STRING)
.encode(data, Botan::OCTET_STRING)
.end_cons();

output() << Botan::PEM_Code::encode(buf, "PUBKEY ENCRYPTED MESSAGE", 72);
}
};

BOTAN_REGISTER_COMMAND("pk_encrypt", PK_Encrypt);

class PK_Decrypt final : public Command
{
public:
PK_Decrypt() : Command("pk_decrypt privkey datafile") {}

std::string group() const override
{
return "pubkey";
}

std::string description() const override
{
return "Decrypt a file using a RSA private key";
}

void go() override
{
Botan::DataSource_Stream input_stream(get_arg("privkey"));
auto get_pass = [this]() { return get_passphrase("Password"); };
std::unique_ptr<Botan::Private_Key> key = Botan::PKCS8::load_key(input_stream, get_pass);

if(!key)
{
throw CLI_Error("Unable to load public key");
}

if(key->algo_name() != "RSA")
{
throw CLI_Usage_Error("This function requires an RSA key");
}

Botan::secure_vector<uint8_t> data;
std::vector<uint8_t> encrypted_key;
std::vector<uint8_t> nonce;
Botan::AlgorithmIdentifier pk_alg_id;
Botan::OID aead_oid;

try
{
Botan::DataSource_Stream input(get_arg("datafile"));

Botan::BER_Decoder(Botan::PEM_Code::decode_check_label(input, "PUBKEY ENCRYPTED MESSAGE"))
.start_cons(Botan::SEQUENCE)
.decode(pk_alg_id)
.decode(encrypted_key, Botan::OCTET_STRING)
.decode(aead_oid)
.decode(nonce, Botan::OCTET_STRING)
.decode(data, Botan::OCTET_STRING)
.end_cons();
}
catch(Botan::Decoding_Error&)
{
output() << "Parsing input file failed: invalid format?\n";
set_return_code(1);
return;
}

const std::string aead_algo = Botan::OIDS::lookup(aead_oid);
if(aead_algo == "")
{
output() << "Ciphertext was encrypted with an unknown algorithm";
set_return_code(1);
return;
}

if(pk_alg_id.get_oid() != Botan::OIDS::lookup("RSA/OAEP"))
{
output() << "Ciphertext was encrypted with something other than RSA/OAEP";
set_return_code(1);
return;
}

Botan::AlgorithmIdentifier oaep_hash;
Botan::BER_Decoder(pk_alg_id.get_parameters()).decode(oaep_hash);

Botan::PK_Decryptor_EME dec(*key, rng(), "OAEP(" + Botan::OIDS::lookup(oaep_hash.get_oid()) + ")");

const Botan::secure_vector<uint8_t> file_key = dec.decrypt(encrypted_key);

std::unique_ptr<Botan::AEAD_Mode> aead =
Botan::AEAD_Mode::create_or_throw(aead_algo, Botan::DECRYPTION);

aead->set_key(file_key);
aead->set_associated_data_vec(encrypted_key);
aead->start(nonce);

try
{
aead->finish(data);

output().write(reinterpret_cast<const char*>(data.data()), data.size());
}
catch(Botan::Integrity_Failure&)
{
output() << "Message authentication failure, possible ciphertext tampering\n";
set_return_code(1);
return;
}
}
};

BOTAN_REGISTER_COMMAND("pk_decrypt", PK_Decrypt);

}

}

#endif
29 changes: 29 additions & 0 deletions src/scripts/test_cli.py
Expand Up @@ -423,6 +423,34 @@ def cli_tls_socket_tests():

tls_server.communicate()

def cli_pk_encrypt_tests():
tmp_dir = tempfile.mkdtemp(prefix='botan_cli')

input_file = os.path.join(tmp_dir, 'input')
ctext_file = os.path.join(tmp_dir, 'ctext')
recovered_file = os.path.join(tmp_dir, 'recovered')
rsa_priv_key = os.path.join(tmp_dir, 'rsa.priv')
rsa_pub_key = os.path.join(tmp_dir, 'rsa.pub')

test_cli("keygen", ["--algo=RSA", "--params=2048", "--output=%s" % (rsa_priv_key)], "")
test_cli("pkcs8", ["--pub-out", "%s/rsa.priv" % (tmp_dir), "--output=%s" % (rsa_pub_key)], "")

# Generate a random input file
test_cli("rng", ["10", "16", "32", "--output=%s" % (input_file)], "")

# Because we used a fixed DRBG for each invocation the same ctext is generated each time
rng_output_hash = "32F5E7B61357DE8397EFDA1E598379DFD5EE21767BDF4E2A435F05117B836AC6"
ctext_hash = "DBF227237924EF2B2171B5B9A1C52C152C388E407CF3D32122A4984D471F8F77"

test_cli("hash", ["--no-fsname", "--algo=SHA-256", input_file], rng_output_hash)

# Encrypt and verify ciphertext is the expected value
test_cli("pk_encrypt", [rsa_pub_key, input_file, "--output=%s" % (ctext_file)], "")
test_cli("hash", ["--no-fsname", "--algo=SHA-256", ctext_file], ctext_hash)

# Decrypt and verify plaintext is recovered
test_cli("pk_decrypt", [rsa_priv_key, ctext_file, "--output=%s" % (recovered_file)], "")
test_cli("hash", ["--no-fsname", "--algo=SHA-256", recovered_file], rng_output_hash)

def cli_speed_tests():
# pylint: disable=too-many-branches
Expand Down Expand Up @@ -557,6 +585,7 @@ def main(args=None):
cli_timing_test_tests()
cli_asn1_tests()
cli_speed_tests()
cli_pk_encrypt_tests()
cli_tls_ciphersuite_tests()
cli_tls_socket_tests()
end_time = time.time()
Expand Down

0 comments on commit a2ce8a8

Please sign in to comment.