-
Notifications
You must be signed in to change notification settings - Fork 598
feat: aes decryption oracle #11907
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
feat: aes decryption oracle #11907
Changes from all commits
8c304f0
af0d7a1
118709d
1f706f6
85b38eb
e57216d
2c6a6e2
bbecb1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| /// Decrypts a ciphertext, using AES128. | ||
| /// | ||
| /// Returns a padded plaintext, of the same size as the input ciphertext. | ||
| /// Note that between 1-16 bytes at the end of the returned plaintext will be pkcs#7 padding. | ||
| /// It's up to the calling function to identify and remove that padding. | ||
| /// See the tests below for an example of how. | ||
| /// It's up to the calling function to determine whether decryption succeeded or failed. | ||
| /// See the tests below for an example of how. | ||
| unconstrained fn aes128_decrypt_oracle_wrapper<let N: u32>( | ||
| ciphertext: [u8; N], | ||
| iv: [u8; 16], | ||
| sym_key: [u8; 16], | ||
| ) -> [u8; N] { | ||
| aes128_decrypt_oracle(ciphertext, iv, sym_key) | ||
| } | ||
|
|
||
| #[oracle(aes128Decrypt)] | ||
| unconstrained fn aes128_decrypt_oracle<let N: u32>( | ||
| ciphertext: [u8; N], | ||
| iv: [u8; 16], | ||
| sym_key: [u8; 16], | ||
| ) -> [u8; N] {} | ||
|
|
||
| mod test { | ||
| use crate::{ | ||
| encrypted_logs::encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, | ||
| utils::point::point_from_x_coord, | ||
| }; | ||
| use super::aes128_decrypt_oracle_wrapper; | ||
| use std::aes128::aes128_encrypt; | ||
|
|
||
| #[test] | ||
| unconstrained fn aes_encrypt_then_decrypt() { | ||
| let ciphertext_shared_secret = point_from_x_coord(1); | ||
|
|
||
| let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( | ||
| ciphertext_shared_secret, | ||
| ); | ||
|
|
||
| let plaintext: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; | ||
|
|
||
| let ciphertext = aes128_encrypt(plaintext, iv, sym_key); | ||
|
|
||
| let received_plaintext = aes128_decrypt_oracle_wrapper(ciphertext, iv, sym_key); | ||
| let padding_length = received_plaintext[received_plaintext.len() - 1] as u32; | ||
|
|
||
| // A BoundedVec could also be used. | ||
| let mut received_plaintext_with_padding_removed = std::collections::vec::Vec::new(); | ||
| for i in 0..received_plaintext.len() - padding_length { | ||
| received_plaintext_with_padding_removed.push(received_plaintext[i]); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing the padding will be done basically always, right? Would it make sense to just have a utility function for it? Or have function calling the oracle and removing the padding and then returning pure plaintext? But if it's just for demonstration purposes now and it will be considered later then I have no issue with it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, it would be good to eventually have a utility function for this, because you're right: everyone will want to do it. I chose not to here, because I wasn't sure what type to choose (between a slice or a |
||
|
|
||
| assert_eq(received_plaintext_with_padding_removed.slice, plaintext.as_slice()); | ||
| } | ||
|
|
||
| global TEST_PLAINTEXT_LENGTH: u32 = 10; | ||
| global TEST_MAC_LENGTH: u32 = 32; | ||
|
|
||
| #[test(should_fail_with = "mac does not match")] | ||
| unconstrained fn aes_encrypt_then_decrypt_with_bad_sym_key_is_caught() { | ||
| // The AES decryption oracle will not fail for any ciphertext; it will always | ||
| // return some data. As for whether the decryption was successful, it's up | ||
| // to the app to check this in a custom way. | ||
| // E.g. if it's a note that's been encrypted, then upon decryption, the app | ||
| // can check to see if the note hash exists onchain. If it doesn't exist | ||
| // onchain, then that's a strong indicator that decryption has failed. | ||
| // E.g. for non-note messages, the plaintext could include a MAC. We | ||
nventuro marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // demonstrate what this could look like in this test. | ||
| // | ||
| // We compute a MAC and we include that MAC in the plaintext. We then encrypt | ||
| // this plaintext to get a ciphertext. We broadcast the [ciphertext, mac] | ||
| // tuple. The eventual decryptor will expect the mac in the decrypted plaintext | ||
| // to match the mac that was broadcast. If not, the recipient knows that | ||
| // decryption has failed. | ||
| let ciphertext_shared_secret = point_from_x_coord(1); | ||
|
|
||
| let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( | ||
| ciphertext_shared_secret, | ||
| ); | ||
|
|
||
| let mac_preimage = 0x42; | ||
| let mac = std::hash::poseidon2::Poseidon2::hash([mac_preimage], 1); | ||
| let mac_as_bytes = mac.to_be_bytes::<TEST_MAC_LENGTH>(); | ||
|
|
||
| let plaintext: [u8; TEST_PLAINTEXT_LENGTH] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; | ||
|
|
||
| // We append the mac to the plaintext. It doesn't necessarily have to be 32 bytes; | ||
| // that's quite an extreme length. 16 bytes or 8 bytes might be sufficient, and would | ||
| // save on data broadcasting costs. | ||
| let mut plaintext_with_mac = [0 as u8; TEST_PLAINTEXT_LENGTH + TEST_MAC_LENGTH]; | ||
| for i in 0..TEST_PLAINTEXT_LENGTH { | ||
| plaintext_with_mac[i] = plaintext[i]; | ||
| } | ||
| for i in 0..TEST_MAC_LENGTH { | ||
| plaintext_with_mac[TEST_PLAINTEXT_LENGTH + i] = mac_as_bytes[i]; | ||
| } | ||
|
|
||
| let ciphertext = aes128_encrypt(plaintext_with_mac, iv, sym_key); | ||
|
|
||
| // We now would broadcast the tuple [ciphertext, mac] to the network. | ||
| // The recipient will then decrypt the ciphertext, and if the mac inside the | ||
| // received plaintext matches the mac that was broadcast, then the recipient | ||
| // knows that decryption was successful. | ||
|
|
||
| // For this test, we intentionally mutate the sym_key to a bad one, so that | ||
| // decryption fails. This allows us to explore how the recipient can detect | ||
| // failed decryption by checking the decrypted mac against the broadcasted | ||
| // mac. | ||
| let mut bad_sym_key = sym_key; | ||
| bad_sym_key[0] = 0; | ||
|
|
||
| let received_plaintext = aes128_decrypt_oracle_wrapper(ciphertext, iv, bad_sym_key); | ||
|
|
||
| let mut extracted_mac_as_bytes = [0 as u8; TEST_MAC_LENGTH]; | ||
| for i in 0..TEST_MAC_LENGTH { | ||
| extracted_mac_as_bytes[i] = received_plaintext[TEST_PLAINTEXT_LENGTH + i]; | ||
| } | ||
|
|
||
| // We expect this assertion to fail, because we used a bad sym key. | ||
| assert_eq(mac_as_bytes, extracted_mac_as_bytes, "mac does not match"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,6 +81,7 @@ import { | |
| type NoteData, | ||
| Oracle, | ||
| type TypedOracle, | ||
| ViewDataOracle, | ||
| WASMSimulator, | ||
| extractCallStack, | ||
| extractPrivateCircuitPublicInputs, | ||
|
|
@@ -115,6 +116,7 @@ export class TXE implements TypedOracle { | |
|
|
||
| private contractDataOracle: ContractDataOracle; | ||
| private simulatorOracle: SimulatorOracle; | ||
| private viewDataOracle: ViewDataOracle; | ||
|
|
||
| private publicDataWrites: PublicDataWrite[] = []; | ||
| private uniqueNoteHashesFromPublic: Fr[] = []; | ||
|
|
@@ -159,6 +161,15 @@ export class TXE implements TypedOracle { | |
| this.simulationProvider, | ||
| ); | ||
|
|
||
| this.viewDataOracle = new ViewDataOracle( | ||
| this.contractAddress, | ||
| [] /* authWitnesses */, | ||
| this.simulatorOracle, // note: SimulatorOracle implements DBOracle | ||
| this.node, | ||
| /* log, */ | ||
| /* scopes, */ | ||
| ); | ||
|
|
||
| this.debug = createDebugOnlyLogger('aztec:kv-pxe-database'); | ||
| } | ||
|
|
||
|
|
@@ -1187,4 +1198,8 @@ export class TXE implements TypedOracle { | |
| } | ||
| return this.txeDatabase.copyCapsule(this.contractAddress, srcSlot, dstSlot, numEntries); | ||
| } | ||
|
|
||
| aes128Decrypt(ciphertext: Buffer, iv: Buffer, symKey: Buffer): Promise<Buffer> { | ||
| return this.viewDataOracle.aes128Decrypt(ciphertext, iv, symKey); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm re-using the implementation from the viewDataOracle. |
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.