-
Notifications
You must be signed in to change notification settings - Fork 598
feat: LogEncryption trait #12942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: LogEncryption trait #12942
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
6594d69
feat: LogEncryption trait
benesjan 0419778
works but in a hacky way
benesjan be09388
WIP
benesjan 34e616c
docs
benesjan 75c182a
fix
benesjan beedd49
fix attempt
benesjan e3d3fab
workaround
benesjan a7532fc
nuking stale comments
benesjan 7b0bebb
more test disablings
benesjan 4e43bcf
Update noir-projects/aztec-nr/aztec/src/encrypted_logs/log_encryption.nr
benesjan 9bf9210
Update noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_s…
benesjan a4777d3
removing unnecessary TODO
benesjan 322cf2a
fix after committing comment suggestion
benesjan 353caf7
prefixing_with_tag test
benesjan 3443f43
warnings fix
benesjan 985a9b4
reverting incorrect change
benesjan 1c989b6
ensuring events are not passed to process_log
benesjan 2eb0717
fix
benesjan 6f30dc0
Merge branch 'master' into 03-21-feat_logencryption_trait
nventuro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
302 changes: 302 additions & 0 deletions
302
...ojects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/aes128.nr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,302 @@ | ||
| use crate::{ | ||
| encrypted_logs::{ | ||
| encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, | ||
| log_assembly_strategies::default_aes128::arithmetic_generics_utils::{ | ||
| get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, | ||
| }, | ||
| log_encryption::{ | ||
| EPH_PK_SIGN_BYTE_SIZE_IN_BYTES, EPH_PK_X_SIZE_IN_FIELDS, | ||
| HEADER_CIPHERTEXT_SIZE_IN_BYTES, LogEncryption, PRIVATE_LOG_CIPHERTEXT_LEN, | ||
| PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS, | ||
| }, | ||
| }, | ||
| keys::{ | ||
| ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, | ||
| ephemeral::generate_ephemeral_key_pair, | ||
| }, | ||
| oracle::{aes128_decrypt::aes128_decrypt_oracle, shared_secret::get_shared_secret}, | ||
| prelude::AztecAddress, | ||
| utils::{ | ||
| array, | ||
| conversion::{ | ||
| bytes_to_fields::{bytes_from_fields, bytes_to_fields}, | ||
| fields_to_bytes::{fields_from_bytes, fields_to_bytes}, | ||
| }, | ||
| point::{get_sign_of_point, point_from_x_coord_and_sign}, | ||
| random::get_random_bytes, | ||
| }, | ||
| }; | ||
| use std::aes128::aes128_encrypt; | ||
|
|
||
| pub struct AES128 {} | ||
|
|
||
| impl LogEncryption for AES128 { | ||
benesjan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| fn encrypt_log<let PLAINTEXT_LEN: u32>( | ||
| plaintext: [Field; PLAINTEXT_LEN], | ||
| recipient: AztecAddress, | ||
| ) -> [Field; PRIVATE_LOG_CIPHERTEXT_LEN] { | ||
| // AES 128 operates on bytes, not fields, so we need to convert the fields to bytes. | ||
| // (This process is then reversed when processing the log in `do_process_log`) | ||
| let plaintext_bytes = fields_to_bytes(plaintext); | ||
|
|
||
| // ***************************************************************************** | ||
| // Compute the shared secret | ||
| // ***************************************************************************** | ||
|
|
||
| let (eph_sk, eph_pk) = generate_ephemeral_key_pair(); | ||
|
|
||
| let eph_pk_sign_byte: u8 = get_sign_of_point(eph_pk) as u8; | ||
|
|
||
| // (not to be confused with the tagging shared secret) | ||
| let ciphertext_shared_secret = | ||
| derive_ecdh_shared_secret_using_aztec_address(eph_sk, recipient); | ||
|
|
||
| // TODO: also use this shared secret for deriving note randomness. | ||
|
|
||
| // ***************************************************************************** | ||
| // Convert the plaintext into whatever format the encryption function expects | ||
| // ***************************************************************************** | ||
|
|
||
| // Already done for this strategy: AES expects bytes. | ||
|
|
||
| // ***************************************************************************** | ||
| // Encrypt the plaintext | ||
| // ***************************************************************************** | ||
|
|
||
| let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( | ||
| ciphertext_shared_secret, | ||
| ); | ||
|
|
||
| let ciphertext_bytes = aes128_encrypt(plaintext_bytes, iv, sym_key); | ||
|
|
||
| // |full_pt| = |pt_length| + |pt| | ||
| // |pt_aes_padding| = 16 - (|full_pt| % 16) | ||
| // or... since a % b is the same as a - b * (a // b) (integer division), so: | ||
| // |pt_aes_padding| = 16 - (|full_pt| - 16 * (|full_pt| // 16)) | ||
| // |ct| = |full_pt| + |pt_aes_padding| | ||
| // = |full_pt| + 16 - (|full_pt| - 16 * (|full_pt| // 16)) | ||
| // = 16 + 16 * (|full_pt| // 16) | ||
| // = 16 * (1 + |full_pt| // 16) | ||
| assert(ciphertext_bytes.len() == 16 * (1 + (PLAINTEXT_LEN * 32) / 16)); | ||
|
|
||
| // ***************************************************************************** | ||
| // Compute the header ciphertext | ||
| // ***************************************************************************** | ||
|
|
||
| // Header contains only the length of the ciphertext stored in 2 bytes. | ||
| // TODO: consider nuking the header altogether and just have a fixed-size ciphertext by padding the plaintext. | ||
| // This would be more costly constraint-wise but cheaper DA-wise. | ||
| let mut header_plaintext: [u8; 2] = [0 as u8; 2]; | ||
| let ciphertext_bytes_length = ciphertext_bytes.len(); | ||
| header_plaintext[0] = (ciphertext_bytes_length >> 8) as u8; | ||
| header_plaintext[1] = ciphertext_bytes_length as u8; | ||
|
|
||
| // TODO: this is insecure and wasteful: | ||
| // "Insecure", because the esk shouldn't be used twice (once for the header, | ||
| // and again for the proper ciphertext) (at least, I never got the | ||
| // "go ahead" that this would be safe, unfortunately). | ||
| // "Wasteful", because the exact same computation is happening further down. | ||
| // I'm leaving that 2nd computation where it is, because this 1st computation | ||
| // will be imminently deleted, when the header logic is deleted. | ||
| let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( | ||
| ciphertext_shared_secret, | ||
| ); | ||
|
|
||
| // Note: the aes128_encrypt builtin fn automatically appends bytes to the | ||
| // input, according to pkcs#7; hence why the output `header_ciphertext_bytes` is 16 | ||
| // bytes larger than the input in this case. | ||
| let header_ciphertext_bytes = aes128_encrypt(header_plaintext, iv, sym_key); | ||
| // I recall that converting a slice to an array incurs constraints, so I'll check the length this way instead: | ||
| assert(header_ciphertext_bytes.len() == HEADER_CIPHERTEXT_SIZE_IN_BYTES); | ||
|
|
||
| // ***************************************************************************** | ||
| // Prepend / append more bytes of data to the ciphertext, before converting back | ||
| // to fields. | ||
| // ***************************************************************************** | ||
|
|
||
| let mut log_bytes_padding_to_mult_31 = | ||
| get_arr_of_size__log_bytes_padding__from_PT::<PLAINTEXT_LEN * 32>(); | ||
| // Safety: this randomness won't be constrained to be random. It's in the | ||
| // interest of the executor of this fn to encrypt with random bytes. | ||
| log_bytes_padding_to_mult_31 = unsafe { get_random_bytes() }; | ||
|
|
||
| let mut log_bytes = get_arr_of_size__log_bytes__from_PT::<PLAINTEXT_LEN * 32>(); | ||
|
|
||
| assert( | ||
| log_bytes.len() % 31 == 0, | ||
| "Unexpected error: log_bytes.len() should be divisible by 31, by construction.", | ||
| ); | ||
|
|
||
| log_bytes[0] = eph_pk_sign_byte; | ||
| let mut offset = 1; | ||
| for i in 0..header_ciphertext_bytes.len() { | ||
| log_bytes[offset + i] = header_ciphertext_bytes[i]; | ||
| } | ||
| offset += header_ciphertext_bytes.len(); | ||
|
|
||
| for i in 0..ciphertext_bytes.len() { | ||
| log_bytes[offset + i] = ciphertext_bytes[i]; | ||
| } | ||
| offset += ciphertext_bytes.len(); | ||
|
|
||
| for i in 0..log_bytes_padding_to_mult_31.len() { | ||
| log_bytes[offset + i] = log_bytes_padding_to_mult_31[i]; | ||
| } | ||
|
|
||
| assert( | ||
| offset + log_bytes_padding_to_mult_31.len() == log_bytes.len(), | ||
| "Something has gone wrong", | ||
| ); | ||
|
|
||
| // ***************************************************************************** | ||
| // Convert bytes back to fields | ||
| // ***************************************************************************** | ||
|
|
||
| // TODO(#12749): As Mike pointed out, we need to make logs produced by different encryption schemes | ||
| // indistinguishable from each other and for this reason the output here and in the last for-loop of this function | ||
| // should cover a full field. | ||
| let log_bytes_as_fields = bytes_to_fields(log_bytes); | ||
|
|
||
| // ***************************************************************************** | ||
| // Prepend / append fields, to create the final log | ||
| // ***************************************************************************** | ||
|
|
||
| let mut ciphertext: [Field; PRIVATE_LOG_CIPHERTEXT_LEN] = [0; PRIVATE_LOG_CIPHERTEXT_LEN]; | ||
|
|
||
| ciphertext[0] = eph_pk.x; | ||
|
|
||
| let mut offset = 1; | ||
| for i in 0..log_bytes_as_fields.len() { | ||
| ciphertext[offset + i] = log_bytes_as_fields[i]; | ||
| } | ||
| offset += log_bytes_as_fields.len(); | ||
|
|
||
| for i in offset..PRIVATE_LOG_CIPHERTEXT_LEN { | ||
| // We need to get a random value that fits in 31 bytes to not leak information about the size of the log | ||
| // (all the "real" log fields contain at most 31 bytes because of the way we convert the bytes to fields). | ||
| // TODO(#12749): Long term, this is not a good solution. | ||
|
|
||
| // Safety: we assume that the sender wants for the log to be private - a malicious one could simply reveal its | ||
| // contents publicly. It is therefore fine to trust the sender to provide random padding. | ||
| let field_bytes = unsafe { get_random_bytes::<31>() }; | ||
| ciphertext[i] = Field::from_be_bytes::<31>(field_bytes); | ||
| } | ||
|
|
||
| ciphertext | ||
| } | ||
|
|
||
| unconstrained fn decrypt_log( | ||
| ciphertext: BoundedVec<Field, PRIVATE_LOG_CIPHERTEXT_LEN>, | ||
| recipient: AztecAddress, | ||
| ) -> BoundedVec<Field, PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS> { | ||
| let eph_pk_x = ciphertext.get(0); | ||
|
|
||
| let ciphertext_without_eph_pk_x_fields = array::subbvec::<Field, PRIVATE_LOG_CIPHERTEXT_LEN, PRIVATE_LOG_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS>( | ||
| ciphertext, | ||
| EPH_PK_X_SIZE_IN_FIELDS, | ||
| ); | ||
|
|
||
| // Convert the ciphertext represented as fields to a byte representation (its original format) | ||
| let ciphertext_without_eph_pk_x = bytes_from_fields(ciphertext_without_eph_pk_x_fields); | ||
|
|
||
| // First byte of the ciphertext represents the ephemeral public key sign | ||
| let eph_pk_sign_bool = ciphertext_without_eph_pk_x.get(0) as bool; | ||
| // With the sign and the x-coordinate of the ephemeral public key, we can reconstruct the point | ||
| let eph_pk = point_from_x_coord_and_sign(eph_pk_x, eph_pk_sign_bool); | ||
|
|
||
| // Derive shared secret and symmetric key | ||
| let ciphertext_shared_secret = get_shared_secret(recipient, eph_pk); | ||
| let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( | ||
| ciphertext_shared_secret, | ||
| ); | ||
|
|
||
| // Extract the header ciphertext | ||
| let header_start = EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; // Skip eph_pk_sign byte | ||
| let header_ciphertext: [u8; HEADER_CIPHERTEXT_SIZE_IN_BYTES] = | ||
| array::subarray(ciphertext_without_eph_pk_x.storage(), header_start); | ||
| // We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's designed to work | ||
| // with logs with unknown length at compile time. This would not be necessary here as the header ciphertext length | ||
| // is fixed. But we do it anyway to not have to have duplicate oracles. | ||
| let header_ciphertext_bvec = | ||
| BoundedVec::<u8, HEADER_CIPHERTEXT_SIZE_IN_BYTES>::from_array(header_ciphertext); | ||
|
|
||
| // Decrypt header | ||
| let header_plaintext = aes128_decrypt_oracle(header_ciphertext_bvec, iv, sym_key); | ||
|
|
||
| // Extract ciphertext length from header (2 bytes, big-endian) | ||
| let ciphertext_length = | ||
| ((header_plaintext.get(0) as u32) << 8) | (header_plaintext.get(1) as u32); | ||
|
|
||
| // Extract and decrypt main ciphertext | ||
| let ciphertext_start = header_start + HEADER_CIPHERTEXT_SIZE_IN_BYTES; | ||
| let ciphertext_with_padding: [u8; (PRIVATE_LOG_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES] = | ||
| array::subarray(ciphertext_without_eph_pk_x.storage(), ciphertext_start); | ||
| let ciphertext: BoundedVec<u8, (PRIVATE_LOG_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES> = | ||
| BoundedVec::from_parts(ciphertext_with_padding, ciphertext_length); | ||
|
|
||
| // Decrypt main ciphertext and return it | ||
| let plaintext_bytes = aes128_decrypt_oracle(ciphertext, iv, sym_key); | ||
|
|
||
| // Each field of the original note log was serialized to 32 bytes so we convert the bytes back to fields. | ||
| fields_from_bytes(plaintext_bytes) | ||
| } | ||
| } | ||
|
|
||
| mod test { | ||
| use crate::{ | ||
| encrypted_logs::log_encryption::{LogEncryption, PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS}, | ||
| keys::ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, | ||
| test::helpers::test_environment::TestEnvironment, | ||
| }; | ||
| use super::AES128; | ||
| use protocol_types::{address::AztecAddress, traits::FromField}; | ||
| use std::{embedded_curve_ops::EmbeddedCurveScalar, test::OracleMock}; | ||
|
|
||
| #[test] | ||
| unconstrained fn encrypt_decrypt_log() { | ||
| let mut env = TestEnvironment::new(); | ||
| // Advance 1 block so we can read historic state from private | ||
| env.advance_block_by(1); | ||
|
|
||
| let plaintext = [1, 2, 3]; | ||
|
|
||
| let recipient = AztecAddress::from_field( | ||
| 0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c, | ||
| ); | ||
|
|
||
| // Mock random values for deterministic test | ||
| let eph_sk = 0x1358d15019d4639393d62b97e1588c095957ce74a1c32d6ec7d62fe6705d9538; | ||
| let _ = OracleMock::mock("getRandomField").returns(eph_sk).times(1); | ||
|
|
||
| let randomness = 0x0101010101010101010101010101010101010101010101010101010101010101; | ||
| let _ = OracleMock::mock("getRandomField").returns(randomness).times(1000000); | ||
|
|
||
| let _ = OracleMock::mock("getIndexedTaggingSecretAsSender").returns([69420, 1337]); | ||
| let _ = OracleMock::mock("incrementAppTaggingSecretIndexAsSender").returns(()); | ||
|
|
||
| // Encrypt the log | ||
| let encrypted_log = BoundedVec::from_array(AES128::encrypt_log(plaintext, recipient)); | ||
|
|
||
| // Mock shared secret for deterministic test | ||
| let shared_secret = derive_ecdh_shared_secret_using_aztec_address( | ||
| EmbeddedCurveScalar::from_field(eph_sk), | ||
| recipient, | ||
| ); | ||
| let _ = OracleMock::mock("getSharedSecret").returns(shared_secret); | ||
|
|
||
| // Decrypt the log | ||
| let decrypted = AES128::decrypt_log(encrypted_log, recipient); | ||
|
|
||
| // The decryption function spits out a BoundedVec because it's designed to work with logs with unknown length | ||
| // at compile time. For this reason we need to convert the original input to a BoundedVec. | ||
| let plaintext_bvec = | ||
| BoundedVec::<Field, PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS>::from_array(plaintext); | ||
|
|
||
| // Verify decryption matches original plaintext | ||
| assert_eq(decrypted, plaintext_bvec, "Decrypted bytes should match original plaintext"); | ||
|
|
||
| // The following is a workaround of "struct is never constructed" Noir compilation error (we only ever use | ||
| // static methods of the struct). | ||
| let _ = AES128 {}; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.