diff --git a/.gitignore b/.gitignore index 52afeba1..f13ebfd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /build*/ /compile_commands.json /.cache/ +/.vscode/ \ No newline at end of file diff --git a/include/session/blinding.h b/include/session/blinding.h new file mode 100644 index 00000000..715ec8d9 --- /dev/null +++ b/include/session/blinding.h @@ -0,0 +1,116 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_blind15_key_pair +/// +/// This function attempts to generate a blind15 key pair. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `server_pk` -- [in] the public key of the open group server to generate the +/// blinded id for (32 bytes). +/// - `blinded_pk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_pk will +/// be written if generation was successful. +/// - `blinded_sk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_sk will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the key was successfully generated, false if generation failed. +LIBSESSION_EXPORT bool session_blind15_key_pair( + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + unsigned char* blinded_pk_out, /* 32 byte output buffer */ + unsigned char* blinded_sk_out /* 32 byte output buffer */); + +/// API: crypto/session_blind25_key_pair +/// +/// This function attempts to generate a blind25 key pair. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `server_pk` -- [in] the public key of the open group server to generate the +/// blinded id for (32 bytes). +/// - `blinded_pk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_pk will +/// be written if generation was successful. +/// - `blinded_sk_out` -- [out] pointer to a buffer of at least 32 bytes where the blinded_sk will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the key was successfully generated, false if generation failed. +LIBSESSION_EXPORT bool session_blind25_key_pair( + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + unsigned char* blinded_pk_out, /* 32 byte output buffer */ + unsigned char* blinded_sk_out /* 32 byte output buffer */); + +/// API: crypto/session_blind15_sign +/// +/// This function attempts to generate a signature for a message using a blind15 private key. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `server_pk` -- [in] the public key of the open group server to generate the +/// blinded id for (32 bytes). +/// - `msg` -- [in] Pointer to a data buffer containing the message to generate a signature for. +/// - `msg_len` -- [in] Length of `msg` +/// - `blinded_sig_out` -- [out] pointer to a buffer of at least 64 bytes where the signature will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the signature was successfully generated, false if generation failed. +LIBSESSION_EXPORT bool session_blind15_sign( + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out /* 64 byte output buffer */); + +/// API: crypto/session_blind25_sign +/// +/// This function attempts to generate a signature for a message using a blind25 private key. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `server_pk` -- [in] the public key of the open group server to generate the +/// blinded id for (32 bytes). +/// - `msg` -- [in] Pointer to a data buffer containing the message to generate a signature for. +/// - `msg_len` -- [in] Length of `msg` +/// - `blinded_sig_out` -- [out] pointer to a buffer of at least 64 bytes where the signature will +/// be written if generation was successful. +/// +/// Outputs: +/// - `bool` -- True if the signature was successfully generated, false if generation failed. +LIBSESSION_EXPORT bool session_blind25_sign( + const unsigned char* ed25519_seckey, /* 64 bytes */ + const unsigned char* server_pk, /* 32 bytes */ + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out /* 64 byte output buffer */); + +/// API: crypto/session_blind25_sign +/// +/// This function attempts to generate a signature for a message using a blind25 private key. +/// +/// Inputs: +/// - `session_id` -- [in] the session_id to compare (66 bytes with a 05 prefix). +/// - `blinded_id` -- [in] the blinded_id to compare, can be either 15 or 25 blinded (66 bytes). +/// - `server_pk` -- [in] the public key of the open group server to the blinded id came from (64 +/// bytes). +/// +/// Outputs: +/// - `bool` -- True if the session_id matches the blinded_id, false if not. +LIBSESSION_EXPORT bool session_id_matches_blinded_id( + const char* session_id, /* 66 bytes */ + const char* blinded_id, /* 66 bytes */ + const char* server_pk /* 64 bytes */); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/blinding.hpp b/include/session/blinding.hpp index 164621d5..c6e41cf5 100644 --- a/include/session/blinding.hpp +++ b/include/session/blinding.hpp @@ -55,10 +55,25 @@ namespace session { /// /// This (R, S) signature is then Ed25519-verifiable using pubkey kA. +/// Returns the blinding factor for 15 blinding. Typically this isn't used directly, but is +/// exposed for debugging/testing. Takes server pk in bytes, not hex. +uc32 blind15_factor(ustring_view server_pk); + /// Returns the blinding factor for 25 blinding. Typically this isn't used directly, but is -/// exposed for debugging/testing. Takes session id and pk in bytes, not hex. session id can -/// be 05-prefixed (33 bytes) or unprefixed (32 bytes). -std::array blind25_factor(ustring_view session_id, ustring_view server_pk); +/// exposed for debugging/testing. Takes session id and server pk in bytes, not hex. session +/// id can be 05-prefixed (33 bytes) or unprefixed (32 bytes). +uc32 blind25_factor(ustring_view session_id, ustring_view server_pk); + +/// Computes the two possible 15-blinded ids from a session id and server pubkey. Values accepted +/// and returned are hex-encoded. +std::array blind15_id(std::string_view session_id, std::string_view server_pk); + +/// Similar to the above, but takes the session id and pubkey as byte values instead of hex, and +/// returns a single 33-byte value (instead of a 66-digit hex value). Unlike the string version, +/// session_id here may be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). Only +/// the *positive* possible ID is returned: the alternative can be computed by flipping the highest +/// bit of byte 32, i.e.: `result[32] ^= 0x80`. +ustring blind15_id(ustring_view session_id, ustring_view server_pk); /// Computes the 25-blinded id from a session id and server pubkey. Values accepted and /// returned are hex-encoded. @@ -69,6 +84,70 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk); /// be passed unprefixed (i.e. 32 bytes instead of 33 with the 05 prefix). ustring blind25_id(ustring_view session_id, ustring_view server_pk); +/// Computes the 15-blinded id from a 32-byte Ed25519 pubkey, i.e. from the known underlying Ed25519 +/// pubkey behind a (X25519) Session ID. Unlike blind15_id, knowing the true Ed25519 pubkey allows +/// thie method to compute the correct sign and so using this does not require considering that the +/// resulting blinded ID might need to have a sign flipped. +/// +/// If the `session_id` is a non-null pointer then it must point at an empty string to be populated +/// with the session_id associated with `ed_pubkey`. This is here for consistency with +/// `blinded25_id_from_ed`, but unlike the 25 version, this value is not read if non-empty, and is +/// not an optimization (that is: it is purely for convenience and is no more efficient to use this +/// than it is to compute it yourself). +ustring blinded15_id_from_ed( + ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); + +/// Computes the 25-blinded id from a 32-byte Ed25519 pubkey, i.e. from the known underlying Ed25519 +/// pubkey behind a (X25519) Session ID. This will be the same as blind25_id (if given the X25519 +/// pubkey that the Ed25519 converts to), but is more efficient when the Ed25519 pubkey is already +/// known. +/// +/// The session_id argument is provided to optimize input or output of the session ID derived from +/// the Ed25519 pubkey: if already computed, this argument can be a pointer to a 33-byte string +/// containing the precomputed value (to avoid needing to compute it again). If unknown but needed +/// then a pointer to an empty string can be given to computed and stored the value here. Otherwise +/// (if omitted or nullptr) then the value will temporarily computed within the function. +ustring blinded25_id_from_ed( + ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id = nullptr); + +/// Computes a 15-blinded key pair. +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 +/// digits) or bytes (32 bytes)). Returns the blinded public key and private key (NOT a seed). +/// +/// Can optionally also return the blinding factor, k, by providing a pointer to a uc32 (or +/// cleared_uc32); if non-nullptr then k will be written to it. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +std::pair blind15_key_pair( + ustring_view ed25519_sk, ustring_view server_pk, uc32* k = nullptr); + +/// Computes a 25-blinded key pair. +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 +/// digits) or bytes (32 bytes)). Returns the blinded public key and private key (NOT a seed). +/// +/// Can optionally also return the blinding factor, k', by providing a pointer to a uc32 (or +/// cleared_uc32); if non-nullptr then k' will be written to it, where k' = ±k. Here, `k'` can be +/// negative to cancel out a negative in the true pubkey, which the remote client will always assume +/// is not present when it does a Session ID -> Ed25519 conversion for blinding purposes. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +std::pair blind25_key_pair( + ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime = nullptr); + +/// Computes a verifiable 15-blinded signature that validates with the blinded pubkey that would +/// be returned from blind15_key_pair(). +/// +/// Takes the Ed25519 secret key (64 bytes, or 32-byte seed) and the server pubkey (in hex (64 +/// digits) or bytes (32 bytes)). Returns the 64-byte signature. +/// +/// It is recommended to pass the full 64-byte libsodium-style secret key for `ed25519_sk` (i.e. +/// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. +ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message); + /// Computes a verifiable 25-blinded signature that validates with the blinded pubkey that would /// be returned from blind25_id(). /// @@ -79,4 +158,13 @@ ustring blind25_id(ustring_view session_id, ustring_view server_pk); /// seed + appended pubkey) as with just the 32-byte seed the public key has to be recomputed. ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk, ustring_view message); +/// Takes in a standard session_id and returns a flag indicating whether it matches the given +/// blinded_id for a given server_pk. +/// +/// Takes either a 15 or 25 blinded_id (66 bytes) and the server pubkey (64 bytes). +/// +/// Returns a flag indicating whether the session_id matches the blinded_id. +bool session_id_matches_blinded_id( + std::string_view session_id, std::string_view blinded_id, std::string_view server_pk); + } // namespace session diff --git a/include/session/curve25519.h b/include/session/curve25519.h new file mode 100644 index 00000000..7d77b175 --- /dev/null +++ b/include/session/curve25519.h @@ -0,0 +1,61 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_curve25519_key_pair +/// +/// Generates a random curve25519 key pair. +/// +/// Inputs: +/// - `curve25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be +/// written. +/// - `curve25519_sk_out` -- [out] pointer to a buffer of 32 bytes where the private key will be +/// written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_curve25519_key_pair( + unsigned char* curve25519_pk_out, /* 32 byte output buffer */ + unsigned char* curve25519_sk_out /* 32 byte output buffer */); + +/// API: crypto/session_to_curve25519_pubkey +/// +/// Generates a curve25519 public key for an ed25519 public key. +/// +/// Inputs: +/// - `ed25519_pubkey` -- the ed25519 public key (32 bytes). +/// - `curve25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be +/// written. +/// +/// Outputs: +/// - `bool` -- True if the public key was successfully generated, false if failed. +LIBSESSION_EXPORT bool session_to_curve25519_pubkey( + const unsigned char* ed25519_pubkey, /* 32 bytes */ + unsigned char* curve25519_pk_out /* 32 byte output buffer */); + +/// API: crypto/session_to_curve25519_seckey +/// +/// Generates a curve25519 secret key given given either a libsodium-style secret key, 64 +/// bytes. Can also be passed as a 32-byte seed. +/// +/// Inputs: +/// - `ed25519_seckey` -- [in] the libsodium-style secret key, 64 bytes. Can also be +/// passed as a 32-byte seed. +/// - `curve25519_sk_out` -- [out] pointer to a buffer of 32 bytes where the secret key will be +/// written. +/// +/// Outputs: +/// - `bool` -- True if the secret key was successfully generated, false if failed. +LIBSESSION_EXPORT bool session_to_curve25519_seckey( + const unsigned char* ed25519_seckey, /* 64 bytes */ + unsigned char* curve25519_sk_out /* 32 byte output buffer */); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/curve25519.hpp b/include/session/curve25519.hpp new file mode 100644 index 00000000..34a437ad --- /dev/null +++ b/include/session/curve25519.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include "types.hpp" + +namespace session::curve25519 { + +/// Generates a random curve25519 key pair +std::pair, std::array> curve25519_key_pair(); + +/// API: curve25519/to_curve25519_pubkey +/// +/// Generates a curve25519 public key for an ed25519 public key. +/// +/// Inputs: +/// - `ed25519_pubkey` -- the ed25519 public key. +/// +/// Outputs: +/// - The curve25519 public key +std::array to_curve25519_pubkey(ustring_view ed25519_pubkey); + +/// API: curve25519/to_curve25519_seckey +/// +/// Generates a curve25519 secret key given given a libsodium-style secret key, 64 +/// bytes. +/// +/// Inputs: +/// - `ed25519_seckey` -- the libsodium-style secret key, 64 bytes. +/// +/// Outputs: +/// - The curve25519 secret key +std::array to_curve25519_seckey(ustring_view ed25519_seckey); + +} // namespace session::curve25519 diff --git a/include/session/ed25519.h b/include/session/ed25519.h new file mode 100644 index 00000000..82abd521 --- /dev/null +++ b/include/session/ed25519.h @@ -0,0 +1,100 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_ed25519_key_pair +/// +/// Generates a random ed25519 key pair. +/// +/// Inputs: +/// - `ed25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be +/// written. +/// - `ed25519_sk_out` -- [out] pointer to a buffer of 64 bytes where the private key will be +/// written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_ed25519_key_pair( + unsigned char* ed25519_pk_out, /* 32 byte output buffer */ + unsigned char* ed25519_sk_out /* 64 byte output buffer */); + +/// API: crypto/session_ed25519_key_pair_seed +/// +/// Generates a ed25519 key pair for a 32 byte seed. +/// +/// Inputs: +/// - `ed25519_seed` -- [in] the 32 byte seed. +/// - `ed25519_pk_out` -- [out] pointer to a buffer of 32 bytes where the public key will be +/// written. +/// - `ed25519_sk_out` -- [out] pointer to a buffer of 64 bytes where the private key will be +/// written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_ed25519_key_pair_seed( + const unsigned char* ed25519_seed, /* 32 bytes */ + unsigned char* ed25519_pk_out, /* 32 byte output buffer */ + unsigned char* ed25519_sk_out /* 64 byte output buffer */); + +/// API: crypto/session_seed_for_ed_privkey +/// +/// Returns the seed for an ed25519 key pair given either the libsodium-style secret key, 64 +/// bytes. +/// +/// Inputs: +/// - `ed25519_privkey` -- [in] the libsodium-style secret key of the sender, 64 bytes. +/// - `ed25519_seed_out` -- [out] pointer to a buffer of 32 bytes where the seed will be written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_seed_for_ed_privkey( + const unsigned char* ed25519_privkey, /* 64 bytes */ + unsigned char* ed25519_seed_out /* 32 byte output buffer */); + +/// API: crypto/session_ed25519_sign +/// +/// Generates a signature for the message using the libsodium-style ed25519 secret key, 64 bytes. +/// +/// Inputs: +/// - `ed25519_privkey` -- [in] the libsodium-style secret key of the sender, 64 bytes. +/// - `msg` -- [in] the data to generate a signature for. +/// - `msg_len` -- [in] the length of the `msg` data. +/// - `ed25519_sig_out` -- [out] pointer to a buffer of 64 bytes where the signature will be +/// written. +/// +/// Outputs: +/// - `bool` -- True if the seed was successfully retrieved, false if failed. +LIBSESSION_EXPORT bool session_ed25519_sign( + const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* msg, + size_t msg_len, + unsigned char* ed25519_sig_out /* 64 byte output buffer */); + +/// API: crypto/session_ed25519_verify +/// +/// Verify a message and signature for a given pubkey. +/// +/// Inputs: +/// - `sig` -- [in] the signature to verify, 64 bytes. +/// - `pubkey` -- [in] the pubkey for the secret key that was used to generate the signature, 32 +/// bytes. +/// - `msg` -- [in] the data to verify the signature for. +/// - `msg_len` -- [in] the length of the `msg` data. +/// +/// Outputs: +/// - A flag indicating whether the signature is valid +LIBSESSION_EXPORT bool session_ed25519_verify( + const unsigned char* sig, /* 64 bytes */ + const unsigned char* pubkey, + const unsigned char* msg, + size_t msg_len); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/ed25519.hpp b/include/session/ed25519.hpp new file mode 100644 index 00000000..e40144af --- /dev/null +++ b/include/session/ed25519.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include "types.hpp" + +namespace session::ed25519 { + +/// Generates a random Ed25519 key pair +std::pair, std::array> ed25519_key_pair(); + +/// Given an Ed25519 seed this returns the associated Ed25519 key pair +std::pair, std::array> ed25519_key_pair( + ustring_view ed25519_seed); + +/// API: ed25519/seed_for_ed_privkey +/// +/// Returns the seed for an ed25519 key pair given either the libsodium-style secret key, 64 +/// bytes. If a 32-byte value is provided it is assumed to be the seed and the value will just +/// be returned directly. +/// +/// Inputs: +/// - `ed25519_privkey` -- the libsodium-style secret key of the sender, 64 bytes. Can also be +/// passed as a 32-byte seed. +/// +/// Outputs: +/// - The ed25519 seed +std::array seed_for_ed_privkey(ustring_view ed25519_privkey); + +/// API: ed25519/sign +/// +/// Generates a signature for the message using the libsodium-style ed25519 secret key, 64 bytes. +/// +/// Inputs: +/// - `ed25519_privkey` -- the libsodium-style secret key, 64 bytes. +/// - `msg` -- the data to generate a signature for. +/// +/// Outputs: +/// - The ed25519 signature +ustring sign(ustring_view ed25519_privkey, ustring_view msg); + +/// API: ed25519/verify +/// +/// Verify a message and signature for a given pubkey. +/// +/// Inputs: +/// - `sig` -- the signature to verify, 64 bytes. +/// - `pubkey` -- the pubkey for the secret key that was used to generate the signature, 32 bytes. +/// - `msg` -- the data to verify the signature for. +/// +/// Outputs: +/// - A flag indicating whether the signature is valid +bool verify(ustring_view sig, ustring_view pubkey, ustring_view msg); + +} // namespace session::ed25519 diff --git a/include/session/hash.h b/include/session/hash.h new file mode 100644 index 00000000..e45bdd59 --- /dev/null +++ b/include/session/hash.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_hash +/// +/// Wrapper around the crypto_generichash_blake2b function. +/// +/// Inputs: +/// - `size` -- [in] length of the hash to be generated. +/// - `msg_in` -- [in] the message a hash should be generated for. +/// - `msg_len` -- [in] length of `msg_in`. +/// - `key_in` -- [in] an optional key to be used when generating the hash. +/// - `key_len` -- [in] length of `key_in`. +/// - `hash_out` -- [out] pointer to a buffer of at least `size` bytes where the +/// hash will be written. +/// +/// Outputs: +/// - `bool` -- True if the generation was successful, false if generation failed. +LIBSESSION_EXPORT bool session_hash( + size_t size, + const unsigned char* msg_in, + size_t msg_len, + const unsigned char* key_in, + size_t key_len, + unsigned char* hash_out); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/hash.hpp b/include/session/hash.hpp new file mode 100644 index 00000000..231a65aa --- /dev/null +++ b/include/session/hash.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "types.hpp" + +namespace session::hash { + +/// API: hash/hash +/// +/// Wrapper around the crypto_generichash_blake2b function. +/// +/// Inputs: +/// - `size` -- length of the hash to be generated. +/// - `msg` -- the message to generate a hash for. +/// - `key` -- an optional key to be used when generating the hash. +/// +/// Outputs: +/// - a `size` byte hash. +ustring hash(const size_t size, ustring_view msg, std::optional key); + +} // namespace session::hash diff --git a/include/session/onionreq/builder.h b/include/session/onionreq/builder.h new file mode 100644 index 00000000..4cb72a1e --- /dev/null +++ b/include/session/onionreq/builder.h @@ -0,0 +1,176 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "../export.h" + +typedef enum ENCRYPT_TYPE { + ENCRYPT_TYPE_AES_GCM = 0, + ENCRYPT_TYPE_X_CHA_CHA_20 = 1, +} ENCRYPT_TYPE; + +typedef struct onion_request_builder_object { + // Internal opaque object pointer; calling code should leave this alone. + void* internals; + + ENCRYPT_TYPE enc_type; +} onion_request_builder_object; + +/// API: groups/onion_request_builder_init +/// +/// Constructs an onion request builder and sets a pointer to it in `builder`. +/// +/// When done with the object the `builder` must be destroyed by either passing the pointer to +/// onion_request_builder_free() or onion_request_builder_build(). +/// +/// Inputs: +/// - `builder` -- [out] Pointer to the builder object +LIBSESSION_EXPORT void onion_request_builder_init(onion_request_builder_object** builder); + +/// API: onion_request_builder_set_enc_type +/// +/// Wrapper around session::onionreq::Builder::onion_request_builder_set_enc_type. +/// +/// Declaration: +/// ```cpp +/// void onion_request_builder_set_enc_type( +/// [in] onion_request_builder_object* builder +/// [in] ENCRYPT_TYPE enc_type +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `enc_type` -- [in] The encryption type to use in the onion request +LIBSESSION_EXPORT void onion_request_builder_set_enc_type( + onion_request_builder_object* builder, ENCRYPT_TYPE enc_type); + +/// API: onion_request_builder_set_snode_destination +/// +/// Wrapper around session::onionreq::Builder::set_snode_destination. ed25519_pubkey and +/// x25519_pubkey are both hex strings and must both be exactly 64 characters. +/// +/// Declaration: +/// ```cpp +/// void onion_request_builder_set_snode_destination( +/// [in] onion_request_builder_object* builder +/// [in] const char* ed25519_pubkey, +/// [in] const char* x25519_pubkey +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode destination +/// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination +LIBSESSION_EXPORT void onion_request_builder_set_snode_destination( + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey); + +/// API: onion_request_builder_set_server_destination +/// +/// Wrapper around session::onionreq::Builder::set_server_destination. x25519_pubkey +/// is a hex string and must both be exactly 64 characters. +/// +/// Declaration: +/// ```cpp +/// void onion_request_builder_set_server_destination( +/// [in] onion_request_builder_object* builder +/// [in] const char* host, +/// [in] const char* target, +/// [in] const char* protocol, +/// [in] uint16_t port, +/// [in] const char* x25519_pubkey +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `host` -- [in] The host for the server destination +/// - `target` -- [in] The target (endpoint) for the server destination +/// - `protocol` -- [in] The protocol to use for the +/// - `port` -- [in] The host for the server destination +/// - `x25519_pubkey` -- [in] The x25519 public key for the snode destination +LIBSESSION_EXPORT void onion_request_builder_set_server_destination( + onion_request_builder_object* builder, + const char* host, + const char* target, + const char* protocol, + uint16_t port, + const char* x25519_pubkey); + +/// API: onion_request_builder_add_hop +/// +/// Wrapper around session::onionreq::Builder::add_hop. ed25519_pubkey and +/// x25519_pubkey are both hex strings and must both be exactly 64 characters. +/// +/// Declaration: +/// ```cpp +/// void onion_request_builder_add_hop( +/// [in] onion_request_builder_object* builder +/// [in] const char* ed25519_pubkey, +/// [in] const char* x25519_pubkey +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `ed25519_pubkey` -- [in] The ed25519 public key for the snode hop +/// - `x25519_pubkey` -- [in] The x25519 public key for the snode hop +LIBSESSION_EXPORT void onion_request_builder_add_hop( + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey); + +/// API: onion_request_builder_build +/// +/// Wrapper around session::onionreq::Builder::build. payload_in is binary: payload_in +/// has the length provided, destination_ed25519_pubkey and destination_x25519_pubkey +/// are both hex strings and must both be exactly 64 characters. Returns a flag indicating +/// success or failure. +/// +/// Declaration: +/// ```cpp +/// bool onion_request_builder_build( +/// [in] onion_request_builder_object* builder +/// [in] const unsigned char* payload_in, +/// [in] size_t payload_in_len, +/// [out] unsigned char** payload_out, +/// [out] size_t* payload_out_len, +/// [out] unsigned char* final_x25519_pubkey_out, +/// [out] unsigned char* final_x25519_seckey_out +/// ); +/// ``` +/// +/// Inputs: +/// - `builder` -- [in] Pointer to the builder object +/// - `payload_in` -- [in] The payload to be sent in the onion request +/// - `payload_in_len` -- [in] The length of the payload_in +/// - `payload_out` -- [out] payload to be sent through the network, will be nullptr on error +/// - `payload_out_len` -- [out] length of payload_out if not null +/// - `final_x25519_pubkey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final +/// x25519 public key used for the onion request will be written if successful +/// - `final_x25519_seckey_out` -- [out] pointer to a buffer of exactly 32 bytes where the final +/// x25519 secret key used for the onion request will be written if successful +/// +/// Outputs: +/// - `bool` -- True if the onion request payload was successfully constructed, false if it failed. +/// If (and only if) true is returned then `payload_out` must be freed when done with it. +LIBSESSION_EXPORT bool onion_request_builder_build( + onion_request_builder_object* builder, + const unsigned char* payload_in, + size_t payload_in_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/include/session/onionreq/builder.hpp b/include/session/onionreq/builder.hpp new file mode 100644 index 00000000..0ebad565 --- /dev/null +++ b/include/session/onionreq/builder.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +#include "key_types.hpp" + +namespace session::onionreq { + +enum class EncryptType { + aes_gcm, + xchacha20, +}; + +// Takes the encryption type as a string, returns the EncryptType value (or throws if invalid). +// Supported values: aes-gcm and xchacha20. gcm is accepted as an aliases for aes-gcm. +EncryptType parse_enc_type(std::string_view enc_type); + +inline constexpr std::string_view to_string(EncryptType type) { + switch (type) { + case EncryptType::xchacha20: return "xchacha20"sv; + case EncryptType::aes_gcm: return "aes-gcm"sv; + } + return ""sv; +} + +// Builder class for preparing onion request payloads. +class Builder { + public: + EncryptType enc_type; + std::optional destination_x25519_public_key = std::nullopt; + std::optional final_hop_x25519_keypair = std::nullopt; + + Builder(EncryptType enc_type_ = EncryptType::xchacha20) : enc_type{enc_type_} {} + + void set_enc_type(EncryptType enc_type_) { enc_type = enc_type_; } + + void set_snode_destination(ed25519_pubkey ed25519_public_key, x25519_pubkey x25519_public_key) { + destination_x25519_public_key.reset(); + ed25519_public_key_.reset(); + destination_x25519_public_key.emplace(x25519_public_key); + ed25519_public_key_.emplace(ed25519_public_key); + } + + void set_server_destination( + std::string host, + std::string target, + std::string protocol, + std::optional port, + x25519_pubkey x25519_public_key) { + destination_x25519_public_key.reset(); + + host_.emplace(host); + target_.emplace(target); + protocol_.emplace(protocol); + + if (port) + port_.emplace(*port); + + destination_x25519_public_key.emplace(x25519_public_key); + } + + void add_hop(std::pair keys) { hops_.push_back(keys); } + + ustring build(ustring payload); + + private: + std::vector> hops_ = {}; + + // Snode request values + + std::optional ed25519_public_key_ = std::nullopt; + + // Proxied request values + + std::optional host_ = std::nullopt; + std::optional target_ = std::nullopt; + std::optional protocol_ = std::nullopt; + std::optional port_ = std::nullopt; +}; + +} // namespace session::onionreq diff --git a/include/session/onionreq/channel_encryption.hpp b/include/session/onionreq/hop_encryption.hpp similarity index 53% rename from include/session/onionreq/channel_encryption.hpp rename to include/session/onionreq/hop_encryption.hpp index db37271c..fcb18136 100644 --- a/include/session/onionreq/channel_encryption.hpp +++ b/include/session/onionreq/hop_encryption.hpp @@ -3,31 +3,15 @@ #include #include +#include "builder.hpp" #include "key_types.hpp" namespace session::onionreq { -enum class EncryptType { - aes_gcm, - xchacha20, -}; - -// Takes the encryption type as a string, returns the EncryptType value (or throws if invalid). -// Supported values: aes-gcm and xchacha20. gcm is accepted as an aliases for aes-gcm. -EncryptType parse_enc_type(std::string_view enc_type); - -inline constexpr std::string_view to_string(EncryptType type) { - switch (type) { - case EncryptType::xchacha20: return "xchacha20"sv; - case EncryptType::aes_gcm: return "aes-gcm"sv; - } - return ""sv; -} - // Encryption/decription class for encryption/decrypting outgoing/incoming messages. -class ChannelEncryption { +class HopEncryption { public: - ChannelEncryption(x25519_seckey private_key, x25519_pubkey public_key, bool server = true) : + HopEncryption(x25519_seckey private_key, x25519_pubkey public_key, bool server = true) : private_key_{std::move(private_key)}, public_key_{std::move(public_key)}, server_{server} {} @@ -35,12 +19,12 @@ class ChannelEncryption { // Encrypts `plaintext` message using encryption `type`. `pubkey` is the recipients public key. // `reply` should be false for a client-to-snode message, and true on a returning // snode-to-client message. - ustring encrypt(EncryptType type, ustring_view plaintext, const x25519_pubkey& pubkey) const; - ustring decrypt(EncryptType type, ustring_view ciphertext, const x25519_pubkey& pubkey) const; + ustring encrypt(EncryptType type, ustring plaintext, const x25519_pubkey& pubkey) const; + ustring decrypt(EncryptType type, ustring ciphertext, const x25519_pubkey& pubkey) const; // AES-GCM encryption. - ustring encrypt_aesgcm(ustring_view plainText, const x25519_pubkey& pubKey) const; - ustring decrypt_aesgcm(ustring_view cipherText, const x25519_pubkey& pubKey) const; + ustring encrypt_aesgcm(ustring plainText, const x25519_pubkey& pubKey) const; + ustring decrypt_aesgcm(ustring cipherText, const x25519_pubkey& pubKey) const; // xchacha20-poly1305 encryption; for a message sent from client Alice to server Bob we use a // shared key of a Blake2B 32-byte (i.e. crypto_aead_xchacha20poly1305_ietf_KEYBYTES) hash of @@ -50,8 +34,8 @@ class ChannelEncryption { // When Bob (the server) encrypts a method for Alice (the client), he uses shared key // H(bA || A || B) (note that this is *different* that what would result if Bob was a client // sending to Alice the client). - ustring encrypt_xchacha20(ustring_view plaintext, const x25519_pubkey& pubKey) const; - ustring decrypt_xchacha20(ustring_view ciphertext, const x25519_pubkey& pubKey) const; + ustring encrypt_xchacha20(ustring plaintext, const x25519_pubkey& pubKey) const; + ustring decrypt_xchacha20(ustring ciphertext, const x25519_pubkey& pubKey) const; private: const x25519_seckey private_key_; diff --git a/include/session/onionreq/parser.hpp b/include/session/onionreq/parser.hpp index 70a938ec..8d2d290e 100644 --- a/include/session/onionreq/parser.hpp +++ b/include/session/onionreq/parser.hpp @@ -1,6 +1,6 @@ #include -#include "session/onionreq/channel_encryption.hpp" +#include "session/onionreq/hop_encryption.hpp" #include "session/types.hpp" namespace session::onionreq { @@ -11,7 +11,7 @@ constexpr size_t DEFAULT_MAX_SIZE = 10'485'760; // 10 MiB class OnionReqParser { private: x25519_keypair keys; - ChannelEncryption enc; + HopEncryption enc; EncryptType enc_type = EncryptType::aes_gcm; x25519_pubkey remote_pk; ustring payload_; diff --git a/include/session/onionreq/response_parser.h b/include/session/onionreq/response_parser.h new file mode 100644 index 00000000..77237d37 --- /dev/null +++ b/include/session/onionreq/response_parser.h @@ -0,0 +1,61 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "../export.h" +#include "builder.h" + +/// API: onion_request_decrypt +/// +/// Wrapper around session::onionreq::ResponseParser. ciphertext_in is binary. +/// enc_type should be set to ENCRYPT_TYPE::ENCRYPT_TYPE_X_CHA_CHA_20 if it's not +/// set when creating the builder destination_x25519_pubkey, final_x25519_pubkey +/// and final_x25519_seckey should be in bytes and be exactly 32 bytes. Returns a +/// flag indicating success or failure. +/// +/// Declaration: +/// ```cpp +/// bool onion_request_decrypt( +/// [in] const unsigned char* ciphertext, +/// [in] size_t ciphertext_len, +/// [in] ENCRYPT_TYPE enc_type_, +/// [in] const char* destination_x25519_pubkey, +/// [in] const char* final_x25519_pubkey, +/// [in] const char* final_x25519_seckey, +/// [out] unsigned char** plaintext_out, +/// [out] size_t* plaintext_out_len +/// ); +/// ``` +/// +/// Inputs: +/// - `ciphertext` -- [in] The onion request response data +/// - `ciphertext_len` -- [in] The length of ciphertext +/// - `enc_type_` -- [in] The encryption type which was used for the onion request +/// - `destination_x25519_pubkey` -- [in] The x25519 public key for the server destination +/// - `final_x25519_pubkey` -- [in] The final x25519 public key used for the onion request +/// - `final_x25519_seckey` -- [in] The final x25519 secret key used for the onion request +/// - `plaintext_out` -- [out] decrypted content contained within ciphertext, will be nullptr on +/// error +/// - `plaintext_out_len` -- [out] length of plaintext_out if not null +/// +/// Outputs: +/// - `bool` -- True if the onion request was successfully constructed, false if it failed. +/// If (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool onion_request_decrypt( + const unsigned char* ciphertext, + size_t ciphertext_len, + ENCRYPT_TYPE enc_type_, + unsigned char* destination_x25519_pubkey, + unsigned char* final_x25519_pubkey, + unsigned char* final_x25519_seckey, + unsigned char** plaintext_out, + size_t* plaintext_out_len); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/include/session/onionreq/response_parser.hpp b/include/session/onionreq/response_parser.hpp new file mode 100644 index 00000000..9f0c764d --- /dev/null +++ b/include/session/onionreq/response_parser.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "hop_encryption.hpp" +#include "key_types.hpp" + +namespace session::onionreq { + +class ResponseParser { + public: + /// Constructs a parser, parsing the given request sent to us. Throws if parsing or decryption + /// fails. + ResponseParser(session::onionreq::Builder builder); + ResponseParser( + x25519_pubkey destination_x25519_public_key, + x25519_keypair x25519_keypair, + EncryptType enc_type = EncryptType::xchacha20) : + destination_x25519_public_key_{std::move(destination_x25519_public_key)}, + x25519_keypair_{std::move(x25519_keypair)}, + enc_type_{enc_type} {} + + ustring decrypt(ustring ciphertext) const; + + private: + x25519_pubkey destination_x25519_public_key_; + x25519_keypair x25519_keypair_; + EncryptType enc_type_; +}; + +} // namespace session::onionreq diff --git a/include/session/random.h b/include/session/random.h new file mode 100644 index 00000000..2d560e38 --- /dev/null +++ b/include/session/random.h @@ -0,0 +1,24 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_random +/// +/// Wrapper around the randombytes_buf function. +/// +/// Inputs: +/// - `size` -- [in] number of bytes to be generated. +/// +/// Outputs: +/// - `unsigned char*` -- pointer to random bytes of `size` bytes. +LIBSESSION_EXPORT unsigned char* session_random(size_t size); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/random.hpp b/include/session/random.hpp new file mode 100644 index 00000000..72fc8b89 --- /dev/null +++ b/include/session/random.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "types.hpp" + +namespace session::random { + +/// API: random/random +/// +/// Wrapper around the randombytes_buf function. +/// +/// Inputs: +/// - `size` -- the number of random bytes to be generated. +/// +/// Outputs: +/// - random bytes of the specified length. +ustring random(size_t size); + +} // namespace session::random diff --git a/include/session/session_encrypt.h b/include/session/session_encrypt.h new file mode 100644 index 00000000..b91628e3 --- /dev/null +++ b/include/session/session_encrypt.h @@ -0,0 +1,219 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "export.h" + +/// API: crypto/session_encrypt_for_recipient_deterministic +/// +/// This function attempts to encrypt a message using the SessionProtocol. +/// +/// Inputs: +/// - `plaintext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `plaintext_len` -- [in] Length of `plaintext_in` +/// - `ed25519_privkey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `recipient_pubkey` -- [in] the x25519 public key of the recipient (32 bytes). +/// - `ciphertext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// encrypted data written to it, and then the pointer to that buffer is stored here. +/// This buffer must be `free()`d by the caller when done with it *unless* the function returns +/// false, in which case the buffer pointer will not be set. +/// - `ciphertext_len` -- [out] Pointer to a size_t where the length of `ciphertext_out` is stored. +/// Not touched if the function returns false. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_encrypt_for_recipient_deterministic( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* recipient_pubkey, /* 32 bytes */ + unsigned char** ciphertext_out, + size_t* ciphertext_len); + +/// API: crypto/session_encrypt_for_blinded_recipient +/// +/// This function attempts to encrypt a message using the SessionBlindingProtocol. +/// +/// Inputs: +/// - `plaintext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `plaintext_len` -- [in] Length of `plaintext_in` +/// - `ed25519_privkey` -- [in] the Ed25519 private key of the sender (64 bytes). +/// - `open_group_pubkey` -- [in] the public key of the open group server to route +/// the blinded message through (32 bytes). +/// - `recipient_blinded_id` -- [in] the blinded id of the recipient including the blinding +/// prefix (33 bytes), 'blind15' or 'blind25' encryption will be chosed based on this value. +/// - `ciphertext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// encrypted data written to it, and then the pointer to that buffer is stored here. +/// This buffer must be `free()`d by the caller when done with it *unless* the function returns +/// false, in which case the buffer pointer will not be set. +/// - `ciphertext_len` -- [out] Pointer to a size_t where the length of `ciphertext_out` is stored. +/// Not touched if the function returns false. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_encrypt_for_blinded_recipient( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* open_group_pubkey, /* 32 bytes */ + const unsigned char* recipient_blinded_id, /* 33 bytes */ + unsigned char** ciphertext_out, + size_t* ciphertext_len); + +/// API: crypto/session_decrypt_incoming +/// +/// This function attempts to decrypt a message using the SessionProtocol. +/// +/// Inputs: +/// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `ciphertext_len` -- [in] Length of `ciphertext_in` +/// - `ed25519_privkey` -- [in] the Ed25519 private key of the receiver (64 bytes). +/// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, +/// hex-encoded session_id of the message's author will be written if decryption/verification was +/// successful. +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted data written to it, and then the pointer to that buffer is stored here. +/// This buffer must be `free()`d by the caller when done with it *unless* the function returns +/// false, in which case the buffer pointer will not be set. +/// - `plaintext_len` -- [out] Pointer to a size_t where the length of `plaintext_out` is stored. +/// Not touched if the function returns false. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_decrypt_incoming( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* ed25519_privkey, /* 64 bytes */ + char* session_id_out, /* 67 byte output buffer */ + unsigned char** plaintext_out, + size_t* plaintext_len); + +/// API: crypto/session_decrypt_incoming_legacy_group +/// +/// This function attempts to decrypt a message using the SessionProtocol. +/// +/// Inputs: +/// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `ciphertext_len` -- [in] Length of `ciphertext_in` +/// - `x25519_pubkey` -- [in] the x25519 public key of the receiver (32 bytes). +/// - `x25519_seckey` -- [in] the x25519 secret key of the receiver (32 bytes). +/// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, +/// hex-encoded session_id of the message's author will be written if decryption/verification was +/// successful. +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted data written to it, and then the pointer to that buffer is stored here. +/// This buffer must be `free()`d by the caller when done with it *unless* the function returns +/// false, in which case the buffer pointer will not be set. +/// - `plaintext_len` -- [out] Pointer to a size_t where the length of `plaintext_out` is stored. +/// Not touched if the function returns false. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_decrypt_incoming_legacy_group( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* x25519_pubkey, /* 32 bytes */ + const unsigned char* x25519_seckey, /* 32 bytes */ + char* session_id_out, /* 67 byte output buffer */ + unsigned char** plaintext_out, + size_t* plaintext_len); + +/// API: crypto/session_decrypt_for_blinded_recipient +/// +/// This function attempts to decrypt a message using the SessionBlindingProtocol. +/// +/// Inputs: +/// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `ciphertext_len` -- [in] Length of `ciphertext_in` +/// - `ed25519_privkey` -- [in] the Ed25519 private key of the receiver (64 bytes). +/// - `open_group_pubkey` -- [in] the public key of the open group server to route +/// the blinded message through (32 bytes). +/// - `sender_id` -- [in] the blinded id of the sender including the blinding prefix (33 bytes), +/// 'blind15' or 'blind25' decryption will be chosed based on this value. +/// - `recipient_id` -- [in] the blinded id of the recipient including the blinding prefix (33 +/// bytes), +/// must match the same 'blind15' or 'blind25' type of the `sender_id`. +/// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, +/// hex-encoded session_id of the message's author will be written if decryption/verification was +/// successful. +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted data written to it, and then the pointer to that buffer is stored here. +/// This buffer must be `free()`d by the caller when done with it *unless* the function returns +/// false, in which case the buffer pointer will not be set. +/// - `plaintext_len` -- [out] Pointer to a size_t where the length of `plaintext_out` is stored. +/// Not touched if the function returns false. +/// +/// Outputs: +/// - `bool` -- True if the message was successfully decrypted, false if decryption failed. If +/// (and only if) true is returned then `plaintext_out` must be freed when done with it. +LIBSESSION_EXPORT bool session_decrypt_for_blinded_recipient( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* ed25519_privkey, /* 64 bytes */ + const unsigned char* open_group_pubkey, /* 32 bytes */ + const unsigned char* sender_id, /* 33 bytes */ + const unsigned char* recipient_id, /* 33 bytes */ + char* session_id_out, /* 67 byte output buffer */ + unsigned char** plaintext_out, + size_t* plaintext_len); + +/// API: crypto/session_decrypt_ons_response +/// +/// This function attempts to decrypt an ONS response. +/// +/// Inputs: +/// - `lowercase_name_in` -- [in] Pointer to a buffer containing the lowercase name used to trigger +/// the response. +/// - `name_len` -- [in] Length of `name_in`. +/// - `ciphertext_in` -- [in] Pointer to a data buffer containing the encrypted data. +/// - `ciphertext_len` -- [in] Length of `ciphertext_in`. +/// - `nonce_in` -- [in] Pointer to a data buffer containing the nonce (24 bytes). +/// - `session_id_out` -- [out] pointer to a buffer of at least 67 bytes where the null-terminated, +/// hex-encoded session_id will be written if decryption was successful. +/// +/// Outputs: +/// - `bool` -- True if the session ID was successfully decrypted, false if decryption failed. +LIBSESSION_EXPORT bool session_decrypt_ons_response( + const char* lowercase_name_in, + size_t name_len, + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* nonce_in, /* 24 bytes */ + char* session_id_out /* 67 byte output buffer */); + +/// API: crypto/session_decrypt_push_notification +/// +/// Decrypts a push notification payload. +/// +/// Inputs: +/// - `payload_in` -- [in] the payload included in the push notification. +/// - `payload_len` -- [in] Length of `payload_in`. +/// - `enc_key_in` -- [in] the device encryption key used when subscribing for push notifications +/// (32 bytes). +/// - `plaintext_out` -- [out] Pointer-pointer to an output buffer; a new buffer is allocated, the +/// decrypted data written to it, and then the pointer to that buffer is stored here. +/// This buffer must be `free()`d by the caller when done with it *unless* the function returns +/// false, in which case the buffer pointer will not be set. +/// - `plaintext_len` -- [out] Pointer to a size_t where the length of `plaintext_out` is stored. +/// Not touched if the function returns false. +/// +/// Outputs: +/// - `bool` -- True if the decryption was successful, false if decryption failed. +LIBSESSION_EXPORT bool session_decrypt_push_notification( + const unsigned char* payload_in, + size_t payload_len, + const unsigned char* enc_key_in, /* 32 bytes */ + unsigned char** plaintext_out, + size_t* plaintext_len); + +#ifdef __cplusplus +} +#endif diff --git a/include/session/session_encrypt.hpp b/include/session/session_encrypt.hpp index b702d16a..6a185a73 100644 --- a/include/session/session_encrypt.hpp +++ b/include/session/session_encrypt.hpp @@ -80,6 +80,27 @@ ustring encrypt_for_recipient( ustring encrypt_for_recipient_deterministic( ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message); +/// API: crypto/session_encrypt_for_blinded_recipient +/// +/// This function attempts to encrypt a message using the SessionBlindingProtocol. +/// +/// Inputs: +/// - `ed25519_privkey` -- the libsodium-style secret key of the sender, 64 bytes. Can also be +/// passed as a 32-byte seed, but the 64-byte value is preferrable (to avoid needing to +/// recompute the public key from the seed). +/// - `recipient_pubkey` -- the recipient blinded id, either 0x15-prefixed or 0x25-prefixed +/// (33 bytes). +/// - `message` -- the message to encrypt for the recipient. +/// +/// Outputs: +/// - The encrypted ciphertext to send. +/// - Throw if encryption fails or (which typically means invalid keys provided) +ustring encrypt_for_blinded_recipient( + ustring_view ed25519_privkey, + ustring_view server_pk, + ustring_view recipient_blinded_id, + ustring_view message); + /// API: crypto/sign_for_recipient /// /// Performs the signing steps for session protocol encryption. This is responsible for producing @@ -118,11 +139,122 @@ ustring sign_for_recipient( /// - `ciphertext` -- the encrypted data /// /// Outputs: -/// - pair consisting of the decrypted message content, and the sender Ed25519 pubkey, *if* the -/// message decrypted and validated successfully. Throws on error. -/// -/// To get the sender's session ID, pass the returned pubkey through -/// crypto_sign_ed25519_pk_to_curve25519. +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on +/// error. std::pair decrypt_incoming(ustring_view ed25519_privkey, ustring_view ciphertext); +/// API: crypto/decrypt_incoming +/// +/// Inverse of `encrypt_for_recipient`: this decrypts the message, extracts the sender Ed25519 +/// pubkey, and verifies that the sender Ed25519 signature on the message. This function is used +/// for decrypting legacy group messages which only have an x25519 key pair, the Ed25519 version +/// of this function should be preferred where possible. +/// +/// Inputs: +/// - `ed25519_privkey` -- the private key of the recipient. Can be a 32-byte seed, or a 64-byte +/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey +/// from the seed. +/// - `ciphertext` -- the encrypted data +/// +/// Outputs: +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// sender's ED25519 pubkey, *if* the message decrypted and validated successfully. Throws on +/// error. +std::pair decrypt_incoming( + ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); + +/// API: crypto/decrypt_incoming +/// +/// Inverse of `encrypt_for_recipient`: this decrypts the message, verifies that the sender Ed25519 +/// signature on the message and converts the extracted sender's Ed25519 pubkey into a session ID. +/// +/// Inputs: +/// - `ed25519_privkey` -- the private key of the recipient. Can be a 32-byte seed, or a 64-byte +/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey +/// from the seed. +/// - `ciphertext` -- the encrypted data +/// +/// Outputs: +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_incoming_session_id( + ustring_view ed25519_privkey, ustring_view ciphertext); + +/// API: crypto/decrypt_incoming +/// +/// Inverse of `encrypt_for_recipient`: this decrypts the message, verifies that the sender Ed25519 +/// signature on the message and converts the extracted sender's Ed25519 pubkey into a session ID. +/// This function is used for decrypting legacy group messages which only have an x25519 key pair, +/// the Ed25519 version of this function should be preferred where possible. +/// +/// Inputs: +/// - `x25519_pubkey` -- the 32 byte x25519 public key of the recipient. +/// - `x25519_seckey` -- the 32 byte x25519 private key of the recipient. +/// - `ciphertext` -- the encrypted data +/// +/// Outputs: +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_incoming_session_id( + ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext); + +/// API: crypto/decrypt_from_blinded_recipient +/// +/// This function attempts to decrypt a message using the SessionBlindingProtocol. If the +/// `sender_id` matches the `blinded_id` generated from the `ed25519_privkey` this function assumes +/// the `ciphertext` is an outgoing message and decrypts it as such. +/// +/// Inputs: +/// - `ed25519_privkey` -- the Ed25519 private key of the receiver. Can be a 32-byte seed, or a +/// 64-byte +/// libsodium secret key. The latter is a bit faster as it doesn't have to re-compute the pubkey +/// from the seed. +/// - `server_pk` -- the public key of the open group server to route the blinded message through +/// (32 bytes). +/// - `sender_id` -- the blinded id of the sender including the blinding prefix (33 bytes), +/// 'blind15' or 'blind25' decryption will be chosed based on this value. +/// - `recipient_id` -- the blinded id of the recipient including the blinding prefix (33 bytes), +/// must match the same 'blind15' or 'blind25' type of the `sender_id`. +/// - `ciphertext` -- Pointer to a data buffer containing the encrypted data. +/// +/// Outputs: +/// - `std::pair` -- the plaintext binary data that was encrypted and the +/// session ID (in hex), *if* the message decrypted and validated successfully. Throws on error. +std::pair decrypt_from_blinded_recipient( + ustring_view ed25519_privkey, + ustring_view server_pk, + ustring_view sender_id, + ustring_view recipient_id, + ustring_view ciphertext); + +/// API: crypto/decrypt_ons_response +/// +/// Decrypts the response of an ONS lookup. +/// +/// Inputs: +/// - `lowercase_name` -- the lowercase name which was looked to up to retrieve this response. +/// - `ciphertext` -- ciphertext returned from the server. +/// - `nonce` -- the nonce returned from the server +/// +/// Outputs: +/// - `std::string` -- the session ID (in hex) returned from the server, *if* the server returned +/// a session ID. Throws on error/failure. +std::string decrypt_ons_response( + std::string_view lowercase_name, ustring_view ciphertext, ustring_view nonce); + +/// API: crypto/decrypt_push_notification +/// +/// Decrypts a push notification payload. +/// +/// Inputs: +/// - `payload` -- the payload included in the push notification. +/// - `enc_key` -- the device encryption key used when subscribing for push notifications (32 +/// bytes). +/// +/// Outputs: +/// - `ustring` -- the decrypted push notification payload, *if* the decryption was +/// successful. Throws on error/failure. +ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key); + } // namespace session diff --git a/include/session/util.hpp b/include/session/util.hpp index 5cea9fa4..65ae5fb2 100644 --- a/include/session/util.hpp +++ b/include/session/util.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -144,6 +145,15 @@ struct sodium_cleared : T { ~sodium_cleared() { sodium_zero_buffer(this, sizeof(*this)); } }; +template +using cleared_array = sodium_cleared>; + +using uc32 = std::array; +using uc33 = std::array; +using uc64 = std::array; +using cleared_uc32 = cleared_array<32>; +using cleared_uc64 = cleared_array<64>; + // This is an optional (i.e. can be empty) fixed-size (at construction) buffer that does allocation // and freeing via libsodium. It is slower and heavier than a regular allocation type but takes // extra precautions, intended for storing sensitive values. diff --git a/include/session/xed25519.h b/include/session/xed25519.h index 5348dafa..4963ce89 100644 --- a/include/session/xed25519.h +++ b/include/session/xed25519.h @@ -4,28 +4,32 @@ extern "C" { #endif +#include + +#include "export.h" + /// XEd25519-signed a message given a curve25519 privkey and message. Writes the 64-byte signature /// to `sig` on success and returns 0. Returns non-zero on failure. -__attribute__((warn_unused_result)) int session_xed25519_sign( +LIBSESSION_EXPORT bool session_xed25519_sign( unsigned char* signature /* 64 byte buffer */, const unsigned char* curve25519_privkey /* 32 bytes */, const unsigned char* msg, - const unsigned int msg_len); + size_t msg_len); /// Verifies an XEd25519-signed message given a 64-byte signature, 32-byte curve25519 pubkey, and /// message. Returns 0 if the signature verifies successfully, non-zero on failure. -__attribute__((warn_unused_result)) int session_xed25519_verify( +LIBSESSION_EXPORT bool session_xed25519_verify( const unsigned char* signature /* 64 bytes */, const unsigned char* pubkey /* 32-bytes */, const unsigned char* msg, - const unsigned int msg_len); + size_t msg_len); /// Given a curve25519 pubkey, this writes the associated XEd25519-derived Ed25519 pubkey into /// ed25519_pubkey. Note, however, that there are *two* possible Ed25519 pubkeys that could result /// in a given curve25519 pubkey: this always returns the positive value. You can get the other /// possibility (the negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`. /// Returns 0 on success, non-0 on failure. -__attribute__((warn_unused_result)) int session_xed25519_pubkey( +LIBSESSION_EXPORT bool session_xed25519_pubkey( unsigned char* ed25519_pubkey /* 32-byte output buffer */, const unsigned char* curve25519_pubkey /* 32 bytes */); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9495b694..6c3f1158 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -47,7 +47,11 @@ endif() add_libsession_util_library(crypto blinding.cpp + curve25519.cpp + ed25519.cpp + hash.cpp multi_encrypt.cpp + random.cpp session_encrypt.cpp util.cpp xed25519.cpp @@ -93,9 +97,11 @@ target_link_libraries(config if(ENABLE_ONIONREQ) add_libsession_util_library(onionreq - onionreq/channel_encryption.cpp + onionreq/builder.cpp + onionreq/hop_encryption.cpp onionreq/key_types.cpp onionreq/parser.cpp + onionreq/response_parser.cpp ) target_link_libraries(onionreq diff --git a/src/blinding.cpp b/src/blinding.cpp index b9783895..a9892fe8 100644 --- a/src/blinding.cpp +++ b/src/blinding.cpp @@ -9,6 +9,7 @@ #include #include +#include "session/export.h" #include "session/xed25519.hpp" namespace session { @@ -19,6 +20,20 @@ using uc32 = std::array; using uc33 = std::array; using uc64 = std::array; +std::array blind15_factor(ustring_view server_pk) { + assert(server_pk.size() == 32); + + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init(&st, nullptr, 0, 64); + crypto_generichash_blake2b_update(&st, server_pk.data(), server_pk.size()); + uc64 blind_hash; + crypto_generichash_blake2b_final(&st, blind_hash.data(), blind_hash.size()); + + uc32 k; + crypto_core_ed25519_scalar_reduce(k.data(), blind_hash.data()); + return k; +} + std::array blind25_factor(ustring_view session_id, ustring_view server_pk) { assert(session_id.size() == 32 || session_id.size() == 33); assert(server_pk.size() == 32); @@ -41,6 +56,16 @@ std::array blind25_factor(ustring_view session_id, ustring_vi namespace { + void blind15_id_impl(ustring_view session_id, ustring_view server_pk, unsigned char* out) { + auto k = blind15_factor(server_pk); + if (session_id.size() == 33) + session_id.remove_prefix(1); + auto ed_pk = xed25519::pubkey(session_id); + if (0 != crypto_scalarmult_ed25519_noclamp(out + 1, k.data(), ed_pk.data())) + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + out[0] = 0x15; + } + void blind25_id_impl(ustring_view session_id, ustring_view server_pk, unsigned char* out) { auto k = blind25_factor(session_id, server_pk); if (session_id.size() == 33) @@ -53,6 +78,45 @@ namespace { } // namespace +ustring blind15_id(ustring_view session_id, ustring_view server_pk) { + if (session_id.size() == 33) { + if (session_id[0] != 0x05) + throw std::invalid_argument{"blind15_id: session_id must start with 0x05"}; + session_id.remove_prefix(1); + } else if (session_id.size() != 32) { + throw std::invalid_argument{"blind15_id: session_id must be 32 or 33 bytes"}; + } + if (server_pk.size() != 32) + throw std::invalid_argument{"blind15_id: server_pk must be 32 bytes"}; + + ustring result; + result.resize(33); + blind15_id_impl(session_id, server_pk, result.data()); + return result; +} + +std::array blind15_id(std::string_view session_id, std::string_view server_pk) { + if (session_id.size() != 66 || !oxenc::is_hex(session_id)) + throw std::invalid_argument{"blind15_id: session_id must be hex (66 digits)"}; + if (session_id[0] != '0' || session_id[1] != '5') + throw std::invalid_argument{"blind15_id: session_id must start with 05"}; + if (server_pk.size() != 64 || !oxenc::is_hex(server_pk)) + throw std::invalid_argument{"blind15_id: server_pk must be hex (64 digits)"}; + + uc33 raw_sid; + oxenc::from_hex(session_id.begin(), session_id.end(), raw_sid.begin()); + uc32 raw_server_pk; + oxenc::from_hex(server_pk.begin(), server_pk.end(), raw_server_pk.begin()); + + uc33 blinded; + blind15_id_impl(to_sv(raw_sid), to_sv(raw_server_pk), blinded.data()); + std::array result; + result[0] = oxenc::to_hex(blinded.begin(), blinded.end()); + blinded.back() ^= 0x80; + result[1] = oxenc::to_hex(blinded.begin(), blinded.end()); + return result; +} + ustring blind25_id(ustring_view session_id, ustring_view server_pk) { if (session_id.size() == 33) { if (session_id[0] != 0x05) @@ -87,10 +151,68 @@ std::string blind25_id(std::string_view session_id, std::string_view server_pk) return oxenc::to_hex(blinded.begin(), blinded.end()); } -static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); -static const auto hash_key_sig = to_unsigned_sv("SessCommBlind25_sig"sv); +ustring blinded15_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id) { + if (ed_pubkey.size() != 32) + throw std::invalid_argument{"blind15_id_from_ed: ed_pubkey must be 32 bytes"}; + if (server_pk.size() != 32) + throw std::invalid_argument{"blind15_id_from_ed: server_pk must be 32 bytes"}; + if (session_id && !session_id->empty()) + throw std::invalid_argument{ + "blind15_id_from_ed: session_id pointer must be an empty string"}; -ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message) { + if (session_id) { + session_id->resize(33); + session_id->front() = 0x05; + if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id->data() + 1, ed_pubkey.data())) + throw std::runtime_error{"ed25519 pubkey to x25519 pubkey conversion failed"}; + } + + ustring result; + result.resize(33); + auto k = blind15_factor(server_pk); + if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), ed_pubkey.data())) + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + result[0] = 0x15; + return result; +} + +ustring blinded25_id_from_ed(ustring_view ed_pubkey, ustring_view server_pk, ustring* session_id) { + if (ed_pubkey.size() != 32) + throw std::invalid_argument{"blind25_id_from_ed: ed_pubkey must be 32 bytes"}; + if (server_pk.size() != 32) + throw std::invalid_argument{"blind25_id_from_ed: server_pk must be 32 bytes"}; + if (session_id && session_id->size() != 0 && session_id->size() != 33) + throw std::invalid_argument{"blind25_id_from_ed: session_id pointer must be 0 or 33 bytes"}; + + ustring tmp_session_id; + if (!session_id) + session_id = &tmp_session_id; + if (session_id->size() == 0) { + session_id->resize(33); + session_id->front() = 0x05; + if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id->data() + 1, ed_pubkey.data())) + throw std::runtime_error{"ed25519 pubkey to x25519 pubkey conversion failed"}; + } + + auto k = blind25_factor(*session_id, server_pk); + + ustring result; + result.resize(33); + // Blinded25 ids are always constructed using the absolute value of the ed pubkey, so if + // negative we need to clear the sign bit to make it positive before computing the blinded + // pubkey. + uc32 pos_ed_pubkey; + std::memcpy(pos_ed_pubkey.data(), ed_pubkey.data(), 32); + pos_ed_pubkey[31] &= 0x7f; + + if (0 != crypto_scalarmult_ed25519_noclamp(result.data() + 1, k.data(), pos_ed_pubkey.data())) + throw std::runtime_error{"Cannot blind: invalid session_id (not on main subgroup)"}; + result[0] = 0x25; + return result; +} + +std::pair blind15_key_pair( + ustring_view ed25519_sk, ustring_view server_pk, uc32* k) { std::array ed_sk_tmp; if (ed25519_sk.size() == 32) { std::array pk_ignore; @@ -99,44 +221,106 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust } if (ed25519_sk.size() != 64) throw std::invalid_argument{ - "blind25_sign: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; - uc32 server_pk; - if (server_pk_in.size() == 32) - std::memcpy(server_pk.data(), server_pk_in.data(), 32); - else if (server_pk_in.size() == 64 && oxenc::is_hex(server_pk_in)) - oxenc::from_hex(server_pk_in.begin(), server_pk_in.end(), server_pk.begin()); - else - throw std::invalid_argument{"blind25_sign: Invalid server_pk: expected 32 bytes or 64 hex"}; + "blind15_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; + + if (server_pk.size() != 32) + throw std::invalid_argument{"blind15_key_pair: server_pk must be 32 bytes"}; + + std::pair result; + auto& [A, a] = result; - ustring_view S{ed25519_sk.data() + 32, 32}; + /// Generate the blinding factor (storing into `*k`, if a pointer was provided) + uc32 k_tmp; + if (!k) + k = &k_tmp; + *k = blind15_factor(server_pk); - uc32 z; - crypto_sign_ed25519_sk_to_curve25519(z.data(), ed25519_sk.data()); + /// Generate a scalar for the private key + if (0 != crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_sk.data())) + throw std::runtime_error{ + "blind15_key_pair: Invalid ed25519_sk; conversion to curve25519 seckey failed"}; + + // Turn a, A into their blinded versions + crypto_core_ed25519_scalar_mul(a.data(), k->data(), a.data()); + crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); + + return result; +} + +std::pair blind25_key_pair( + ustring_view ed25519_sk, ustring_view server_pk, uc32* k_prime) { + std::array ed_sk_tmp; + if (ed25519_sk.size() == 32) { + std::array pk_ignore; + crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); + ed25519_sk = {ed_sk_tmp.data(), 64}; + } + if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "blind15_key_pair: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; + + if (server_pk.size() != 32) + throw std::invalid_argument{"blind15_key_pair: server_pk must be 32 bytes"}; uc33 session_id; session_id[0] = 0x05; if (0 != crypto_sign_ed25519_pk_to_curve25519(session_id.data() + 1, ed25519_sk.data() + 32)) throw std::runtime_error{ - "blind25_sign: Invalid ed25519_sk; conversion to curve25519 pubkey failed"}; + "blind25_key_pair: Invalid ed25519_sk; conversion to curve25519 pubkey failed"}; ustring_view X{session_id.data() + 1, 32}; - auto k = blind25_factor(X, {server_pk.data(), server_pk.size()}); + /// Generate the blinding factor (storing into `*k`, if a pointer was provided) + uc32 k_tmp; + if (!k_prime) + k_prime = &k_tmp; + *k_prime = blind25_factor(X, {server_pk.data(), server_pk.size()}); + + // For a negative pubkey we use k' = -k so that k'A == kA when A is positive, and k'A = -kA = + // k|A| when A is negative. + if (*(ed25519_sk.data() + 63) & 0x80) + crypto_core_ed25519_scalar_negate(k_prime->data(), k_prime->data()); + + std::pair result; + auto& [A, a] = result; - uc32 a; - uc32 A; - std::memcpy(A.data(), S.data(), 32); - if (S[31] & 0x80) { - // Ed25519 pubkey is negative, so we need to negate `z` to make things come out right - crypto_core_ed25519_scalar_negate(a.data(), z.data()); - A[31] &= 0x7f; - } else - std::memcpy(a.data(), z.data(), 32); + // Generate the private key (scalar), a; (the sodium function naming here is misleading; this + // call actually has nothing to do with conversion to X25519, it just so happens that the + // conversion method is the easiest way to get `a` out of libsodium). + if (0 != crypto_sign_ed25519_sk_to_curve25519(a.data(), ed25519_sk.data())) + throw std::runtime_error{ + "blind25_key_pair: Invalid ed25519_sk; conversion to curve25519 seckey failed"}; // Turn a, A into their blinded versions - crypto_core_ed25519_scalar_mul(a.data(), k.data(), a.data()); + crypto_core_ed25519_scalar_mul(a.data(), k_prime->data(), a.data()); crypto_scalarmult_ed25519_base_noclamp(A.data(), a.data()); + return result; +} + +static const auto hash_key_seed = to_unsigned_sv("SessCommBlind25_seed"sv); +static const auto hash_key_sig = to_unsigned_sv("SessCommBlind25_sig"sv); + +ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message) { + std::array ed_sk_tmp; + if (ed25519_sk.size() == 32) { + std::array pk_ignore; + crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); + ed25519_sk = {ed_sk_tmp.data(), 64}; + } + if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "blind25_sign: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; + uc32 server_pk; + if (server_pk_in.size() == 32) + std::memcpy(server_pk.data(), server_pk_in.data(), 32); + else if (server_pk_in.size() == 64 && oxenc::is_hex(server_pk_in)) + oxenc::from_hex(server_pk_in.begin(), server_pk_in.end(), server_pk.begin()); + else + throw std::invalid_argument{"blind25_sign: Invalid server_pk: expected 32 bytes or 64 hex"}; + + auto [A, a] = blind25_key_pair(ed25519_sk, to_sv(server_pk)); + uc32 seedhash; crypto_generichash_blake2b( seedhash.data(), @@ -179,4 +363,175 @@ ustring blind25_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ust return result; } +ustring blind15_sign(ustring_view ed25519_sk, std::string_view server_pk_in, ustring_view message) { + std::array ed_sk_tmp; + if (ed25519_sk.size() == 32) { + std::array pk_ignore; + crypto_sign_ed25519_seed_keypair(pk_ignore.data(), ed_sk_tmp.data(), ed25519_sk.data()); + ed25519_sk = {ed_sk_tmp.data(), 64}; + } + if (ed25519_sk.size() != 64) + throw std::invalid_argument{ + "blind15_sign: Invalid ed25519_sk is not the expected 32- or 64-byte value"}; + + uc32 server_pk; + if (server_pk_in.size() == 32) + std::memcpy(server_pk.data(), server_pk_in.data(), 32); + else if (server_pk_in.size() == 64 && oxenc::is_hex(server_pk_in)) + oxenc::from_hex(server_pk_in.begin(), server_pk_in.end(), server_pk.begin()); + else + throw std::invalid_argument{"blind15_sign: Invalid server_pk: expected 32 bytes or 64 hex"}; + + auto [blind_15_pk, blind_15_sk] = blind15_key_pair(ed25519_sk, {server_pk.data(), 32}); + + // H_rh = sha512(s.encode()).digest()[32:] + uc64 hrh; + crypto_hash_sha512_state st1; + crypto_hash_sha512_init(&st1); + crypto_hash_sha512_update(&st1, ed25519_sk.data(), 64); + crypto_hash_sha512_final(&st1, hrh.data()); + + // r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts)) + auto hrh_suffix = hrh.data() + 32; + uc32 r; + uc64 r_hash; + crypto_hash_sha512_state st2; + crypto_hash_sha512_init(&st2); + crypto_hash_sha512_update(&st2, hrh_suffix, 32); + crypto_hash_sha512_update(&st2, blind_15_pk.data(), blind_15_pk.size()); + crypto_hash_sha512_update(&st2, message.data(), message.size()); + crypto_hash_sha512_final(&st2, r_hash.data()); + crypto_core_ed25519_scalar_reduce(r.data(), r_hash.data()); + + // sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r) + ustring result; + result.resize(64); + auto* sig_R = result.data(); + auto* sig_S = result.data() + 32; + crypto_scalarmult_ed25519_base_noclamp(sig_R, r.data()); + + // HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts)) + uc64 hram; + crypto_hash_sha512_state st3; + crypto_hash_sha512_init(&st3); + crypto_hash_sha512_update(&st3, sig_R, 32); + crypto_hash_sha512_update(&st3, blind_15_pk.data(), blind_15_pk.size()); + crypto_hash_sha512_update(&st3, message.data(), message.size()); + crypto_hash_sha512_final(&st3, hram.data()); + + // sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka)) + crypto_core_ed25519_scalar_reduce(sig_S, hram.data()); // S = H(R||A||M) + crypto_core_ed25519_scalar_mul(sig_S, sig_S, blind_15_sk.data()); // S = H(R||A||M) a + crypto_core_ed25519_scalar_add(sig_S, sig_S, r.data()); // S = r + H(R||A||M) a + + return result; +} + +bool session_id_matches_blinded_id( + std::string_view session_id, std::string_view blinded_id, std::string_view server_pk) { + if (session_id.size() != 66 || !oxenc::is_hex(session_id)) + throw std::invalid_argument{ + "session_id_matches_blinded_id: session_id must be hex (66 digits)"}; + if (session_id[0] != '0' || session_id[1] != '5') + throw std::invalid_argument{"session_id_matches_blinded_id: session_id must start with 05"}; + if (blinded_id[1] != '5' && (blinded_id[0] != '1' || blinded_id[0] != '2')) + throw std::invalid_argument{ + "session_id_matches_blinded_id: blinded_id must start with 15 or 25"}; + if (server_pk.size() != 64 || !oxenc::is_hex(server_pk)) + throw std::invalid_argument{ + "session_id_matches_blinded_id: server_pk must be hex (64 digits)"}; + + std::string converted_blind_id1, converted_blind_id2; + ustring converted_blind_id1_raw; + + switch (blinded_id[0]) { + case '1': { + auto [converted_blind_id1, converted_blind_id2] = blind15_id(session_id, server_pk); + return (blinded_id == converted_blind_id1 || blinded_id == converted_blind_id2); + } + + // blind25 doesn't run into the negative issue that blind15 did + case '2': return blinded_id == blind25_id(session_id, server_pk); + default: throw std::invalid_argument{"Invalid blinded_id: must start with 15 or 25"}; + } +} + } // namespace session + +using namespace session; + +LIBSESSION_C_API bool session_blind15_key_pair( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out) { + try { + auto result = session::blind15_key_pair({ed25519_seckey, 64}, {server_pk, 32}); + auto [b_pk, b_sk] = result; + std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); + std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_blind25_key_pair( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + unsigned char* blinded_pk_out, + unsigned char* blinded_sk_out) { + try { + auto result = session::blind25_key_pair({ed25519_seckey, 64}, {server_pk, 32}); + auto [b_pk, b_sk] = result; + std::memcpy(blinded_pk_out, b_pk.data(), b_pk.size()); + std::memcpy(blinded_sk_out, b_sk.data(), b_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_blind15_sign( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out) { + try { + auto result = session::blind15_sign( + {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); + auto sig = result; + std::memcpy(blinded_sig_out, sig.data(), sig.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_blind25_sign( + const unsigned char* ed25519_seckey, + const unsigned char* server_pk, + const unsigned char* msg, + size_t msg_len, + unsigned char* blinded_sig_out) { + try { + auto result = session::blind25_sign( + {ed25519_seckey, 64}, {from_unsigned(server_pk), 32}, {msg, msg_len}); + auto sig = result; + std::memcpy(blinded_sig_out, sig.data(), sig.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_id_matches_blinded_id( + const char* session_id, const char* blinded_id, const char* server_pk) { + try { + return session::session_id_matches_blinded_id( + {session_id, 66}, {blinded_id, 66}, {server_pk, 64}); + } catch (...) { + return false; + } +} diff --git a/src/curve25519.cpp b/src/curve25519.cpp new file mode 100644 index 00000000..81870cc3 --- /dev/null +++ b/src/curve25519.cpp @@ -0,0 +1,87 @@ +#include "session/curve25519.hpp" + +#include +#include + +#include + +#include "session/export.h" +#include "session/util.hpp" + +namespace session::curve25519 { + +std::pair, std::array> curve25519_key_pair() { + std::array curve_pk; + std::array curve_sk; + crypto_box_keypair(curve_pk.data(), curve_sk.data()); + + return {curve_pk, curve_sk}; +} + +std::array to_curve25519_pubkey(ustring_view ed25519_pubkey) { + if (ed25519_pubkey.size() != 32) { + throw std::invalid_argument{"Invalid ed25519_pubkey: expected 32 bytes"}; + } + + std::array curve_pk; + + if (0 != crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed25519_pubkey.data())) + throw std::runtime_error{ + "An error occured while attempting to convert Ed25519 pubkey to curve25519; " + "is the pubkey valid?"}; + + return curve_pk; +} + +std::array to_curve25519_seckey(ustring_view ed25519_seckey) { + if (ed25519_seckey.size() != 64) { + throw std::invalid_argument{"Invalid ed25519_seckey: expected 64 bytes"}; + } + + std::array curve_sk; + if (0 != crypto_sign_ed25519_sk_to_curve25519(curve_sk.data(), ed25519_seckey.data())) + throw std::runtime_error{ + "An error occured while attempting to convert Ed25519 pubkey to curve25519; " + "is the seckey valid?"}; + + return curve_sk; +} + +} // namespace session::curve25519 + +using namespace session; + +LIBSESSION_C_API bool session_curve25519_key_pair( + unsigned char* curve25519_pk_out, unsigned char* curve25519_sk_out) { + try { + auto result = session::curve25519::curve25519_key_pair(); + auto [curve_pk, curve_sk] = result; + std::memcpy(curve25519_pk_out, curve_pk.data(), curve_pk.size()); + std::memcpy(curve25519_sk_out, curve_sk.data(), curve_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_to_curve25519_pubkey( + const unsigned char* ed25519_pubkey, unsigned char* curve25519_pk_out) { + try { + auto curve_pk = session::curve25519::to_curve25519_pubkey(ustring_view{ed25519_pubkey, 32}); + std::memcpy(curve25519_pk_out, curve_pk.data(), curve_pk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_to_curve25519_seckey( + const unsigned char* ed25519_seckey, unsigned char* curve25519_sk_out) { + try { + auto curve_sk = session::curve25519::to_curve25519_seckey(ustring_view{ed25519_seckey, 64}); + std::memcpy(curve25519_sk_out, curve_sk.data(), curve_sk.size()); + return true; + } catch (...) { + return false; + } +} diff --git a/src/ed25519.cpp b/src/ed25519.cpp new file mode 100644 index 00000000..87297b09 --- /dev/null +++ b/src/ed25519.cpp @@ -0,0 +1,148 @@ +#include "session/ed25519.hpp" + +#include +#include + +#include + +#include "session/export.h" +#include "session/util.hpp" + +namespace session::ed25519 { + +template +using cleared_array = sodium_cleared>; + +using uc32 = std::array; +using cleared_uc64 = cleared_array<64>; + +std::pair, std::array> ed25519_key_pair() { + std::array ed_pk; + std::array ed_sk; + crypto_sign_ed25519_keypair(ed_pk.data(), ed_sk.data()); + + return {ed_pk, ed_sk}; +} + +std::pair, std::array> ed25519_key_pair( + ustring_view ed25519_seed) { + if (ed25519_seed.size() != 32) { + throw std::invalid_argument{"Invalid ed25519_seed: expected 32 bytes"}; + } + + std::array ed_pk; + std::array ed_sk; + + crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), ed25519_seed.data()); + + return {ed_pk, ed_sk}; +} + +std::array seed_for_ed_privkey(ustring_view ed25519_privkey) { + std::array seed; + + if (ed25519_privkey.size() == 32 || ed25519_privkey.size() == 64) + // The first 32 bytes of a 64 byte ed25519 private key are the seed, otherwise + // if the provided value is 32 bytes we just assume we were given a seed + std::memcpy(seed.data(), ed25519_privkey.data(), 32); + else + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + + return seed; +} + +ustring sign(ustring_view ed25519_privkey, ustring_view msg) { + cleared_uc64 ed_sk_from_seed; + if (ed25519_privkey.size() == 32) { + uc32 ignore_pk; + crypto_sign_ed25519_seed_keypair( + ignore_pk.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; + } else if (ed25519_privkey.size() != 64) { + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + } + + std::array sig; + if (0 != crypto_sign_ed25519_detached( + sig.data(), nullptr, msg.data(), msg.size(), ed25519_privkey.data())) + throw std::runtime_error{"Failed to sign; perhaps the secret key is invalid?"}; + + return {sig.data(), sig.size()}; +} + +bool verify(ustring_view sig, ustring_view pubkey, ustring_view msg) { + if (sig.size() != 64) + throw std::invalid_argument{"Invalid sig: expected 64 bytes"}; + if (pubkey.size() != 32) + throw std::invalid_argument{"Invalid pubkey: expected 32 bytes"}; + + return (0 == + crypto_sign_ed25519_verify_detached(sig.data(), msg.data(), msg.size(), pubkey.data())); +} + +} // namespace session::ed25519 + +using namespace session; + +LIBSESSION_C_API bool session_ed25519_key_pair( + unsigned char* ed25519_pk_out, unsigned char* ed25519_sk_out) { + try { + auto result = session::ed25519::ed25519_key_pair(); + auto [ed_pk, ed_sk] = result; + std::memcpy(ed25519_pk_out, ed_pk.data(), ed_pk.size()); + std::memcpy(ed25519_sk_out, ed_sk.data(), ed_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_ed25519_key_pair_seed( + const unsigned char* ed25519_seed, + unsigned char* ed25519_pk_out, + unsigned char* ed25519_sk_out) { + try { + auto result = session::ed25519::ed25519_key_pair(ustring_view{ed25519_seed, 32}); + auto [ed_pk, ed_sk] = result; + std::memcpy(ed25519_pk_out, ed_pk.data(), ed_pk.size()); + std::memcpy(ed25519_sk_out, ed_sk.data(), ed_sk.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_seed_for_ed_privkey( + const unsigned char* ed25519_privkey, unsigned char* ed25519_seed_out) { + try { + auto result = session::ed25519::seed_for_ed_privkey(ustring_view{ed25519_privkey, 64}); + std::memcpy(ed25519_seed_out, result.data(), result.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_ed25519_sign( + const unsigned char* ed25519_privkey, + const unsigned char* msg, + size_t msg_len, + unsigned char* ed25519_sig_out) { + try { + auto result = session::ed25519::sign( + ustring_view{ed25519_privkey, 64}, ustring_view{msg, msg_len}); + std::memcpy(ed25519_sig_out, result.data(), result.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_ed25519_verify( + const unsigned char* sig, + const unsigned char* pubkey, + const unsigned char* msg, + size_t msg_len) { + return session::ed25519::verify( + ustring_view{sig, 64}, ustring_view{pubkey, 32}, ustring_view{msg, msg_len}); +} diff --git a/src/hash.cpp b/src/hash.cpp new file mode 100644 index 00000000..fd16949f --- /dev/null +++ b/src/hash.cpp @@ -0,0 +1,65 @@ +#include "session/hash.hpp" + +#include + +#include "session/export.h" +#include "session/util.hpp" + +namespace session::hash { + +ustring hash(const size_t size, ustring_view msg, std::optional key) { + if (size < crypto_generichash_blake2b_BYTES_MIN || size > crypto_generichash_blake2b_BYTES_MAX) + throw std::invalid_argument{"Invalid size: expected between 16 and 64 bytes (inclusive)"}; + + if (key && static_cast(*key).size() > crypto_generichash_blake2b_BYTES_MAX) + throw std::invalid_argument{"Invalid key: expected less than 65 bytes"}; + + auto result_code = 0; + unsigned char result[size]; + + if (key) + result_code = crypto_generichash_blake2b( + result, + size, + msg.data(), + msg.size(), + static_cast(*key).data(), + static_cast(*key).size()); + else + result_code = crypto_generichash_blake2b(result, size, msg.data(), msg.size(), nullptr, 0); + + if (result_code != 0) + throw std::runtime_error{"Hash generation failed"}; + + return {result, size}; +} + +} // namespace session::hash + +using session::ustring; +using session::ustring_view; + +extern "C" { + +LIBSESSION_C_API bool session_hash( + size_t size, + const unsigned char* msg_in, + size_t msg_len, + const unsigned char* key_in, + size_t key_len, + unsigned char* hash_out) { + try { + std::optional key; + + if (key_in && key_len) + key = {key_in, key_len}; + + ustring result = session::hash::hash(size, {msg_in, msg_len}, key); + std::memcpy(hash_out, result.data(), size); + return true; + } catch (...) { + return false; + } +} + +} // extern "C" diff --git a/src/onionreq/builder.cpp b/src/onionreq/builder.cpp new file mode 100644 index 00000000..54143d14 --- /dev/null +++ b/src/onionreq/builder.cpp @@ -0,0 +1,275 @@ +#include "session/onionreq/builder.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "session/export.h" +#include "session/onionreq/builder.h" +#include "session/onionreq/hop_encryption.hpp" +#include "session/onionreq/key_types.hpp" +#include "session/util.hpp" +#include "session/xed25519.hpp" + +namespace session::onionreq { + +namespace { + + ustring encode_size(uint32_t s) { + ustring result; + result.resize(4); + oxenc::write_host_as_little(s, result.data()); + return result; + } +} // namespace + +EncryptType parse_enc_type(std::string_view enc_type) { + if (enc_type == "xchacha20" || enc_type == "xchacha20-poly1305") + return EncryptType::xchacha20; + if (enc_type == "aes-gcm" || enc_type == "gcm") + return EncryptType::aes_gcm; + throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; +} + +ustring Builder::build(ustring payload) { + ustring blob; + + // First hop: + // + // [N][ENCRYPTED]{json} + // + // where json has the ephemeral_key indicating how we encrypted ENCRYPTED for this first hop. + // The first hop decrypts ENCRYPTED into: + // + // [N][BLOB]{json} + // + // where [N] is the length of the blob and {json} now contains either: + // - a "headers" key with an empty value. This is how we indicate that the request is for this + // node as the final hop, and means that the BLOB is actually JSON it should parse to get the + // request info (which has "method", "params", etc. in it). + // - "host"/"target"/"port"/"protocol" asking for an HTTP or HTTPS proxy request to be made + // (though "target" must start with /loki/ or /oxen/ and end with /lsrpc). (There is still a + // blob here, but it is not used and typically empty). + // - "destination" and "ephemeral_key" to forward the request to the next hop. + // + // This later case continues onion routing by giving us something like: + // + // {"destination":"ed25519pubkey","ephemeral_key":"x25519-eph-pubkey-for-decryption","enc_type":"xchacha20"} + // + // (enc_type can also be aes-gcm, and defaults to that if not specified). We forward this via + // oxenmq to the given ed25519pubkey (but since oxenmq uses x25519 pubkeys we first have to go + // look it up), sending an oxenmq request to sn.onion_req_v2 of the following (but bencoded, not + // json): + // + // { "d": "BLOB", "ek": "ephemeral-key-in-binary", "et": "xchacha20", "nh": N } + // + // where BLOB is the opaque data received from the previous hop and N is the hop number which + // gets incremented at each hop (and terminates if it exceeds 15). That next hop decrypts BLOB, + // giving it a value interpreted as the same [N][BLOB]{json} as above, and we recurse. + // + // On the *return* trip, the message gets encrypted (once!) at the final destination using the + // derived key from the pubkey given to the final hop, base64-encoded, then passed back without + // any onion encryption at all all the way back to the client. + + // Ephemeral keypair: + x25519_pubkey A; + x25519_seckey a; + nlohmann::json final_route; + + { + crypto_box_keypair(A.data(), a.data()); + HopEncryption e{a, A, false}; + + // The data we send to the destination differs depending on whether the destination is a + // server or a service node + if (host_ && target_ && protocol_ && destination_x25519_public_key) { + final_route = { + {"host", host_.value()}, + {"target", target_.value()}, + {"method", "POST"}, + {"protocol", protocol_.value()}, + {"port", port_.value_or(protocol_.value() == "https" ? 443 : 80)}, + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the + // *next* hop to use + {"enc_type", to_string(enc_type)}, + }; + + blob = e.encrypt(enc_type, payload.data(), *destination_x25519_public_key); + } else if (ed25519_public_key_ && destination_x25519_public_key) { + nlohmann::json control{{"headers", ""}}; + final_route = { + {"destination", ed25519_public_key_.value().hex()}, // Next hop's ed25519 key + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the + // *next* hop to use + {"enc_type", to_string(enc_type)}, + }; + + auto data = encode_size(payload.size()); + data += payload; + data += to_unsigned_sv(control.dump()); + blob = e.encrypt(enc_type, data, *destination_x25519_public_key); + } else { + throw std::runtime_error{"Destination not set"}; + } + + // Save these because we need them again to decrypt the final response: + final_hop_x25519_keypair.reset(); + final_hop_x25519_keypair.emplace(A, a); + } + + for (auto it = hops_.rbegin(); it != hops_.rend(); ++it) { + // Routing data for this hop: + nlohmann::json routing; + + if (it == hops_.rbegin()) { + routing = final_route; + } else { + routing = { + {"destination", std::prev(it)->first.hex()}, // Next hop's ed25519 key + {"ephemeral_key", A.hex()}, // The x25519 ephemeral_key here is the key for the + // *next* hop to use + {"enc_type", to_string(enc_type)}, + }; + } + + auto data = encode_size(blob.size()); + data += blob; + data += to_unsigned_sv(routing.dump()); + + // Generate eph key for *this* request and encrypt it: + crypto_box_keypair(A.data(), a.data()); + HopEncryption e{a, A, false}; + blob = e.encrypt(enc_type, data, it->second); + } + + // The data going to the first hop needs to be wrapped in one more layer to tell the first hop + // how to decrypt the initial payload: + auto result = encode_size(blob.size()); + result += blob; + result += to_unsigned_sv( + nlohmann::json{{"ephemeral_key", A.hex()}, {"enc_type", to_string(enc_type)}}.dump()); + + return result; +} +} // namespace session::onionreq + +namespace { + +session::onionreq::Builder& unbox(onion_request_builder_object* builder) { + assert(builder && builder->internals); + return *static_cast(builder->internals); +} + +} // namespace + +extern "C" { + +using session::ustring; + +LIBSESSION_C_API void onion_request_builder_init(onion_request_builder_object** builder) { + auto c = std::make_unique(); + auto c_builder = std::make_unique(); + c_builder->internals = c.release(); + *builder = c_builder.release(); +} + +LIBSESSION_C_API void onion_request_builder_set_enc_type( + onion_request_builder_object* builder, ENCRYPT_TYPE enc_type) { + assert(builder); + + switch (enc_type) { + case ENCRYPT_TYPE::ENCRYPT_TYPE_AES_GCM: + unbox(builder).set_enc_type(session::onionreq::EncryptType::aes_gcm); + break; + + case ENCRYPT_TYPE::ENCRYPT_TYPE_X_CHA_CHA_20: + unbox(builder).set_enc_type(session::onionreq::EncryptType::xchacha20); + break; + + default: throw std::runtime_error{"Invalid encryption type"}; + } +} + +LIBSESSION_C_API void onion_request_builder_set_snode_destination( + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey) { + assert(builder && ed25519_pubkey && x25519_pubkey); + + unbox(builder).set_snode_destination( + session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64})); +} + +LIBSESSION_C_API void onion_request_builder_set_server_destination( + onion_request_builder_object* builder, + const char* host, + const char* target, + const char* protocol, + uint16_t port, + const char* x25519_pubkey) { + assert(builder && host && target && protocol && x25519_pubkey); + + unbox(builder).set_server_destination( + host, + target, + protocol, + port, + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64})); +} + +LIBSESSION_C_API void onion_request_builder_add_hop( + onion_request_builder_object* builder, + const char* ed25519_pubkey, + const char* x25519_pubkey) { + assert(builder && ed25519_pubkey && x25519_pubkey); + + unbox(builder).add_hop( + {session::onionreq::ed25519_pubkey::from_hex({ed25519_pubkey, 64}), + session::onionreq::x25519_pubkey::from_hex({x25519_pubkey, 64})}); +} + +LIBSESSION_C_API bool onion_request_builder_build( + onion_request_builder_object* builder, + const unsigned char* payload_in, + size_t payload_in_len, + unsigned char** payload_out, + size_t* payload_out_len, + unsigned char* final_x25519_pubkey_out, + unsigned char* final_x25519_seckey_out) { + assert(builder && payload_in); + + try { + auto unboxed_builder = unbox(builder); + auto payload = unboxed_builder.build(ustring{payload_in, payload_in_len}); + + if (unboxed_builder.final_hop_x25519_keypair) { + auto key_pair = unboxed_builder.final_hop_x25519_keypair.value(); + std::memcpy(final_x25519_pubkey_out, key_pair.first.data(), key_pair.first.size()); + std::memcpy(final_x25519_seckey_out, key_pair.second.data(), key_pair.second.size()); + } else { + throw std::runtime_error{"Final keypair not generated"}; + } + + *payload_out = static_cast(malloc(payload.size())); + *payload_out_len = payload.size(); + std::memcpy(*payload_out, payload.data(), payload.size()); + + return true; + } catch (...) { + return false; + } +} +} \ No newline at end of file diff --git a/src/onionreq/channel_encryption.cpp b/src/onionreq/hop_encryption.cpp similarity index 84% rename from src/onionreq/channel_encryption.cpp rename to src/onionreq/hop_encryption.cpp index afcc6b56..d15ef641 100644 --- a/src/onionreq/channel_encryption.cpp +++ b/src/onionreq/hop_encryption.cpp @@ -1,9 +1,11 @@ -#include "session/onionreq/channel_encryption.hpp" +#include "session/onionreq/hop_encryption.hpp" #include +#include #include #include #include +#include #include #include #include @@ -12,6 +14,13 @@ #include #include #include +#include + +#include "session/export.h" +#include "session/onionreq/builder.hpp" +#include "session/onionreq/key_types.hpp" +#include "session/util.hpp" +#include "session/xed25519.hpp" namespace session::onionreq { @@ -71,16 +80,8 @@ namespace { } // namespace -EncryptType parse_enc_type(std::string_view enc_type) { - if (enc_type == "xchacha20" || enc_type == "xchacha20-poly1305") - return EncryptType::xchacha20; - if (enc_type == "aes-gcm" || enc_type == "gcm") - return EncryptType::aes_gcm; - throw std::runtime_error{"Invalid encryption type " + std::string{enc_type}}; -} - -ustring ChannelEncryption::encrypt( - EncryptType type, ustring_view plaintext, const x25519_pubkey& pubkey) const { +ustring HopEncryption::encrypt( + EncryptType type, ustring plaintext, const x25519_pubkey& pubkey) const { switch (type) { case EncryptType::xchacha20: return encrypt_xchacha20(plaintext, pubkey); case EncryptType::aes_gcm: return encrypt_aesgcm(plaintext, pubkey); @@ -88,8 +89,8 @@ ustring ChannelEncryption::encrypt( throw std::runtime_error{"Invalid encryption type"}; } -ustring ChannelEncryption::decrypt( - EncryptType type, ustring_view ciphertext, const x25519_pubkey& pubkey) const { +ustring HopEncryption::decrypt( + EncryptType type, ustring ciphertext, const x25519_pubkey& pubkey) const { switch (type) { case EncryptType::xchacha20: return decrypt_xchacha20(ciphertext, pubkey); case EncryptType::aes_gcm: return decrypt_aesgcm(ciphertext, pubkey); @@ -97,9 +98,7 @@ ustring ChannelEncryption::decrypt( throw std::runtime_error{"Invalid decryption type"}; } -ustring ChannelEncryption::encrypt_aesgcm( - ustring_view plaintext, const x25519_pubkey& pubKey) const { - +ustring HopEncryption::encrypt_aesgcm(ustring plaintext, const x25519_pubkey& pubKey) const { auto key = derive_symmetric_key(private_key_, pubKey); // Initialise cipher context with the key @@ -129,8 +128,8 @@ ustring ChannelEncryption::encrypt_aesgcm( return output; } -ustring ChannelEncryption::decrypt_aesgcm( - ustring_view ciphertext, const x25519_pubkey& pubKey) const { +ustring HopEncryption::decrypt_aesgcm(ustring ciphertext_, const x25519_pubkey& pubKey) const { + ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; if (ciphertext.size() < GCM_IV_SIZE + GCM_DIGEST_SIZE) throw std::runtime_error{"ciphertext data is too short"}; @@ -162,8 +161,7 @@ ustring ChannelEncryption::decrypt_aesgcm( return plaintext; } -ustring ChannelEncryption::encrypt_xchacha20( - ustring_view plaintext, const x25519_pubkey& pubKey) const { +ustring HopEncryption::encrypt_xchacha20(ustring plaintext, const x25519_pubkey& pubKey) const { ustring ciphertext; ciphertext.resize( @@ -175,7 +173,8 @@ ustring ChannelEncryption::encrypt_xchacha20( // Generate random nonce, and stash it at the beginning of ciphertext: randombytes_buf(ciphertext.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); - auto* c = ciphertext.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; + auto* c = reinterpret_cast(ciphertext.data()) + + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; unsigned long long clen; crypto_aead_xchacha20poly1305_ietf_encrypt( @@ -186,15 +185,15 @@ ustring ChannelEncryption::encrypt_xchacha20( nullptr, 0, // additional data nullptr, // nsec (always unused) - ciphertext.data(), + reinterpret_cast(ciphertext.data()), key.data()); assert(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen <= ciphertext.size()); ciphertext.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + clen); return ciphertext; } -ustring ChannelEncryption::decrypt_xchacha20( - ustring_view ciphertext, const x25519_pubkey& pubKey) const { +ustring HopEncryption::decrypt_xchacha20(ustring ciphertext_, const x25519_pubkey& pubKey) const { + ustring_view ciphertext = {ciphertext_.data(), ciphertext_.size()}; // Extract nonce from the beginning of the ciphertext: auto nonce = ciphertext.substr(0, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); @@ -206,7 +205,7 @@ ustring ChannelEncryption::decrypt_xchacha20( ustring plaintext; plaintext.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); - auto* m = plaintext.data(); + auto* m = reinterpret_cast(plaintext.data()); unsigned long long mlen; if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( m, diff --git a/src/onionreq/parser.cpp b/src/onionreq/parser.cpp index a1167a34..4c8448c5 100644 --- a/src/onionreq/parser.cpp +++ b/src/onionreq/parser.cpp @@ -36,11 +36,12 @@ OnionReqParser::OnionReqParser( else throw std::invalid_argument{"metadata does not have 'ephemeral_key' entry"}; - payload_ = enc.decrypt(enc_type, ciphertext, remote_pk); + auto plaintext = enc.decrypt(enc_type, {ciphertext.data(), ciphertext.size()}, remote_pk); + payload_ = {to_unsigned(plaintext.data()), plaintext.size()}; } ustring OnionReqParser::encrypt_reply(ustring_view reply) const { - return enc.encrypt(enc_type, reply, remote_pk); + return enc.encrypt(enc_type, {reply.data(), reply.size()}, remote_pk); } } // namespace session::onionreq diff --git a/src/onionreq/response_parser.cpp b/src/onionreq/response_parser.cpp new file mode 100644 index 00000000..2ace3237 --- /dev/null +++ b/src/onionreq/response_parser.cpp @@ -0,0 +1,113 @@ +#include "session/onionreq/response_parser.hpp" + +#include +#include + +#include + +#include "session/export.h" +#include "session/onionreq/builder.h" +#include "session/onionreq/builder.hpp" +#include "session/onionreq/hop_encryption.hpp" + +namespace session::onionreq { + +ResponseParser::ResponseParser(session::onionreq::Builder builder) { + if (!builder.destination_x25519_public_key.has_value()) + throw std::runtime_error{"Builder does not contain destination x25519 public key"}; + if (!builder.final_hop_x25519_keypair.has_value()) + throw std::runtime_error{"Builder does not contain final keypair"}; + + enc_type_ = builder.enc_type; + destination_x25519_public_key_ = builder.destination_x25519_public_key.value(); + x25519_keypair_ = builder.final_hop_x25519_keypair.value(); +} + +ustring ResponseParser::decrypt(ustring ciphertext) const { + HopEncryption d{x25519_keypair_.second, x25519_keypair_.first, false}; + + // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an + // error encrypted with 'aes_gcm' so try to decrypt in case that is what happened - this + // workaround can be removed once the legacy PN server is removed + try { + return d.decrypt(enc_type_, ciphertext, destination_x25519_public_key_); + } catch (const std::exception& e) { + if (enc_type_ == session::onionreq::EncryptType::xchacha20) + return d.decrypt( + session::onionreq::EncryptType::aes_gcm, + ciphertext, + destination_x25519_public_key_); + else + throw e; + } +} + +} // namespace session::onionreq + +extern "C" { + +using session::ustring; + +LIBSESSION_C_API bool onion_request_decrypt( + const unsigned char* ciphertext, + size_t ciphertext_len, + ENCRYPT_TYPE enc_type_, + unsigned char* destination_x25519_pubkey, + unsigned char* final_x25519_pubkey, + unsigned char* final_x25519_seckey, + unsigned char** plaintext_out, + size_t* plaintext_out_len) { + assert(ciphertext && destination_x25519_pubkey && final_x25519_pubkey && final_x25519_seckey && + ciphertext_len > 0); + + try { + auto enc_type = session::onionreq::EncryptType::xchacha20; + + switch (enc_type_) { + case ENCRYPT_TYPE::ENCRYPT_TYPE_AES_GCM: + enc_type = session::onionreq::EncryptType::aes_gcm; + break; + + case ENCRYPT_TYPE::ENCRYPT_TYPE_X_CHA_CHA_20: + enc_type = session::onionreq::EncryptType::xchacha20; + break; + + default: + throw std::runtime_error{"Invalid decryption type " + std::to_string(enc_type_)}; + } + + session::onionreq::HopEncryption d{ + session::onionreq::x25519_seckey::from_bytes({final_x25519_seckey, 32}), + session::onionreq::x25519_pubkey::from_bytes({final_x25519_pubkey, 32}), + false}; + + ustring result; + + // FIXME: The legacy PN server doesn't support 'xchacha20' onion requests so would return an + // error encrypted with 'aes_gcm' so try to decrypt in case that is what happened - this + // workaround can be removed once the legacy PN server is removed + try { + result = d.decrypt( + enc_type, + ustring{ciphertext, ciphertext_len}, + session::onionreq::x25519_pubkey::from_bytes({destination_x25519_pubkey, 32})); + } catch (...) { + if (enc_type == session::onionreq::EncryptType::xchacha20) + result = d.decrypt( + session::onionreq::EncryptType::aes_gcm, + ustring{ciphertext, ciphertext_len}, + session::onionreq::x25519_pubkey::from_bytes( + {destination_x25519_pubkey, 32})); + else + return false; + } + + *plaintext_out = static_cast(malloc(result.size())); + *plaintext_out_len = result.size(); + std::memcpy(*plaintext_out, result.data(), result.size()); + return true; + } catch (...) { + return false; + } +} +} diff --git a/src/random.cpp b/src/random.cpp new file mode 100644 index 00000000..693dd489 --- /dev/null +++ b/src/random.cpp @@ -0,0 +1,29 @@ +#include "session/random.hpp" + +#include + +#include "session/export.h" +#include "session/util.hpp" + +namespace session::random { + +ustring random(size_t size) { + ustring result; + result.resize(size); + randombytes_buf(result.data(), size); + + return result; +} + +} // namespace session::random + +extern "C" { + +LIBSESSION_C_API unsigned char* session_random(size_t size) { + auto result = session::random::random(size); + auto* ret = static_cast(malloc(size)); + std::memcpy(ret, result.data(), result.size()); + return ret; +} + +} // extern "C" diff --git a/src/session_encrypt.cpp b/src/session_encrypt.cpp index 1e8c0d8a..88bc4e7a 100644 --- a/src/session_encrypt.cpp +++ b/src/session_encrypt.cpp @@ -1,28 +1,31 @@ #include "session/session_encrypt.hpp" +#include +#include +#include #include +#include #include #include +#include #include +#include #include #include #include #include +#include "session/blinding.hpp" #include "session/util.hpp" using namespace std::literals; namespace session { -template -using cleared_array = sodium_cleared>; - -using uc32 = std::array; -using uc64 = std::array; -using cleared_uc32 = cleared_array<32>; -using cleared_uc64 = cleared_array<64>; +// Version tag we prepend to encrypted-for-blinded-user messages. This is here so we can detect if +// some future version changes the format (and if not even try to load it). +inline constexpr unsigned char BLINDED_ENCRYPT_VERSION = 0; ustring sign_for_recipient( ustring_view ed25519_privkey, ustring_view recipient_pubkey, ustring_view message) { @@ -133,9 +136,217 @@ ustring encrypt_for_recipient_deterministic( return result; } -std::pair decrypt_incoming( +// Calculate the shared encryption key, sending from blinded sender kS (k = S's blinding factor) to +// blinded receiver jR (j = R's blinding factor). +// +// The sender knows s, k, S, and jR, but not j/R individually. +// The receiver knows r, j, R, and kS, but not k/S individually. +// +// From the sender's perspective, then, we compute: +// +// BLAKE2b(s k[jR] || kS || [jR]) +// +// The receiver can calulate the same value via: +// +// BLAKE2b(r j[kS] || [kS] || jR) +// +// (which will be the same because sR = rS, and so skjR = kjsR = kjrS = rjkS). +// +// For 15 blinding, however, the blinding factor depended only on the SOGS server pubkey, and so `j +// = k`, and so for *15* keys we don't do the double-blinding (i.e. the first terms above drop the +// double-blinding factors and become just sjR and rkS). +// +// Arguments. "A" and "B" here are either sender and receiver, or receiver and sender, depending on +// the value of `sending`. +// +// seed -- A's 32-byte secret key (can also be 64 bytes; only the first 32 are used). +// kA -- A's 33-byte blinded id, beginning with 0x15 or 0x25 +// jB -- A's 33-byte blinded id, beginning with 0x15 or 0x25 (must be the same prefix as kA). +// server_pk -- the server's pubkey (needed to compute A's `k` value) +// sending -- true if this for a message from A to B, false if this is from B to A. +static cleared_uc32 blinded_shared_secret( + ustring_view seed, ustring_view kA, ustring_view jB, ustring_view server_pk, bool sending) { + + // Because we're doing this generically, we use notation a/A/k for ourselves and b/jB for the + // other person; this notion keeps everything exactly as above *except* for the concatenation in + // the BLAKE2b hashed value: there we have to use kA || jB if we are the sender, but reverse the + // order to jB || kA if we are the receiver. + + std::pair blinded_key_pair; + cleared_uc32 k; + + if (seed.size() != 64 && seed.size() != 32) + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + if (server_pk.size() != 32) + throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; + if (kA.size() != 33) + throw std::invalid_argument{"Invalid local blinded id: expected 33 bytes"}; + if (jB.size() != 33) + throw std::invalid_argument{"Invalid remote blinded id: expected 33 bytes"}; + if (kA[0] == 0x15 && jB[0] == 0x15) + blinded_key_pair = blind15_key_pair(seed, server_pk, &k); + else if (kA[0] == 0x25 && jB[0] == 0x25) + blinded_key_pair = blind25_key_pair(seed, server_pk, &k); + else + throw std::invalid_argument{"Both ids must start with the same 0x15 or 0x25 prefix"}; + + bool blind25 = kA[0] == 0x25; + + kA.remove_prefix(1); + jB.remove_prefix(1); + + cleared_uc32 ka; + // Not really switching to x25519 here, this is just an easy way to compute `a` + crypto_sign_ed25519_sk_to_curve25519(ka.data(), seed.data()); + + if (blind25) + // Multiply a by k, so that we end up computing kajB = kjaB, which the other side can + // compute as jkbA. + crypto_core_ed25519_scalar_mul(ka.data(), ka.data(), k.data()); + // Else for 15 blinding we leave "ka" as just a, because j=k and so we don't need the + // double-blind. + + cleared_uc32 shared_secret; + if (0 != crypto_scalarmult_ed25519_noclamp(shared_secret.data(), ka.data(), jB.data())) + throw std::runtime_error{"Shared secret generation failed"}; + + auto& sender = sending ? kA : jB; + auto& recipient = sending ? jB : kA; + + // H(kjsR || kS || jR): + crypto_generichash_blake2b_state st; + crypto_generichash_blake2b_init(&st, nullptr, 0, 32); + crypto_generichash_blake2b_update(&st, shared_secret.data(), shared_secret.size()); + crypto_generichash_blake2b_update(&st, sender.data(), sender.size()); + crypto_generichash_blake2b_update(&st, recipient.data(), recipient.size()); + crypto_generichash_blake2b_final(&st, shared_secret.data(), shared_secret.size()); + + return shared_secret; +} + +ustring encrypt_for_blinded_recipient( + ustring_view ed25519_privkey, + ustring_view server_pk, + ustring_view recipient_blinded_id, + ustring_view message) { + if (ed25519_privkey.size() != 64 && ed25519_privkey.size() != 32) + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + if (server_pk.size() != 32) + throw std::invalid_argument{"Invalid server_pk: expected 32 bytes"}; + if (recipient_blinded_id.size() != 33) + throw std::invalid_argument{"Invalid recipient_blinded_id: expected 33 bytes"}; + + // Generate the blinded key pair & shared encryption key + std::pair blinded_key_pair; + switch (recipient_blinded_id[0]) { + case 0x15: blinded_key_pair = blind15_key_pair(ed25519_privkey, server_pk); break; + + case 0x25: blinded_key_pair = blind25_key_pair(ed25519_privkey, server_pk); break; + + default: + throw std::invalid_argument{ + "Invalid recipient_blinded_id: must start with 0x15 or 0x25"}; + } + ustring blinded_id; + blinded_id.reserve(33); + blinded_id += recipient_blinded_id[0]; + blinded_id.append(blinded_key_pair.first.begin(), blinded_key_pair.first.end()); + + auto enc_key = blinded_shared_secret( + ed25519_privkey, blinded_id, recipient_blinded_id, server_pk, true); + + // Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey) + ustring buf; + buf.reserve(message.size() + 32); + buf += message; + + // append A (pubkey) + if (ed25519_privkey.size() == 64) { + buf += ed25519_privkey.substr(32); + } else { + cleared_uc64 ed_sk_from_seed; + uc32 ed_pk_buf; + crypto_sign_ed25519_seed_keypair( + ed_pk_buf.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + buf += to_sv(ed_pk_buf); + } + + // Encrypt using xchacha20-poly1305 + cleared_array nonce; + randombytes_buf(nonce.data(), nonce.size()); + + ustring ciphertext; + unsigned long long outlen = 0; + ciphertext.resize( + 1 + buf.size() + crypto_aead_xchacha20poly1305_ietf_ABYTES + + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + // Prepend with a version byte, so that the recipient can reliably detect if a future version is + // no longer encrypting things the way it expects. + ciphertext[0] = BLINDED_ENCRYPT_VERSION; + + if (0 != crypto_aead_xchacha20poly1305_ietf_encrypt( + ciphertext.data() + 1, + &outlen, + buf.data(), + buf.size(), + nullptr, + 0, + nullptr, + nonce.data(), + enc_key.data())) + throw std::runtime_error{"Crypto aead encryption failed"}; + + assert(outlen == ciphertext.size() - 1 - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + // append the nonce, so that we have: data = b'\x00' + ciphertext + nonce + std::memcpy(ciphertext.data() + (1 + outlen), nonce.data(), nonce.size()); + + return ciphertext; +} + +std::pair decrypt_incoming_session_id( ustring_view ed25519_privkey, ustring_view ciphertext) { + auto [buf, sender_ed_pk] = decrypt_incoming(ed25519_privkey, ciphertext); + + // Convert the sender_ed_pk to the sender's session ID + std::array sender_x_pk; + + if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) + throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + + // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to + // the sender session ID + std::string sender_session_id; + sender_session_id.reserve(66); + sender_session_id += "05"; + oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); + return {buf, sender_session_id}; +} + +std::pair decrypt_incoming_session_id( + ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext) { + auto [buf, sender_ed_pk] = decrypt_incoming(x25519_pubkey, x25519_seckey, ciphertext); + + // Convert the sender_ed_pk to the sender's session ID + std::array sender_x_pk; + + if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) + throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + + // Everything is good, so just drop A and Y off the message and prepend the '05' prefix to + // the sender session ID + std::string sender_session_id; + sender_session_id.reserve(66); + sender_session_id += "05"; + oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); + + return {buf, sender_session_id}; +} + +std::pair decrypt_incoming( + ustring_view ed25519_privkey, ustring_view ciphertext) { cleared_uc64 ed_sk_from_seed; if (ed25519_privkey.size() == 32) { uc32 ignore_pk; @@ -146,29 +357,39 @@ std::pair decrypt_incoming( throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; } - if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) - throw std::runtime_error{"Invalid incoming message: ciphertext is too small"}; - const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; - const size_t msg_size = outer_size - 32 - 64; - cleared_uc32 x_sec; uc32 x_pub; crypto_sign_ed25519_sk_to_curve25519(x_sec.data(), ed25519_privkey.data()); crypto_scalarmult_base(x_pub.data(), x_sec.data()); + return decrypt_incoming({x_pub.data(), 32}, {x_sec.data(), 32}, ciphertext); +} + +std::pair decrypt_incoming( + ustring_view x25519_pubkey, ustring_view x25519_seckey, ustring_view ciphertext) { + + if (ciphertext.size() < crypto_box_SEALBYTES + 32 + 64) + throw std::runtime_error{"Invalid incoming message: ciphertext is too small"}; + const size_t outer_size = ciphertext.size() - crypto_box_SEALBYTES; + const size_t msg_size = outer_size - 32 - 64; + std::pair result; auto& [buf, sender_ed_pk] = result; buf.resize(outer_size); if (0 != crypto_box_seal_open( - buf.data(), ciphertext.data(), ciphertext.size(), x_pub.data(), x_sec.data())) + buf.data(), + ciphertext.data(), + ciphertext.size(), + x25519_pubkey.data(), + x25519_seckey.data())) throw std::runtime_error{"Decryption failed"}; uc64 sig; sender_ed_pk = buf.substr(msg_size, 32); std::memcpy(sig.data(), buf.data() + msg_size + 32, 64); buf.resize(buf.size() - 64); // Remove SIG, then append Y so that we get M||A||Y to verify - buf += ustring_view{x_pub.data(), 32}; + buf += ustring_view{x25519_pubkey.data(), 32}; if (0 != crypto_sign_ed25519_verify_detached( sig.data(), buf.data(), buf.size(), sender_ed_pk.data())) @@ -180,4 +401,356 @@ std::pair decrypt_incoming( return result; } +std::pair decrypt_from_blinded_recipient( + ustring_view ed25519_privkey, + ustring_view server_pk, + ustring_view sender_id, + ustring_view recipient_id, + ustring_view ciphertext) { + uc32 ed_pk_from_seed; + cleared_uc64 ed_sk_from_seed; + if (ed25519_privkey.size() == 32) { + crypto_sign_ed25519_seed_keypair( + ed_pk_from_seed.data(), ed_sk_from_seed.data(), ed25519_privkey.data()); + ed25519_privkey = {ed_sk_from_seed.data(), ed_sk_from_seed.size()}; + } else if (ed25519_privkey.size() == 64) + std::memcpy(ed_pk_from_seed.data(), ed25519_privkey.data() + 32, 32); + else + throw std::invalid_argument{"Invalid ed25519_privkey: expected 32 or 64 bytes"}; + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + 1 + + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::invalid_argument{ + "Invalid ciphertext: too short to contain valid encrypted data"}; + + cleared_uc32 dec_key; + auto blinded_id = recipient_id[0] == 0x25 + ? blinded25_id_from_ed(to_sv(ed_pk_from_seed), server_pk) + : blinded15_id_from_ed(to_sv(ed_pk_from_seed), server_pk); + + if (sender_id == blinded_id) + dec_key = blinded_shared_secret(ed25519_privkey, sender_id, recipient_id, server_pk, true); + else + dec_key = blinded_shared_secret(ed25519_privkey, recipient_id, sender_id, server_pk, false); + + std::pair result; + auto& [buf, sender_session_id] = result; + + // v, ct, nc = data[0], data[1:-24], data[-24:] + if (ciphertext[0] != BLINDED_ENCRYPT_VERSION) + throw std::invalid_argument{ + "Invalid ciphertext: version is not " + std::to_string(BLINDED_ENCRYPT_VERSION)}; + + ustring nonce; + const size_t msg_size = + (ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - 1 - + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + if (msg_size < 32) + throw std::invalid_argument{"Invalid ciphertext: innerBytes too short"}; + buf.resize(msg_size); + + unsigned long long buf_len = 0; + + nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + std::memcpy( + nonce.data(), + ciphertext.data() + msg_size + 1 + crypto_aead_xchacha20poly1305_ietf_ABYTES, + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + buf.data(), + &buf_len, + nullptr, + ciphertext.data() + 1, + msg_size + crypto_aead_xchacha20poly1305_ietf_ABYTES, + nullptr, + 0, + nonce.data(), + dec_key.data())) + throw std::invalid_argument{"Decryption failed"}; + + assert(buf_len == buf.size()); + + // Split up: the last 32 bytes are the sender's *unblinded* ed25519 key + uc32 sender_ed_pk; + std::memcpy(sender_ed_pk.data(), buf.data() + (buf.size() - 32), 32); + + // Convert the sender_ed_pk to the sender's session ID + uc32 sender_x_pk; + if (0 != crypto_sign_ed25519_pk_to_curve25519(sender_x_pk.data(), sender_ed_pk.data())) + throw std::runtime_error{"Sender ed25519 pubkey to x25519 pubkey conversion failed"}; + + ustring session_id; // Gets populated by the following ..._from_ed calls + + // Verify that the inner sender_ed_pk (A) yields the same outer kA we got with the message + auto extracted_sender = + recipient_id[0] == 0x25 + ? blinded25_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id) + : blinded15_id_from_ed(to_sv(sender_ed_pk), server_pk, &session_id); + + bool matched = sender_id == extracted_sender; + if (!matched && extracted_sender[0] == 0x15) { + // With 15-blinding we might need the negative instead: + extracted_sender[31] ^= 0x80; + matched = sender_id == extracted_sender; + } + if (!matched) + throw std::runtime_error{"Blinded sender id does not match the actual sender"}; + + // Everything is good, so just drop the sender_ed_pk off the message and prepend the '05' prefix + // to the sender session ID + buf.resize(buf.size() - 32); + sender_session_id.reserve(66); + sender_session_id += "05"; + oxenc::to_hex(sender_x_pk.begin(), sender_x_pk.end(), std::back_inserter(sender_session_id)); + + return result; +} + +std::string decrypt_ons_response( + std::string_view lowercase_name, ustring_view ciphertext, ustring_view nonce) { + if (ciphertext.size() < crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::invalid_argument{"Invalid ciphertext: expected to be greater than 16 bytes"}; + if (nonce.size() != crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) + throw std::invalid_argument{"Invalid nonce: expected to be 24 bytes"}; + + // Hash the ONS name using BLAKE2b + // + // xchacha-based encryption + // key = H(name, key=H(name)) + uc32 key; + uc32 name_hash; + auto name_bytes = to_unsigned(lowercase_name.data()); + crypto_generichash_blake2b( + name_hash.data(), name_hash.size(), name_bytes, lowercase_name.size(), nullptr, 0); + crypto_generichash_blake2b( + key.data(), + key.size(), + name_bytes, + lowercase_name.size(), + name_hash.data(), + name_hash.size()); + + ustring buf; + unsigned long long buf_len = 0; + buf.resize(ciphertext.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + buf.data(), + &buf_len, + nullptr, + ciphertext.data(), + ciphertext.size(), + nullptr, + 0, + nonce.data(), + key.data())) + throw std::runtime_error{"Failed to decrypt"}; + + if (buf_len != 33) + throw std::runtime_error{"Invalid decrypted value: expected to be 33 bytes"}; + + std::string session_id = oxenc::to_hex(buf.begin(), buf.end()); + return session_id; +} + +ustring decrypt_push_notification(ustring_view payload, ustring_view enc_key) { + if (payload.size() < + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES + crypto_aead_xchacha20poly1305_ietf_ABYTES) + throw std::invalid_argument{"Invalid payload: too short to contain valid encrypted data"}; + if (enc_key.size() != 32) + throw std::invalid_argument{"Invalid enc_key: expected 32 bytes"}; + + ustring buf; + ustring nonce; + const size_t msg_size = + (payload.size() - crypto_aead_xchacha20poly1305_ietf_ABYTES - + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + unsigned long long buf_len = 0; + buf.resize(msg_size); + nonce.resize(crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + std::memcpy(nonce.data(), payload.data(), crypto_aead_xchacha20poly1305_ietf_NPUBBYTES); + + if (0 != crypto_aead_xchacha20poly1305_ietf_decrypt( + buf.data(), + &buf_len, + nullptr, + payload.data() + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + payload.size() - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + nullptr, + 0, + nonce.data(), + enc_key.data())) + throw std::runtime_error{"Failed to decrypt; perhaps the secret key is invalid?"}; + + // Removing any null padding bytes from the end + if (auto pos = buf.find_last_not_of('\0'); pos != std::string::npos) + buf.resize(pos + 1); + + return buf; +} + } // namespace session + +using namespace session; + +LIBSESSION_C_API bool session_encrypt_for_recipient_deterministic( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* ed25519_privkey, + const unsigned char* recipient_pubkey, + unsigned char** ciphertext_out, + size_t* ciphertext_len) { + try { + auto ciphertext = session::encrypt_for_recipient_deterministic( + ustring_view{ed25519_privkey, 64}, + ustring_view{recipient_pubkey, 32}, + ustring_view{plaintext_in, plaintext_len}); + + *ciphertext_out = static_cast(malloc(ciphertext.size())); + *ciphertext_len = ciphertext.size(); + std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_encrypt_for_blinded_recipient( + const unsigned char* plaintext_in, + size_t plaintext_len, + const unsigned char* ed25519_privkey, + const unsigned char* open_group_pubkey, + const unsigned char* recipient_blinded_id, + unsigned char** ciphertext_out, + size_t* ciphertext_len) { + try { + auto ciphertext = session::encrypt_for_blinded_recipient( + ustring_view{ed25519_privkey, 64}, + ustring_view{open_group_pubkey, 32}, + ustring_view{recipient_blinded_id, 33}, + ustring_view{plaintext_in, plaintext_len}); + + *ciphertext_out = static_cast(malloc(ciphertext.size())); + *ciphertext_len = ciphertext.size(); + std::memcpy(*ciphertext_out, ciphertext.data(), ciphertext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_incoming( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* ed25519_privkey, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len) { + try { + auto result = session::decrypt_incoming_session_id( + ustring_view{ed25519_privkey, 64}, ustring_view{ciphertext_in, ciphertext_len}); + auto [plaintext, session_id] = result; + + std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_incoming_legacy_group( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* x25519_pubkey, + const unsigned char* x25519_seckey, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len) { + try { + auto result = session::decrypt_incoming_session_id( + ustring_view{x25519_pubkey, 32}, + ustring_view{x25519_seckey, 32}, + ustring_view{ciphertext_in, ciphertext_len}); + auto [plaintext, session_id] = result; + + std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_for_blinded_recipient( + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* ed25519_privkey, + const unsigned char* open_group_pubkey, + const unsigned char* sender_id, + const unsigned char* recipient_id, + char* session_id_out, + unsigned char** plaintext_out, + size_t* plaintext_len) { + try { + auto result = session::decrypt_from_blinded_recipient( + ustring_view{ed25519_privkey, 64}, + ustring_view{open_group_pubkey, 32}, + ustring_view{sender_id, 33}, + ustring_view{recipient_id, 33}, + ustring_view{ciphertext_in, ciphertext_len}); + auto [plaintext, session_id] = result; + + std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_ons_response( + const char* name_in, + size_t name_len, + const unsigned char* ciphertext_in, + size_t ciphertext_len, + const unsigned char* nonce_in, + char* session_id_out) { + try { + auto session_id = session::decrypt_ons_response( + std::string_view{name_in, name_len}, + ustring_view{ciphertext_in, ciphertext_len}, + ustring_view{nonce_in, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES}); + + std::memcpy(session_id_out, session_id.c_str(), session_id.size() + 1); + return true; + } catch (...) { + return false; + } +} + +LIBSESSION_C_API bool session_decrypt_push_notification( + const unsigned char* payload_in, + size_t payload_len, + const unsigned char* enc_key_in, + unsigned char** plaintext_out, + size_t* plaintext_len) { + try { + auto plaintext = session::decrypt_push_notification( + ustring_view{payload_in, payload_len}, ustring_view{enc_key_in, 32}); + + *plaintext_out = static_cast(malloc(plaintext.size())); + *plaintext_len = plaintext.size(); + std::memcpy(*plaintext_out, plaintext.data(), plaintext.size()); + return true; + } catch (...) { + return false; + } +} diff --git a/src/xed25519.cpp b/src/xed25519.cpp index e885ff1a..a311e258 100644 --- a/src/xed25519.cpp +++ b/src/xed25519.cpp @@ -12,6 +12,7 @@ #include #include "session/export.h" +#include "session/util.hpp" namespace session::xed25519 { @@ -155,39 +156,39 @@ using session::xed25519::ustring_view; extern "C" { -LIBSESSION_EXPORT int session_xed25519_sign( +LIBSESSION_C_API bool session_xed25519_sign( unsigned char* signature, const unsigned char* curve25519_privkey, const unsigned char* msg, - const unsigned int msg_len) { + size_t msg_len) { assert(signature != NULL); try { auto sig = session::xed25519::sign({curve25519_privkey, 32}, {msg, msg_len}); std::memcpy(signature, sig.data(), sig.size()); - return 0; + return true; } catch (...) { + return false; } - return 1; } -LIBSESSION_EXPORT int session_xed25519_verify( +LIBSESSION_C_API bool session_xed25519_verify( const unsigned char* signature, const unsigned char* pubkey, const unsigned char* msg, - const unsigned int msg_len) { - return session::xed25519::verify({signature, 64}, {pubkey, 32}, {msg, msg_len}) ? 0 : 1; + size_t msg_len) { + return session::xed25519::verify({signature, 64}, {pubkey, 32}, {msg, msg_len}); } -LIBSESSION_EXPORT int session_xed25519_pubkey( +LIBSESSION_C_API bool session_xed25519_pubkey( unsigned char* ed25519_pubkey, const unsigned char* curve25519_pubkey) { assert(ed25519_pubkey != NULL); try { auto edpk = session::xed25519::pubkey({curve25519_pubkey, 32}); std::memcpy(ed25519_pubkey, edpk.data(), edpk.size()); - return 0; + return true; } catch (...) { + return false; } - return 1; } } // extern "C" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8a994202..a6a1d127 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,13 +10,17 @@ add_executable(testAll test_configdata.cpp test_config_contacts.cpp test_config_convo_info_volatile.cpp + test_curve25519.cpp + test_ed25519.cpp test_encrypt.cpp test_group_keys.cpp test_group_info.cpp test_group_members.cpp + test_hash.cpp test_multi_encrypt.cpp test_onionreq.cpp test_proto.cpp + test_random.cpp test_session_encrypt.cpp test_xed25519.cpp ) diff --git a/tests/test_blinding.cpp b/tests/test_blinding.cpp index b8d8bd51..0b347735 100644 --- a/tests/test_blinding.cpp +++ b/tests/test_blinding.cpp @@ -162,3 +162,153 @@ TEST_CASE("Communities 25xxx-blinded signing", "[blinding25][sign]") { 4, to_unsigned(oxenc::from_hex(b25_6).data()) + 1)); } + +TEST_CASE("Communities 15xxx-blinded pubkey derivation", "[blinding15][pubkey]") { + REQUIRE(sodium_init() >= 0); + + ustring session_id1_raw, session_id2_raw; + oxenc::from_hex(session_id1.begin(), session_id1.end(), std::back_inserter(session_id1_raw)); + oxenc::from_hex(session_id2.begin(), session_id2.end(), std::back_inserter(session_id2_raw)); + CHECK(oxenc::to_hex(blind15_id( + session_id1_raw, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + "15b74ed205f1f931e1bb1291183778a9456b835937d923b0f2e248aa3a44c07844"); + CHECK(oxenc::to_hex(blind15_id( + session_id2_raw, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + "1561e070286ff7a71f167e92b18c709882b148d8238c8872caf414b301ba0564fd"); + CHECK(oxenc::to_hex(blind15_id( + session_id1_raw.substr(1), + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"_hexbytes)) == + "15b74ed205f1f931e1bb1291183778a9456b835937d923b0f2e248aa3a44c07844"); +} + +TEST_CASE("Communities 15xxx-blinded signing", "[blinding15][sign]") { + REQUIRE(sodium_init() >= 0); + + std::array server_pks = { + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv}; + auto b15_1 = blind15_id(session_id1, server_pks[0])[0]; + auto b15_2 = blind15_id(session_id1, server_pks[1])[0]; + // session_id2 has a negative pubkey, so these next three need the negative [1] instead: + auto b15_3 = blind15_id(session_id2, server_pks[2])[1]; + auto b15_4 = blind15_id(session_id2, server_pks[3])[1]; + auto b15_5 = blind15_id(session_id2, server_pks[4])[1]; + auto b15_6 = blind15_id(session_id1, server_pks[5])[0]; + + auto sig1 = blind15_sign(to_usv(seed1), server_pks[0], to_unsigned_sv("hello")); + CHECK(oxenc::to_hex(sig1) == + "1a5ade20b43af0e16b3e591d6f86303938d7557c0ac54469dd4f5aea759f82d22cafa42587251756e133acdd" + "dd8cbec2f707a9ce09a49f2193f46a91502c5006"); + CHECK(0 == crypto_sign_verify_detached( + sig1.data(), + to_unsigned("hello"), + 5, + to_unsigned(oxenc::from_hex(b15_1).data()) + 1)); + + auto sig2 = blind15_sign(to_usv(seed1), server_pks[1], to_unsigned_sv("world")); + CHECK(oxenc::to_hex(sig2) == + "d357f74c5ec5536840aec575051f71fdb22d70f35ef31db1715f5f694842de3b39aa647c84aa8e28ec56eb76" + "2d237c9e030639c83f429826d419ac719cd4df03"); + CHECK(0 == crypto_sign_verify_detached( + sig2.data(), + to_unsigned("world"), + 5, + to_unsigned(oxenc::from_hex(b15_2).data()) + 1)); + + auto sig3 = blind15_sign(to_usv(seed2), server_pks[2], to_unsigned_sv("this")); + CHECK(oxenc::to_hex(sig3) == + "dacf91dfb411e99cd8ef4cb07b195b49289cf1a724fef122c73462818560bc29832a98d870ec4feb79dedca5" + "b59aba6a466d3ce8f3e35adf25a1813f6989fd0a"); + CHECK(0 == crypto_sign_verify_detached( + sig3.data(), + to_unsigned("this"), + 4, + to_unsigned(oxenc::from_hex(b15_3).data()) + 1)); + + auto sig4 = blind15_sign(to_usv(seed2), server_pks[3], to_unsigned_sv("is")); + CHECK(oxenc::to_hex(sig4) == + "8339ea9887d3e44131e33403df160539cdc7a0a8107772172c311e95773660a0d39ed0a6c2b2c794dde1fdc6" + "40943e403497aa02c4d1a21a7d9030742beabb05"); + CHECK(0 == crypto_sign_verify_detached( + sig4.data(), + to_unsigned("is"), + 2, + to_unsigned(oxenc::from_hex(b15_4).data()) + 1)); + + auto sig5 = blind15_sign(to_usv(seed2), server_pks[4], to_unsigned_sv("")); + CHECK(oxenc::to_hex(sig5) == + "8b0d6447decff3a21ec1809141580139c4a51e24977b0605fe7984439993f5377ebc9681e4962593108d03cc" + "8b6873c5c5ba8c30287188137d2dee9ab10afd0f"); + CHECK(0 == + crypto_sign_verify_detached( + sig5.data(), to_unsigned(""), 0, to_unsigned(oxenc::from_hex(b15_5).data()) + 1)); + + auto sig6 = blind15_sign(to_usv(seed1), server_pks[5], to_unsigned_sv("omg!")); + CHECK(oxenc::to_hex(sig6) == + "946725055399376ecebb605c79f845fbf689a47f98507c2a1f239516fd9c9104e19fe533631c27ba4e744457" + "4f0e4f0f0d422b7256ed63681a3ab2fe7e040601"); + CHECK(0 == crypto_sign_verify_detached( + sig6.data(), + to_unsigned("omg!"), + 4, + to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); + + // Test that it works when given just the seed instead of the whole sk: + auto sig6b = blind15_sign(to_usv(seed1).substr(0, 32), server_pks[5], to_unsigned_sv("omg!")); + CHECK(oxenc::to_hex(sig6b) == + "946725055399376ecebb605c79f845fbf689a47f98507c2a1f239516fd9c9104e19fe533631c27ba4e744457" + "4f0e4f0f0d422b7256ed63681a3ab2fe7e040601"); + CHECK(0 == crypto_sign_verify_detached( + sig6b.data(), + to_unsigned("omg!"), + 4, + to_unsigned(oxenc::from_hex(b15_6).data()) + 1)); +} + +TEST_CASE("Communities session id blinded id matching", "[blinding][matching]") { + std::array server_pks = { + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "00cdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "999def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "888def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv, + "777def0123456789abcdef0123456789abcdef0123456789abcdef0123456789"sv}; + auto b15_1 = blind15_id(session_id1, server_pks[0])[0]; + auto b15_2 = blind15_id(session_id1, server_pks[1])[0]; + auto b15_3 = blind15_id(session_id2, server_pks[2])[1]; + auto b15_4 = blind15_id(session_id2, server_pks[3])[1]; + auto b15_5 = blind15_id(session_id2, server_pks[4])[1]; + auto b15_6 = blind15_id(session_id1, server_pks[5])[0]; + auto b25_1 = blind25_id(session_id1, server_pks[0]); + auto b25_2 = blind25_id(session_id1, server_pks[1]); + auto b25_3 = blind25_id(session_id2, server_pks[2]); + auto b25_4 = blind25_id(session_id2, server_pks[3]); + auto b25_5 = blind25_id(session_id2, server_pks[4]); + auto b25_6 = blind25_id(session_id1, server_pks[5]); + + CHECK(session_id_matches_blinded_id(session_id1, b15_1, server_pks[0])); + CHECK(session_id_matches_blinded_id(session_id1, b15_2, server_pks[1])); + CHECK(session_id_matches_blinded_id(session_id2, b15_3, server_pks[2])); + CHECK(session_id_matches_blinded_id(session_id2, b15_4, server_pks[3])); + CHECK(session_id_matches_blinded_id(session_id2, b15_5, server_pks[4])); + CHECK(session_id_matches_blinded_id(session_id1, b15_6, server_pks[5])); + CHECK(session_id_matches_blinded_id(session_id1, b25_1, server_pks[0])); + CHECK(session_id_matches_blinded_id(session_id1, b25_2, server_pks[1])); + CHECK(session_id_matches_blinded_id(session_id2, b25_3, server_pks[2])); + CHECK(session_id_matches_blinded_id(session_id2, b25_4, server_pks[3])); + CHECK(session_id_matches_blinded_id(session_id2, b25_5, server_pks[4])); + CHECK(session_id_matches_blinded_id(session_id1, b25_6, server_pks[5])); + + auto invalid_session_id = "9" + session_id1.substr(1, 65); + auto invalid_blinded_id = "9" + b15_1.substr(1, 65); + auto invalid_server_pk = server_pks[0].substr(0, 60); + CHECK_THROWS(session_id_matches_blinded_id(invalid_session_id, b15_1, server_pks[0])); + CHECK_THROWS(session_id_matches_blinded_id(session_id1, invalid_blinded_id, server_pks[0])); + CHECK_THROWS(session_id_matches_blinded_id(session_id1, invalid_blinded_id, invalid_server_pk)); +} diff --git a/tests/test_curve25519.cpp b/tests/test_curve25519.cpp new file mode 100644 index 00000000..3acf6aa4 --- /dev/null +++ b/tests/test_curve25519.cpp @@ -0,0 +1,51 @@ +#include + +#include +#include + +#include "session/curve25519.h" +#include "session/curve25519.hpp" +#include "utils.hpp" + +TEST_CASE("X25519 key pair generation", "[curve25519][keypair]") { + auto kp1 = session::curve25519::curve25519_key_pair(); + auto kp2 = session::curve25519::curve25519_key_pair(); + + CHECK(kp1.first.size() == 32); + CHECK(kp1.second.size() == 64); + CHECK(kp1.first != kp2.first); + CHECK(kp1.second != kp2.second); +} + +TEST_CASE("X25519 conversion", "[curve25519][to curve25519 pubkey]") { + using namespace session; + + auto ed_pk1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto ed_pk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; + + auto x_pk1 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk1)); + auto x_pk2 = curve25519::to_curve25519_pubkey(to_unsigned_sv(ed_pk2)); + + CHECK(oxenc::to_hex(x_pk1.begin(), x_pk1.end()) == + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK(oxenc::to_hex(x_pk2.begin(), x_pk2.end()) == + "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); +} + +TEST_CASE("X25519 conversion", "[curve25519][to curve25519 seckey]") { + using namespace session; + + auto ed_sk1 = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto ed_sk2 = + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" + "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"_hexbytes; + auto x_sk1 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk1)); + auto x_sk2 = curve25519::to_curve25519_seckey(to_unsigned_sv(ed_sk2)); + + CHECK(oxenc::to_hex(x_sk1.begin(), x_sk1.end()) == + "207e5d97e761300f96c10adc11efdd6d5c15188a9a7682ec05b30ca017e9b447"); + CHECK(oxenc::to_hex(x_sk2.begin(), x_sk2.end()) == + "904943eff27142a8e5cd37c84e2437c9979a560b044bf9a65a8d644b325fe56a"); +} diff --git a/tests/test_ed25519.cpp b/tests/test_ed25519.cpp new file mode 100644 index 00000000..31ecb520 --- /dev/null +++ b/tests/test_ed25519.cpp @@ -0,0 +1,88 @@ +#include + +#include +#include + +#include "session/ed25519.h" +#include "session/ed25519.hpp" +#include "utils.hpp" + +TEST_CASE("Ed25519 key pair generation", "[ed25519][keypair]") { + // Generate two random key pairs and make sure they don't match + auto kp1 = session::ed25519::ed25519_key_pair(); + auto kp2 = session::ed25519::ed25519_key_pair(); + + CHECK(kp1.first.size() == 32); + CHECK(kp1.second.size() == 64); + CHECK(kp1.first != kp2.first); + CHECK(kp1.second != kp2.second); +} + +TEST_CASE("Ed25519 key pair generation seed", "[ed25519][keypair]") { + using namespace session; + + auto ed_seed1 = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto ed_seed2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; + auto ed_seed_invalid = "010203040506070809"_hexbytes; + + auto kp1 = session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed1)); + auto kp2 = session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed2)); + CHECK_THROWS(session::ed25519::ed25519_key_pair(to_unsigned_sv(ed_seed_invalid))); + + CHECK(kp1.first.size() == 32); + CHECK(kp1.second.size() == 64); + CHECK(kp1.first != kp2.first); + CHECK(kp1.second != kp2.second); + CHECK(oxenc::to_hex(kp1.first.begin(), kp1.first.end()) == + "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"); + CHECK(oxenc::to_hex(kp2.first.begin(), kp2.first.end()) == + "cd83ca3d13ad8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"); + + auto kp_sk1 = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"; + auto kp_sk2 = + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876cd83ca3d13a" + "d8a954d5011aa7861abe3a29ac25b70c4ed5234aff74d34ef5786"; + CHECK(oxenc::to_hex(kp1.second.begin(), kp1.second.end()) == kp_sk1); + CHECK(oxenc::to_hex(kp2.second.begin(), kp2.second.end()) == kp_sk2); +} + +TEST_CASE("Ed25519 seed for private key", "[ed25519][seed]") { + using namespace session; + + auto ed_sk1 = + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab78862834829a" + "87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto ed_sk2 = "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"_hexbytes; + auto ed_sk_invalid = "010203040506070809"_hexbytes; + + auto seed1 = session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk1)); + auto seed2 = session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk2)); + CHECK_THROWS(session::ed25519::seed_for_ed_privkey(to_unsigned_sv(ed_sk_invalid))); + + CHECK(oxenc::to_hex(seed1.begin(), seed1.end()) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + CHECK(oxenc::to_hex(seed2.begin(), seed2.end()) == + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); +} + +TEST_CASE("Ed25519", "[ed25519][signature]") { + using namespace session; + + auto ed_seed = "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"_hexbytes; + auto ed_pk = "8862834829a87e0afadfed763fa8785e893dbde7f2c001ff1071aa55005c347f"_hexbytes; + auto ed_invalid = "010203040506070809"_hexbytes; + + auto sig1 = session::ed25519::sign(to_unsigned_sv(ed_seed), to_unsigned_sv("hello")); + CHECK_THROWS(session::ed25519::sign(to_unsigned_sv(ed_invalid), to_unsigned_sv("hello"))); + + auto expected_sig_hex = + "e03b6e87a53d83f202f2501e9b52193dbe4a64c6503f88244948dee53271" + "85011574589aa7b59bc9757f9b9c31b7be9c9212b92ac7c81e029ee21c338ee12405"; + CHECK(oxenc::to_hex(sig1.begin(), sig1.end()) == expected_sig_hex); + + CHECK(session::ed25519::verify(sig1, ed_pk, to_unsigned_sv("hello"))); + CHECK_THROWS(session::ed25519::verify(ed_invalid, ed_pk, to_unsigned_sv("hello"))); + CHECK_THROWS(session::ed25519::verify(ed_pk, ed_invalid, to_unsigned_sv("hello"))); +} diff --git a/tests/test_hash.cpp b/tests/test_hash.cpp new file mode 100644 index 00000000..626cd57d --- /dev/null +++ b/tests/test_hash.cpp @@ -0,0 +1,50 @@ +#include + +#include "session/hash.h" +#include "session/hash.hpp" +#include "utils.hpp" + +TEST_CASE("Hash generation", "[hash][hash]") { + auto hash1 = session::hash::hash(32, to_usv("TestMessage"), std::nullopt); + auto hash2 = session::hash::hash(32, to_usv("TestMessage"), std::nullopt); + auto hash3 = session::hash::hash(32, to_usv("TestMessage"), to_usv("TestKey")); + auto hash4 = session::hash::hash(32, to_usv("TestMessage"), to_usv("TestKey")); + auto hash5 = session::hash::hash(64, to_usv("TestMessage"), std::nullopt); + auto hash6 = session::hash::hash(64, to_usv("TestMessage"), to_usv("TestKey")); + CHECK_THROWS(session::hash::hash(10, to_usv("TestMessage"), std::nullopt)); + CHECK_THROWS(session::hash::hash(100, to_usv("TestMessage"), std::nullopt)); + CHECK_THROWS(session::hash::hash( + 32, + to_usv("TestMessage"), + to_usv("KeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLongKeyThatIsTooLon" + "g"))); + + CHECK(hash1.size() == 32); + CHECK(hash2.size() == 32); + CHECK(hash3.size() == 32); + CHECK(hash4.size() == 32); + CHECK(hash5.size() == 64); + CHECK(hash6.size() == 64); + CHECK(hash1 == hash2); + CHECK(hash1 != hash3); + CHECK(hash3 == hash4); + CHECK(hash1 != hash5); + CHECK(hash3 != hash6); + CHECK(oxenc::to_hex(hash1) == + "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); + CHECK(oxenc::to_hex(hash2) == + "2a48a12262e4548afb97fe2b04a912a02297d451169ee7ef2d01a28ea20286ab"); + CHECK(oxenc::to_hex(hash3) == + "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); + CHECK(oxenc::to_hex(hash4) == + "3d643e479b626bb2907476e32ccf7bdbd1ac3efa0da6e2c335255c48dcc216b6"); + + auto expected_hash5 = + "9d9085ac026fe3542abbeb2ea2ec05f5c37aecd7695f6cc41e9ccf39014196a39c02db69c44" + "16d5c45acc2e9469b7f274992b2858f3bb2746becb48c8b56ce4b"; + auto expected_hash6 = + "6a2faad89cf9010a4270cba07cc96cfb36688106e080b15fef66bb03c68e877874c9059edf5" + "3d03c1330b2655efdad6e4aa259118b6ea88698ea038efb9d52ce"; + CHECK(oxenc::to_hex(hash5) == expected_hash5); + CHECK(oxenc::to_hex(hash6) == expected_hash6); +} diff --git a/tests/test_onionreq.cpp b/tests/test_onionreq.cpp index 93465674..a4c42dd3 100644 --- a/tests/test_onionreq.cpp +++ b/tests/test_onionreq.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include "utils.hpp" @@ -33,7 +33,7 @@ TEST_CASE("Onion request encryption", "[encryption][onionreq]") { "9e1a3abe60eff3ea5c23556ccfe225b6f94355315f7281f66ecf4dbb06e7899a52b863e03cde3b28" "7d1638d765db75de02b032"_hexbytes; - ChannelEncryption e{x25519_seckey::from_bytes(b), x25519_pubkey::from_bytes(B), true}; + HopEncryption e{x25519_seckey::from_bytes(b), x25519_pubkey::from_bytes(B), true}; CHECK(from_unsigned_sv(e.decrypt_aesgcm(enc_gcm, x25519_pubkey::from_bytes(A))) == "Hello world"); @@ -73,7 +73,7 @@ TEST_CASE("Onion request parser", "[onionreq][parser]") { auto aes_reply = parser_gcm.encrypt_reply(to_unsigned_sv("Goodbye world")); CHECK(aes_reply.size() == 12 + 13 + 16); - ChannelEncryption e{x25519_seckey::from_bytes(a), x25519_pubkey::from_bytes(A), false}; + HopEncryption e{x25519_seckey::from_bytes(a), x25519_pubkey::from_bytes(A), false}; CHECK(from_unsigned_sv(e.decrypt_aesgcm(aes_reply, x25519_pubkey::from_bytes(B))) == "Goodbye world"); diff --git a/tests/test_random.cpp b/tests/test_random.cpp new file mode 100644 index 00000000..c02ad515 --- /dev/null +++ b/tests/test_random.cpp @@ -0,0 +1,16 @@ +#include + +#include "session/random.h" +#include "session/random.hpp" +#include "utils.hpp" + +TEST_CASE("Random generation", "[random][random]") { + auto rand1 = session::random::random(10); + auto rand2 = session::random::random(10); + auto rand3 = session::random::random(20); + + CHECK(rand1.size() == 10); + CHECK(rand2.size() == 10); + CHECK(rand3.size() == 20); + CHECK(rand1 != rand2); +} diff --git a/tests/test_session_encrypt.cpp b/tests/test_session_encrypt.cpp index a168c6e6..8c286b69 100644 --- a/tests/test_session_encrypt.cpp +++ b/tests/test_session_encrypt.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -67,7 +68,8 @@ TEST_CASE("Session protocol encryption", "[session-protocol][encrypt]") { "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " "culpa qui officia deserunt mollit anim id est laborum."sv; - auto enc = encrypt_for_recipient(to_sv(ed_sk), sid_raw2, to_unsigned_sv(lorem_ipsum)); + auto enc = encrypt_for_recipient( + {to_sv(ed_sk).data(), 32}, sid_raw2, to_unsigned_sv(lorem_ipsum)); CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); CHECK_THROWS(decrypt_incoming(to_sv(ed_sk), enc)); @@ -137,3 +139,308 @@ TEST_CASE("Session protocol deterministic encryption", "[session-protocol][encry CHECK(oxenc::to_hex(sender) == oxenc::to_hex(ed_pk.begin(), ed_pk.end())); CHECK(from_unsigned_sv(msg) == "hello"); } + +static std::array prefixed(unsigned char prefix, const session::uc32& pubkey) { + std::array result; + result[0] = prefix; + std::memcpy(result.data() + 1, pubkey.data(), 32); + return result; +} + +TEST_CASE("Session blinding protocol encryption", "[session-blinding-protocol][encrypt]") { + + using namespace session; + + const auto seed = "0123456789abcdef0123456789abcdef00000000000000000000000000000000"_hexbytes; + const auto server_pk = + "1d7e7f92b1ed3643855c98ecac02fc7274033a3467653f047d6e433540c03f17"_hexbytes; + std::array ed_pk, curve_pk; + std::array ed_sk; + crypto_sign_ed25519_seed_keypair(ed_pk.data(), ed_sk.data(), seed.data()); + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk.data(), ed_pk.data())); + REQUIRE(oxenc::to_hex(ed_pk.begin(), ed_pk.end()) == + "4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"); + REQUIRE(oxenc::to_hex(curve_pk.begin(), curve_pk.end()) == + "d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + auto sid = "05" + oxenc::to_hex(curve_pk.begin(), curve_pk.end()); + ustring sid_raw; + oxenc::from_hex(sid.begin(), sid.end(), std::back_inserter(sid_raw)); + REQUIRE(sid == "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + REQUIRE(sid_raw == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes); + auto [blind15_pk, blind15_sk] = blind15_key_pair(to_sv(ed_sk), to_unsigned_sv(server_pk)); + auto [blind25_pk, blind25_sk] = blind25_key_pair(to_sv(ed_sk), to_unsigned_sv(server_pk)); + auto blind15_pk_prefixed = prefixed(0x15, blind15_pk); + auto blind25_pk_prefixed = prefixed(0x25, blind25_pk); + + const auto seed2 = "00112233445566778899aabbccddeeff00000000000000000000000000000000"_hexbytes; + std::array ed_pk2, curve_pk2; + std::array ed_sk2; + crypto_sign_ed25519_seed_keypair(ed_pk2.data(), ed_sk2.data(), seed2.data()); + REQUIRE(0 == crypto_sign_ed25519_pk_to_curve25519(curve_pk2.data(), ed_pk2.data())); + REQUIRE(oxenc::to_hex(ed_pk2.begin(), ed_pk2.end()) == + "5ea34e72bb044654a6a23675690ef5ffaaf1656b02f93fb76655f9cbdbe89876"); + REQUIRE(oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()) == + "aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + auto sid2 = "05" + oxenc::to_hex(curve_pk2.begin(), curve_pk2.end()); + REQUIRE(sid2 == "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"); + ustring sid_raw2; + oxenc::from_hex(sid2.begin(), sid2.end(), std::back_inserter(sid_raw2)); + REQUIRE(sid_raw2 == + "05aa654f00fc39fc69fd0db829410ca38177d7732a8d2f0934ab3872ac56d5aa74"_hexbytes); + auto [blind15_pk2, blind15_sk2] = blind15_key_pair(to_sv(ed_sk2), to_unsigned_sv(server_pk)); + auto [blind25_pk2, blind25_sk2] = blind25_key_pair(to_sv(ed_sk2), to_unsigned_sv(server_pk)); + auto blind15_pk2_prefixed = prefixed(0x15, blind15_pk2); + auto blind25_pk2_prefixed = prefixed(0x25, blind25_pk2); + + SECTION("blind15, full secret, recipient decrypt") { + auto enc = encrypt_for_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, + to_unsigned_sv("hello")); + CHECK(from_unsigned_sv(enc) != "hello"); + + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + to_sv(blind15_pk), + {blind15_pk2_prefixed.data(), 33}, + enc)); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + to_sv(blind15_pk2), + enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == "hello"); + + auto broken = enc; + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + broken)); + } + SECTION("blind15, only seed, sender decrypt") { + constexpr auto lorem_ipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum."sv; + auto enc = encrypt_for_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, + to_unsigned_sv(lorem_ipsum)); + CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + + auto [msg, sender] = decrypt_from_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == lorem_ipsum); + + auto broken = enc; + broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + broken)); + } + SECTION("blind15, only seed, recipient decrypt") { + constexpr auto lorem_ipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum."sv; + auto enc = encrypt_for_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk2_prefixed.data(), 33}, + to_unsigned_sv(lorem_ipsum)); + CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + + auto [msg, sender] = decrypt_from_blinded_recipient( + {to_sv(ed_sk2).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == lorem_ipsum); + + auto broken = enc; + broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + {to_sv(ed_sk2).data(), 32}, + to_unsigned_sv(server_pk), + {blind15_pk_prefixed.data(), 33}, + {blind15_pk2_prefixed.data(), 33}, + broken)); + } + SECTION("blind25, full secret, sender decrypt") { + auto enc = encrypt_for_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, + to_unsigned_sv("hello")); + CHECK(from_unsigned_sv(enc) != "hello"); + + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + to_sv(blind25_pk), + {blind25_pk2_prefixed.data(), 33}, + enc)); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + to_sv(blind25_pk2), + enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == "hello"); + + auto broken = enc; + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + broken)); + } + SECTION("blind25, full secret, recipient decrypt") { + auto enc = encrypt_for_blinded_recipient( + to_sv(ed_sk), + to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, + to_unsigned_sv("hello")); + CHECK(from_unsigned_sv(enc) != "hello"); + + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + to_sv(blind25_pk), + {blind25_pk2_prefixed.data(), 33}, + enc)); + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + to_sv(blind25_pk2), + enc)); + + auto [msg, sender] = decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == "hello"); + + auto broken = enc; + broken[23] ^= 0x80; // 1 + 5 + 16 = 22 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + to_sv(ed_sk2), + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + broken)); + } + SECTION("blind25, only seed, recipient decrypt") { + constexpr auto lorem_ipsum = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis " + "nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in " + "culpa qui officia deserunt mollit anim id est laborum."sv; + auto enc = encrypt_for_blinded_recipient( + {to_sv(ed_sk).data(), 32}, + to_unsigned_sv(server_pk), + {blind25_pk2_prefixed.data(), 33}, + to_unsigned_sv(lorem_ipsum)); + CHECK(enc.find(to_unsigned("dolore magna")) == std::string::npos); + + auto [msg, sender] = decrypt_from_blinded_recipient( + {to_sv(ed_sk2).data(), 32}, + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + enc); + CHECK(sender == sid); + CHECK(from_unsigned_sv(msg) == lorem_ipsum); + + auto broken = enc; + broken[463] ^= 0x80; // 1 + 445 + 16 = 462 is the start of the nonce + CHECK_THROWS(decrypt_from_blinded_recipient( + {to_sv(ed_sk2).data(), 32}, + to_unsigned_sv(server_pk), + {blind25_pk_prefixed.data(), 33}, + {blind25_pk2_prefixed.data(), 33}, + broken)); + } +} + +TEST_CASE("Session ONS response decryption", "[session-ons][decrypt]") { + using namespace session; + + std::string_view name = "test"; + auto ciphertext = + "3575802dd9bfea72672a208840f37ca289ceade5d3ffacabe2d231f109d204329fc33e28c33" + "1580d9a8c9b8a64cacfec97"_hexbytes; + auto nonce = "00112233445566778899aabbccddeeff00ffeeddccbbaa99"_hexbytes; + ustring sid_data = + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"_hexbytes; + + CHECK(decrypt_ons_response(name, ciphertext, nonce) == + "05d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"); + CHECK_THROWS(decrypt_ons_response(name, to_unsigned_sv("invalid"), nonce)); + CHECK_THROWS(decrypt_ons_response(name, ciphertext, to_unsigned_sv("invalid"))); +} + +TEST_CASE("Session push notification decryption", "[session-notification][decrypt]") { + using namespace session; + + auto payload = + "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" + "ab4aca5f0991e99eb0344ceeafa"_hexbytes; + auto payload_padded = + "00112233445566778899aabbccddeeff00ffeeddccbbaa991bcba42892762dbeecbfb1a375f" + "ab4aca5f0991e99eb0344ceeafa"_hexbytes; + auto enc_key = "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes; + + CHECK(decrypt_push_notification(payload, enc_key) == to_unsigned("TestMessage")); + CHECK(decrypt_push_notification(payload_padded, enc_key) == to_unsigned("TestMessage")); + CHECK_THROWS(decrypt_push_notification(to_unsigned_sv("invalid"), enc_key)); + CHECK_THROWS(decrypt_push_notification(payload, to_unsigned_sv("invalid"))); +} diff --git a/tests/test_xed25519.cpp b/tests/test_xed25519.cpp index 67089302..337ad46f 100644 --- a/tests/test_xed25519.cpp +++ b/tests/test_xed25519.cpp @@ -159,10 +159,8 @@ TEST_CASE("XEd25519 signing (C wrapper)", "[xed25519][sign][c]") { const auto msg = view("hello world"); std::array xed_sig1, xed_sig2; - rc = session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); - rc = session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); + REQUIRE(session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size())); + REQUIRE(session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size())); rc = crypto_sign_ed25519_verify_detached(xed_sig1.data(), msg.data(), msg.size(), pub1.data()); REQUIRE(rc == 0); @@ -186,13 +184,9 @@ TEST_CASE("XEd25519 verification (C wrapper)", "[xed25519][verify][c]") { const auto msg = view("hello world"); std::array xed_sig1, xed_sig2; - rc = session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); - rc = session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); + REQUIRE(session_xed25519_sign(xed_sig1.data(), xsk1.data(), msg.data(), msg.size())); + REQUIRE(session_xed25519_sign(xed_sig2.data(), xsk2.data(), msg.data(), msg.size())); - rc = session_xed25519_verify(xed_sig1.data(), xpub1.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); - rc = session_xed25519_verify(xed_sig2.data(), xpub2.data(), msg.data(), msg.size()); - REQUIRE(rc == 0); + REQUIRE(session_xed25519_verify(xed_sig1.data(), xpub1.data(), msg.data(), msg.size())); + REQUIRE(session_xed25519_verify(xed_sig2.data(), xpub2.data(), msg.data(), msg.size())); }