diff options
Diffstat (limited to 'src/keyring.cpp')
| -rw-r--r-- | src/keyring.cpp | 318 |
1 files changed, 245 insertions, 73 deletions
diff --git a/src/keyring.cpp b/src/keyring.cpp index eb71e53..324aec2 100644 --- a/src/keyring.cpp +++ b/src/keyring.cpp @@ -16,6 +16,10 @@ #include "file_functions.h" #include "keyring.h" +extern "C" { +#include "tpm/tpm.h" +} + // Encrypted secret key serialized format version. // // Version 1: Secret key encrypted with ChaCha20-Poly1305 using password-based @@ -45,6 +49,10 @@ static constexpr quint8 all_zero_nonce[crypto_aead_chacha20poly1305_NPUBBYTES] = // authentication mechanism. static const auto domain_sep = QByteArrayLiteral("Einsof-Auth-DHCR"); +// Internal state, set by tpm2_init if TPM check and self-test succeed, and we +// can use hardware keys. +static int tpm_available = 0; + static QString get_keyring_path() { return QDir(get_app_path()).filePath("keyring.cbor"); @@ -67,10 +75,73 @@ int keyring_initialize() index.close(); } + if (tpm2_init() == STKB_SUCCESS) + { + tpm_available = 1; + } + + return 0; +} + +// Expose a way to query status without making the internal state variable +// global. +// Yes, this is stupid. +int keyring_hardware_available() +{ + return tpm_available; +} + +// Prepare the key CBOR entry and atomically insert it into the keyring. +// To ensure durability, file locking and atomic overwrite are used as +// provided by Qt. Frankly, I don't see the keyring being prone to corruption +// as there's no concurrent access, save for a contrived edge case of a user +// generating/deleting keys simultaneously from two client instances. It's +// done regardless for a good measure. +static int insert_into_keyring(quint8 tag, QStringView name, const QByteArray &pub, const QByteArray &sec) +{ + QCborMap entry; + entry.insert(0, QCborValue(tag)); + entry.insert(1, QCborValue(name)); + entry.insert(2, QCborValue(pub)); + entry.insert(3, QCborValue(sec)); + QFile index_file(get_keyring_path()); + QLockFile index_lock(QDir(get_app_path()).filePath("keyring.lock")); + if (!index_lock.lock()) + { + return 1; + } + if (!index_file.open(QIODevice::ReadOnly)) + { + return 1; + } + QCborStreamReader idx_in(&index_file); + QCborValue index = QCborValue::fromCbor(idx_in); + index_file.close(); + if (!index.isMap()) + { + index = QCborMap(); + } + QCborMap index_map = index.toMap(); + // Key fingerprint is used as its unique identifier. + QByteArray fingerprint; + fingerprint.append(tag); + fingerprint.append(pub); + uchar fpr_hash[crypto_generichash_BYTES]; + crypto_generichash(fpr_hash, sizeof(fpr_hash), (const uchar *)fingerprint.constData(), fingerprint.size(), nullptr, 0); + index_map.insert(QCborValue(QByteArray((const char *)fpr_hash, (qsizetype)sizeof(fpr_hash))), QCborValue(entry)); + QSaveFile index_file_w(get_keyring_path()); + if (!index_file_w.open(QIODevice::WriteOnly)) + { + return 1; + } + index_file_w.write(QCborValue(index_map).toCbor()); + index_file_w.commit(); + index_lock.unlock(); return 0; } -int generate_key(QStringView name, const QByteArray &password) +// Software X25519 keys protected by a passphrase. +int generate_software_key(QStringView name, const QByteArray &password) { // If we let users specify arbitrary parameters, additional constraints must // be enforced: min/max opslimit, type of memlimit (uint32_t vs size_t). @@ -127,94 +198,73 @@ int generate_key(QStringView name, const QByteArray &password) } sodium_munlock(wrap_key, sizeof(wrap_key)); sodium_munlock(secret_key, sizeof(secret_key)); - QByteArray public_key_array((const char *)public_key, (qsizetype)sizeof(public_key)); - quint8 tag = cert_x25519_tag; - - // Key fingerprint is used as its unique identifier. - QByteArray fingerprint; - fingerprint.append(tag); - fingerprint.append(public_key_array); - uchar fpr_hash[crypto_generichash_BYTES]; - crypto_generichash(fpr_hash, sizeof(fpr_hash), (const uchar *)fingerprint.constData(), fingerprint.size(), nullptr, 0); + QByteArray public_key_array((const char *)public_key, (qsizetype)sizeof(public_key)); QByteArray encrypted_secret(packed_header); encrypted_secret.append((const char *)payload, sizeof(payload)); - // Prepare the key CBOR entry and atomically insert it into the keyring. - // To ensure durability, file locking and atomic overwrite are used as - // provided by Qt. Frankly, I don't see the keyring being prone to corruption - // as there's no concurrent access, save for a contrived edge case of a user - // generating/deleting keys simultaneously from two client instances. It's - // done regardless for a good measure. - QCborMap entry; - entry.insert(0, QCborValue(tag)); - entry.insert(1, QCborValue(name)); - entry.insert(2, QCborValue(public_key_array)); - entry.insert(3, QCborValue(encrypted_secret)); - QFile index_file(get_keyring_path()); - QLockFile index_lock(QDir(get_app_path()).filePath("keyring.lock")); - if (!index_lock.lock()) + if (insert_into_keyring(cert_x25519_tag, name, public_key_array, encrypted_secret)) { - return 9; + return 5; } - if (!index_file.open(QIODevice::ReadOnly)) - { - return 6; - } - QCborStreamReader idx_in(&index_file); - QCborValue index = QCborValue::fromCbor(idx_in); - index_file.close(); - if (!index.isMap()) - { - index = QCborMap(); - } - QCborMap index_map = index.toMap(); - index_map.insert(QCborValue(QByteArray((const char *)fpr_hash, (qsizetype)sizeof(fpr_hash))), QCborValue(entry)); - QSaveFile index_file_w(get_keyring_path()); - if (!index_file_w.open(QIODevice::WriteOnly)) - { - return 8; - } - index_file_w.write(QCborValue(index_map).toCbor()); - index_file_w.commit(); - index_lock.unlock(); return 0; } -// Look up the key_id in the keyring, unlock it with supplied password using -// parameters from its header, perform a Diffie-Hellman key exchange with the -// server's ephemeral key to derive a shared secret, and hash it along with -// additional data to prove your authenticity to the server. -ResponseResult unlock_and_auth(QByteArrayView key_id, QByteArrayView password, QByteArrayView ephemeral_key, QByteArrayView username, AuthResponse &out) +static constexpr quint8 cert_hwkey_p256_tag = 0x33; + +static std::wstring optional_hardware_key_name(QStringView name) { - if (password.size() < crypto_pwhash_PASSWD_MIN || password.size() > crypto_pwhash_PASSWD_MAX) - { - return ResponseResult::invalid_password; - } +#ifdef SOF_AO_HARDWARE_KEY_REQUIRES_PIN_UI + (void)name; + return std::wstring(); +#else + return name.toString().toStdWString(); +#endif +} - QFile index_file(get_keyring_path()); - if (!index_file.open(QIODevice::ReadOnly)) - { - return ResponseResult::inaccessible_keyring; +// Hardware P-256 keys (because most TPMs can only do P-256, unfortunately); +// the TPM verifies the PIN. +int generate_hardware_key(QStringView name, const QByteArray &pin) +{ + // Compressed P-256 public key. + uint8_t pub[33]; + // Protected secret blobs differ by size depending on the platform (and in + // case of TSS2 implementation, size is unspecified). Prepare a sufficient + // buffer to write to, the actual output size will be in `sec_written`. + uint8_t sec[512]; + size_t sec_written = 0; + std::wstring optional_name_wstr = optional_hardware_key_name(name); + stkb_user_input input = { + .pin = (const uint8_t *)pin.constData(), + .pin_len = (size_t)pin.size(), + .key_name = optional_name_wstr.c_str(), + }; + stkb_rc rc = tpm2_keygen(pub, sec, sizeof(sec), &sec_written, input); + switch (rc) { + case STKB_SUCCESS: + break; + default: + return rc; } - QCborStreamReader idx_in(&index_file); - QCborValue index = QCborValue::fromCbor(idx_in); - index_file.close(); - QCborMap tmp_map = index.toMap().value(QCborValue(QByteArray(key_id))).toMap(); - QCborValue val = tmp_map.value(3); - if (!val.isByteArray()) + QByteArray public_array((const char *)pub, (qsizetype)sizeof(pub)); + QByteArray protected_array((const char *)sec, (qsizetype)sec_written); + if (insert_into_keyring(cert_hwkey_p256_tag, name, public_array, protected_array)) + return 1; + return 0; +} + +static ResponseResult compute_with_software_key(quint8 *shared_secret, const QByteArray &encrypted_secret, QByteArrayView password, QByteArrayView ephemeral_key) +{ + if (password.size() < crypto_pwhash_PASSWD_MIN || password.size() > crypto_pwhash_PASSWD_MAX) { - return ResponseResult::corrupted_entry; + return ResponseResult::invalid_password; } - QByteArray encrypted_secret = val.toByteArray(); - val = tmp_map.value(2); - if (!val.isByteArray()) + if (ephemeral_key.size() != crypto_kx_PUBLICKEYBYTES) { - return ResponseResult::corrupted_entry; + return ResponseResult::incompatible_arguments; } - QByteArray public_key = val.toByteArray(); // After retrieving the key from the keyring, the process is the same as // in key generation, but in the opposite direction. @@ -264,7 +314,6 @@ ResponseResult unlock_and_auth(QByteArrayView key_id, QByteArrayView password, Q // Now we've unlocked the key, and ready to perform DH with the server's // ephemeral key to prove ourselves. - quint8 shared_secret[crypto_scalarmult_BYTES]; if (crypto_scalarmult(shared_secret, secret_key, (const uchar *)ephemeral_key.constData())) { // We ended up with the point at infinity, something stupid must've @@ -274,6 +323,126 @@ ResponseResult unlock_and_auth(QByteArrayView key_id, QByteArrayView password, Q } sodium_munlock(secret_key, sizeof(secret_key)); + return ResponseResult::success; +} + +static ResponseResult compute_with_hardware_key(quint8 *shared_secret, const QByteArray &blob, QByteArrayView pin, QByteArrayView ephemeral_key) +{ + if (ephemeral_key.size() != 64) + { + return ResponseResult::incompatible_arguments; + } + stkb_user_input input = { + .pin = (const uint8_t *)pin.constData(), + .pin_len = (size_t)pin.size(), + .key_name = nullptr, + }; + stkb_rc rc = tpm2_compute_ss(shared_secret, (const uint8_t *)blob.constData(), (size_t)blob.size(), (const uint8_t *)ephemeral_key.constData(), input); + switch (rc) { + case STKB_SUCCESS: + return ResponseResult::success; + case STKB_ERROR: + return ResponseResult::hardware_fault; + case STKB_BAD_PIN: + case STKB_AUTH_FAILURE: + return ResponseResult::invalid_pin; + case STKB_LOCKOUT: + return ResponseResult::hardware_lockout; + case STKB_TPM_UNAVAILABLE: + default: + return ResponseResult::inaccessible_keyring; + } +} + +KeyringKey acquire_keyring_key(QByteArrayView key_id) +{ + struct KeyringKey key; + QFile index_file(get_keyring_path()); + if (!index_file.open(QIODevice::ReadOnly)) + { + return key; + } + QCborStreamReader idx_in(&index_file); + QCborValue index = QCborValue::fromCbor(idx_in); + index_file.close(); + QCborMap tmp_map = index.toMap().value(QCborValue(QByteArray(key_id))).toMap(); + + QCborValue val = tmp_map.value(2); + if (!val.isByteArray()) + { + return key; + } + key.public_key = val.toByteArray(); + + val = tmp_map.value(3); + if (!val.isByteArray()) + { + return key; + } + key.protected_secret = val.toByteArray(); + + val = tmp_map.value(0); + if (!val.isInteger()) + { + return key; + } + qint64 raw_tag = val.toInteger(); + if (raw_tag < 0 || raw_tag > 255) + { + return key; + } + quint8 tag = (quint8)raw_tag; + switch (tag) { + case cert_x25519_tag: + key.type = KeyringStorageType::software; + break; + case cert_hwkey_p256_tag: + key.type = KeyringStorageType::hardware; + break; + default: + break; + } + + val = tmp_map.value(1); + if (!val.isString()) + { + return key; + } + key.name = val.toString(); + + + key.id = QByteArray(key_id); + + return key; +} + +// Look up the key_id in the keyring, unlock it with supplied password using +// parameters from its header, perform a Diffie-Hellman key exchange with the +// server's ephemeral key to derive a shared secret, and hash it along with +// additional data to prove your authenticity to the server. +ResponseResult unlock_and_auth(const KeyringKey &key, QByteArrayView input, QByteArrayView ephemeral_key, QByteArrayView username, AuthResponse &out) +{ + uint8_t shared_secret[32]; + sodium_mlock(shared_secret, sizeof(shared_secret)); + + ResponseResult result; + switch (key.type) { + case KeyringStorageType::software: + result = compute_with_software_key(shared_secret, key.protected_secret, input, ephemeral_key); + break; + case KeyringStorageType::hardware: + result = compute_with_hardware_key(shared_secret, key.protected_secret, input, ephemeral_key); + break; + default: + result = ResponseResult::corrupted_entry; + break; + } + if (result != ResponseResult::success) + { + sodium_munlock(shared_secret, sizeof(shared_secret)); + return result; + } + // The proof is a BLAKE2b-256 hash over the following: // 1. Domain-separating constant. // 2. Shared DH secret, serving as a proof of posession of the secret key @@ -297,11 +466,14 @@ ResponseResult unlock_and_auth(QByteArrayView key_id, QByteArrayView password, Q crypto_generichash_update(&state, (const uchar *)domain_sep.constData(), domain_sep.size()); crypto_generichash_update(&state, shared_secret, sizeof(shared_secret)); crypto_generichash_update(&state, (const uchar *)ephemeral_key.constData(), ephemeral_key.size()); - crypto_generichash_update(&state, (const uchar *)public_key.constData(), public_key.size()); + crypto_generichash_update(&state, (const uchar *)key.public_key.constData(), key.public_key.size()); crypto_generichash_update(&state, (const uchar *)username.constData(), username.size()); crypto_generichash_final(&state, proof, sizeof(proof)); out.response = QByteArray((const char *)proof, (qsizetype)sizeof(proof)); + + sodium_munlock(shared_secret, sizeof(shared_secret)); + return ResponseResult::success; } |
