diff --git a/depends/packages/librustzcash.mk b/depends/packages/librustzcash.mk index 11ee085f4..3a338cc21 100644 --- a/depends/packages/librustzcash.mk +++ b/depends/packages/librustzcash.mk @@ -3,8 +3,8 @@ $(package)_version=0.1 $(package)_download_path=https://github.com/zcash/$(package)/archive/ $(package)_file_name=$(package)-$($(package)_git_commit).tar.gz $(package)_download_file=$($(package)_git_commit).tar.gz -$(package)_sha256_hash=5231145ea6abf61092c21b6770baf3af65994f83dff96b10118ba5dd53451f26 -$(package)_git_commit=0af1ce8bf121e1ad367db907c39d214581e270a6 +$(package)_sha256_hash=5a50aae38a9ef4823cd319278ad95706a129cc091e1cca9342802f1ff75aba15 +$(package)_git_commit=93e26d1d8716ac88f8bb372d442315edcd2deabd $(package)_dependencies=rust $(rust_crates) $(package)_patches=cargo.config diff --git a/src/gtest/test_noteencryption.cpp b/src/gtest/test_noteencryption.cpp index 754832179..1aded18f2 100644 --- a/src/gtest/test_noteencryption.cpp +++ b/src/gtest/test_noteencryption.cpp @@ -6,7 +6,9 @@ #include "zcash/NoteEncryption.hpp" #include "zcash/prf.h" +#include "zcash/Address.hpp" #include "crypto/sha256.h" +#include "librustzcash.h" class TestNoteDecryption : public ZCNoteDecryption { public: @@ -17,6 +19,189 @@ public: } }; +TEST(noteencryption, SaplingApi) +{ + using namespace libzcash; + + // Create recipient addresses + auto sk = SaplingSpendingKey(uint256()).expanded_spending_key(); + auto vk = sk.full_viewing_key(); + auto ivk = vk.in_viewing_key(); + SaplingPaymentAddress pk_1 = *ivk.address({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + SaplingPaymentAddress pk_2 = *ivk.address({4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + + // Blob of stuff we're encrypting + std::array message; + for (size_t i = 0; i < ZC_SAPLING_ENCPLAINTEXT_SIZE; i++) { + // Fill the message with dummy data + message[i] = (unsigned char) i; + } + + std::array small_message; + for (size_t i = 0; i < ZC_SAPLING_OUTPLAINTEXT_SIZE; i++) { + // Fill the message with dummy data + small_message[i] = (unsigned char) i; + } + + // Invalid diversifier + ASSERT_EQ(boost::none, SaplingNoteEncryption::FromDiversifier({1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + + // Encrypt to pk_1 + auto enc = *SaplingNoteEncryption::FromDiversifier(pk_1.d); + auto ciphertext_1 = *enc.encrypt_to_recipient( + pk_1.pk_d, + message + ); + auto epk_1 = enc.get_epk(); + { + uint256 test_epk; + uint256 test_esk = enc.get_esk(); + ASSERT_TRUE(librustzcash_sapling_ka_derivepublic(pk_1.d.begin(), test_esk.begin(), test_epk.begin())); + ASSERT_TRUE(test_epk == epk_1); + } + auto cv_1 = random_uint256(); + auto cm_1 = random_uint256(); + auto out_ciphertext_1 = enc.encrypt_to_ourselves( + sk.ovk, + cv_1, + cm_1, + small_message + ); + + // Encrypt to pk_2 + enc = *SaplingNoteEncryption::FromDiversifier(pk_2.d); + auto ciphertext_2 = *enc.encrypt_to_recipient( + pk_2.pk_d, + message + ); + auto epk_2 = enc.get_epk(); + + auto cv_2 = random_uint256(); + auto cm_2 = random_uint256(); + auto out_ciphertext_2 = enc.encrypt_to_ourselves( + sk.ovk, + cv_2, + cm_2, + small_message + ); + + // Test nonce-reuse resistance of API + { + auto tmp_enc = *SaplingNoteEncryption::FromDiversifier(pk_1.d); + + tmp_enc.encrypt_to_recipient( + pk_1.pk_d, + message + ); + + ASSERT_THROW(tmp_enc.encrypt_to_recipient( + pk_1.pk_d, + message + ), std::logic_error); + + tmp_enc.encrypt_to_ourselves( + sk.ovk, + cv_2, + cm_2, + small_message + ); + + ASSERT_THROW(tmp_enc.encrypt_to_ourselves( + sk.ovk, + cv_2, + cm_2, + small_message + ), std::logic_error); + } + + // Try to decrypt + auto plaintext_1 = *AttemptSaplingEncDecryption( + ciphertext_1, + ivk, + epk_1 + ); + ASSERT_TRUE(message == plaintext_1); + + auto small_plaintext_1 = *AttemptSaplingOutDecryption( + out_ciphertext_1, + sk.ovk, + cv_1, + cm_1, + epk_1 + ); + ASSERT_TRUE(small_message == small_plaintext_1); + + auto plaintext_2 = *AttemptSaplingEncDecryption( + ciphertext_2, + ivk, + epk_2 + ); + ASSERT_TRUE(message == plaintext_2); + + auto small_plaintext_2 = *AttemptSaplingOutDecryption( + out_ciphertext_2, + sk.ovk, + cv_2, + cm_2, + epk_2 + ); + ASSERT_TRUE(small_message == small_plaintext_2); + + // Try to decrypt out ciphertext with wrong key material + ASSERT_FALSE(AttemptSaplingOutDecryption( + out_ciphertext_1, + random_uint256(), + cv_1, + cm_1, + epk_1 + )); + ASSERT_FALSE(AttemptSaplingOutDecryption( + out_ciphertext_1, + sk.ovk, + random_uint256(), + cm_1, + epk_1 + )); + ASSERT_FALSE(AttemptSaplingOutDecryption( + out_ciphertext_1, + sk.ovk, + cv_1, + random_uint256(), + epk_1 + )); + ASSERT_FALSE(AttemptSaplingOutDecryption( + out_ciphertext_1, + sk.ovk, + cv_1, + cm_1, + random_uint256() + )); + + // Try to decrypt with wrong ephemeral key + ASSERT_FALSE(AttemptSaplingEncDecryption( + ciphertext_1, + ivk, + epk_2 + )); + ASSERT_FALSE(AttemptSaplingEncDecryption( + ciphertext_2, + ivk, + epk_1 + )); + + // Try to decrypt with wrong ivk + ASSERT_FALSE(AttemptSaplingEncDecryption( + ciphertext_1, + uint256(), + epk_1 + )); + ASSERT_FALSE(AttemptSaplingEncDecryption( + ciphertext_2, + uint256(), + epk_2 + )); +} + TEST(noteencryption, api) { uint256 sk_enc = ZCNoteEncryption::generate_privkey(uint252(uint256S("21035d60bc1983e37950ce4803418a8fb33ea68d5b937ca382ecbae7564d6a07"))); diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 715c8672f..4295dea9f 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -84,33 +84,17 @@ public: } }; -static constexpr size_t SAPLING_ENC_CIPHERTEXT_SIZE = ( - 1 + // leading byte - 11 + // d - 8 + // value - 32 + // rcm - ZC_MEMO_SIZE + // memo - NOTEENCRYPTION_AUTH_BYTES); - -static constexpr size_t SAPLING_OUT_CIPHERTEXT_SIZE = ( - 32 + // pkd_new - 32 + // esk - NOTEENCRYPTION_AUTH_BYTES); - /** * A shielded output to a transaction. It contains data that describes an Output transfer. */ class OutputDescription { public: - typedef std::array sapling_enc_ct_t; // TODO: Replace with actual type - typedef std::array sapling_out_ct_t; // TODO: Replace with actual type - uint256 cv; //!< A value commitment to the value of the output note. uint256 cm; //!< The note commitment for the output note. uint256 ephemeralKey; //!< A Jubjub public key. - sapling_enc_ct_t encCiphertext; //!< A ciphertext component for the encrypted output note. - sapling_out_ct_t outCiphertext; //!< A ciphertext component for the encrypted output note. + libzcash::SaplingEncCiphertext encCiphertext; //!< A ciphertext component for the encrypted output note. + libzcash::SaplingOutCiphertext outCiphertext; //!< A ciphertext component for the encrypted output note. libzcash::GrothProof zkproof; //!< A zero-knowledge proof using the output circuit. OutputDescription() { } diff --git a/src/zcash/Address.hpp b/src/zcash/Address.hpp index 01eb14f54..5a6ec2a60 100644 --- a/src/zcash/Address.hpp +++ b/src/zcash/Address.hpp @@ -4,6 +4,7 @@ #include "uint256.h" #include "uint252.h" #include "serialize.h" +#include "Zcash.h" #include @@ -18,7 +19,7 @@ const size_t SerializedPaymentAddressSize = 64; const size_t SerializedViewingKeySize = 64; const size_t SerializedSpendingKeySize = 32; -typedef std::array diversifier_t; +typedef std::array diversifier_t; class SproutPaymentAddress { public: diff --git a/src/zcash/NoteEncryption.cpp b/src/zcash/NoteEncryption.cpp index 9ae0ba5c3..a00999bc3 100644 --- a/src/zcash/NoteEncryption.cpp +++ b/src/zcash/NoteEncryption.cpp @@ -3,6 +3,7 @@ #include "sodium.h" #include #include "prf.h" +#include "librustzcash.h" #define NOTEENCRYPTION_CIPHER_KEYSIZE 32 @@ -13,6 +14,58 @@ void clamp_curve25519(unsigned char key[crypto_scalarmult_SCALARBYTES]) key[31] |= 64; } +void PRF_ock( + unsigned char K[NOTEENCRYPTION_CIPHER_KEYSIZE], + const uint256 &ovk, + const uint256 &cv, + const uint256 &cm, + const uint256 &epk +) +{ + unsigned char block[128] = {}; + memcpy(block+0, ovk.begin(), 32); + memcpy(block+32, cv.begin(), 32); + memcpy(block+64, cm.begin(), 32); + memcpy(block+96, epk.begin(), 32); + + unsigned char personalization[crypto_generichash_blake2b_PERSONALBYTES] = {}; + memcpy(personalization, "Zcash_Derive_ock", 16); + + if (crypto_generichash_blake2b_salt_personal(K, NOTEENCRYPTION_CIPHER_KEYSIZE, + block, 128, + NULL, 0, // No key. + NULL, // No salt. + personalization + ) != 0) + { + throw std::logic_error("hash function failure"); + } +} + +void KDF_Sapling( + unsigned char K[NOTEENCRYPTION_CIPHER_KEYSIZE], + const uint256 &dhsecret, + const uint256 &epk +) +{ + unsigned char block[64] = {}; + memcpy(block+0, dhsecret.begin(), 32); + memcpy(block+32, epk.begin(), 32); + + unsigned char personalization[crypto_generichash_blake2b_PERSONALBYTES] = {}; + memcpy(personalization, "Zcash_SaplingKDF", 16); + + if (crypto_generichash_blake2b_salt_personal(K, NOTEENCRYPTION_CIPHER_KEYSIZE, + block, 64, + NULL, 0, // No key. + NULL, // No salt. + personalization + ) != 0) + { + throw std::logic_error("hash function failure"); + } +} + void KDF(unsigned char K[NOTEENCRYPTION_CIPHER_KEYSIZE], const uint256 &dhsecret, const uint256 &epk, @@ -48,6 +101,155 @@ void KDF(unsigned char K[NOTEENCRYPTION_CIPHER_KEYSIZE], namespace libzcash { +boost::optional SaplingNoteEncryption::FromDiversifier(diversifier_t d) { + uint256 epk; + uint256 esk; + + // Pick random esk + librustzcash_sapling_generate_r(esk.begin()); + + // Compute epk given the diversifier + if (!librustzcash_sapling_ka_derivepublic(d.begin(), esk.begin(), epk.begin())) { + return boost::none; + } + + return SaplingNoteEncryption(epk, esk); +} + +boost::optional SaplingNoteEncryption::encrypt_to_recipient( + const uint256 &pk_d, + const SaplingEncPlaintext &message +) +{ + if (already_encrypted_enc) { + throw std::logic_error("already encrypted to the recipient using this key"); + } + + uint256 dhsecret; + + if (!librustzcash_sapling_ka_agree(pk_d.begin(), esk.begin(), dhsecret.begin())) { + return boost::none; + } + + // Construct the symmetric key + unsigned char K[NOTEENCRYPTION_CIPHER_KEYSIZE]; + KDF_Sapling(K, dhsecret, epk); + + // The nonce is zero because we never reuse keys + unsigned char cipher_nonce[crypto_aead_chacha20poly1305_IETF_NPUBBYTES] = {}; + + SaplingEncCiphertext ciphertext; + + crypto_aead_chacha20poly1305_ietf_encrypt( + ciphertext.begin(), NULL, + message.begin(), ZC_SAPLING_ENCPLAINTEXT_SIZE, + NULL, 0, // no "additional data" + NULL, cipher_nonce, K + ); + + already_encrypted_enc = true; + + return ciphertext; +} + +boost::optional AttemptSaplingEncDecryption( + const SaplingEncCiphertext &ciphertext, + const uint256 &ivk, + const uint256 &epk +) +{ + uint256 dhsecret; + + if (!librustzcash_sapling_ka_agree(epk.begin(), ivk.begin(), dhsecret.begin())) { + return boost::none; + } + + // Construct the symmetric key + unsigned char K[NOTEENCRYPTION_CIPHER_KEYSIZE]; + KDF_Sapling(K, dhsecret, epk); + + // The nonce is zero because we never reuse keys + unsigned char cipher_nonce[crypto_aead_chacha20poly1305_IETF_NPUBBYTES] = {}; + + SaplingEncPlaintext plaintext; + + if (crypto_aead_chacha20poly1305_ietf_decrypt( + plaintext.begin(), NULL, + NULL, + ciphertext.begin(), ZC_SAPLING_ENCCIPHERTEXT_SIZE, + NULL, + 0, + cipher_nonce, K) != 0) + { + return boost::none; + } + + return plaintext; +} + +SaplingOutCiphertext SaplingNoteEncryption::encrypt_to_ourselves( + const uint256 &ovk, + const uint256 &cv, + const uint256 &cm, + const SaplingOutPlaintext &message +) +{ + if (already_encrypted_out) { + throw std::logic_error("already encrypted to the recipient using this key"); + } + + // Construct the symmetric key + unsigned char K[NOTEENCRYPTION_CIPHER_KEYSIZE]; + PRF_ock(K, ovk, cv, cm, epk); + + // The nonce is zero because we never reuse keys + unsigned char cipher_nonce[crypto_aead_chacha20poly1305_IETF_NPUBBYTES] = {}; + + SaplingOutCiphertext ciphertext; + + crypto_aead_chacha20poly1305_ietf_encrypt( + ciphertext.begin(), NULL, + message.begin(), ZC_SAPLING_OUTPLAINTEXT_SIZE, + NULL, 0, // no "additional data" + NULL, cipher_nonce, K + ); + + already_encrypted_out = true; + + return ciphertext; +} + +boost::optional AttemptSaplingOutDecryption( + const SaplingOutCiphertext &ciphertext, + const uint256 &ovk, + const uint256 &cv, + const uint256 &cm, + const uint256 &epk +) +{ + // Construct the symmetric key + unsigned char K[NOTEENCRYPTION_CIPHER_KEYSIZE]; + PRF_ock(K, ovk, cv, cm, epk); + + // The nonce is zero because we never reuse keys + unsigned char cipher_nonce[crypto_aead_chacha20poly1305_IETF_NPUBBYTES] = {}; + + SaplingOutPlaintext plaintext; + + if (crypto_aead_chacha20poly1305_ietf_decrypt( + plaintext.begin(), NULL, + NULL, + ciphertext.begin(), ZC_SAPLING_OUTCIPHERTEXT_SIZE, + NULL, + 0, + cipher_nonce, K) != 0) + { + return boost::none; + } + + return plaintext; +} + template NoteEncryption::NoteEncryption(uint256 hSig) : nonce(0), hSig(hSig) { // All of this code assumes crypto_scalarmult_BYTES is 32 diff --git a/src/zcash/NoteEncryption.hpp b/src/zcash/NoteEncryption.hpp index 86de8f44b..82f8b2abf 100644 --- a/src/zcash/NoteEncryption.hpp +++ b/src/zcash/NoteEncryption.hpp @@ -10,12 +10,78 @@ https://github.com/zcash/zips/blob/master/protocol/protocol.pdf #include "uint252.h" #include "zcash/Zcash.h" +#include "zcash/Address.hpp" #include namespace libzcash { -#define NOTEENCRYPTION_AUTH_BYTES 16 +// Ciphertext for the recipient to decrypt +typedef std::array SaplingEncCiphertext; +typedef std::array SaplingEncPlaintext; + +// Ciphertext for outgoing viewing key to decrypt +typedef std::array SaplingOutCiphertext; +typedef std::array SaplingOutPlaintext; + +//! This is not a thread-safe API. +class SaplingNoteEncryption { +protected: + // Ephemeral public key + uint256 epk; + + // Ephemeral secret key + uint256 esk; + + bool already_encrypted_enc; + bool already_encrypted_out; + + SaplingNoteEncryption(uint256 epk, uint256 esk) : epk(epk), esk(esk), already_encrypted_enc(false), already_encrypted_out(false) { + + } + +public: + + static boost::optional FromDiversifier(diversifier_t d); + + boost::optional encrypt_to_recipient( + const uint256 &pk_d, + const SaplingEncPlaintext &message + ); + + SaplingOutCiphertext encrypt_to_ourselves( + const uint256 &ovk, + const uint256 &cv, + const uint256 &cm, + const SaplingOutPlaintext &message + ); + + uint256 get_epk() const { + return epk; + } + + uint256 get_esk() const { + return esk; + } +}; + +// Attempts to decrypt a Sapling note. This will not check that the contents +// of the ciphertext are correct. +boost::optional AttemptSaplingEncDecryption( + const SaplingEncCiphertext &ciphertext, + const uint256 &ivk, + const uint256 &epk +); + +// Attempts to decrypt a Sapling note. This will not check that the contents +// of the ciphertext are correct. +boost::optional AttemptSaplingOutDecryption( + const SaplingOutCiphertext &ciphertext, + const uint256 &ovk, + const uint256 &cv, + const uint256 &cm, + const uint256 &epk +); template class NoteEncryption { diff --git a/src/zcash/Zcash.h b/src/zcash/Zcash.h index bb805eef5..84dfe9525 100644 --- a/src/zcash/Zcash.h +++ b/src/zcash/Zcash.h @@ -8,12 +8,23 @@ #define SAPLING_INCREMENTAL_MERKLE_TREE_DEPTH 32 +#define NOTEENCRYPTION_AUTH_BYTES 16 + #define ZC_NOTEPLAINTEXT_LEADING 1 #define ZC_V_SIZE 8 #define ZC_RHO_SIZE 32 #define ZC_R_SIZE 32 #define ZC_MEMO_SIZE 512 +#define ZC_DIVERSIFIER_SIZE 11 +#define ZC_JUBJUB_POINT_SIZE 32 +#define ZC_JUBJUB_SCALAR_SIZE 32 #define ZC_NOTEPLAINTEXT_SIZE (ZC_NOTEPLAINTEXT_LEADING + ZC_V_SIZE + ZC_RHO_SIZE + ZC_R_SIZE + ZC_MEMO_SIZE) +#define ZC_SAPLING_ENCPLAINTEXT_SIZE (ZC_NOTEPLAINTEXT_LEADING + ZC_DIVERSIFIER_SIZE + ZC_V_SIZE + ZC_R_SIZE + ZC_MEMO_SIZE) +#define ZC_SAPLING_OUTPLAINTEXT_SIZE (ZC_JUBJUB_POINT_SIZE + ZC_JUBJUB_SCALAR_SIZE) + +#define ZC_SAPLING_ENCCIPHERTEXT_SIZE (ZC_SAPLING_ENCPLAINTEXT_SIZE + NOTEENCRYPTION_AUTH_BYTES) +#define ZC_SAPLING_OUTCIPHERTEXT_SIZE (ZC_SAPLING_OUTPLAINTEXT_SIZE + NOTEENCRYPTION_AUTH_BYTES) + #endif // ZC_ZCASH_H_