diff --git a/CLAUDE.md b/CLAUDE.md index 3ea9a1a7..78d357f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -136,7 +136,11 @@ Repo mechanics behind those rules, which the documents don't spell out: - `./dev_scripts/quality_stats.sh` produces the fallibility metrics both documents ask you to check. Run it before and after a change and compare, rather than eyeballing the diff. - **CLI commands stream.** The `cli/` binary is stdin→stdout with ~1 KB buffers so commands compose in shell - pipelines; preserve that when adding subcommands. + pipelines; preserve that when adding subcommands. The exception is a construction that is not + itself streamable, such as CCM (SP 800-38C Sec 3: "CCM is not designed to support partial + processing or stream processing", because the payload length is inside the first block the MAC + covers) -- there, read the whole input once and process it in place, rather than adding a second + buffer the size of the input on top of it; see `aes_ccm_cmd.rs`. - Trait → factory → CLI is the wiring path for a new primitive; see [the workspace architecture](#the-core--core-test-framework--factory-spine) above for the crates involved. ## Scope of changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f250d37..cf6575c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,10 +43,10 @@ Some specifics: * Public APIs of a library should be both ergonomic and expressive. When defining a new trait or public function, ask yourself whether a programmer who is new to cryptography is likely to use this in a way that will get them into trouble. -* Variables should be well-named, well-structured, and well-commented (a comment-to-code ration of 1:1 is a goal to be +* Variables should be well-named, well-structured, and well-commented (a comment-to-code ratio of 1:1 is a goal to be strived for!). Think about memory footprint and, where possible, use unnamed scopes to allow the compiler to pop intermediate value variables off the stack as soon as they are no longer needed. -* Always run your code through `cargo mutants` and get the issue count as low as your can. As a first pass, this forces +* Always run your code through `cargo mutants` and get the issue count as low as you can. As a first pass, this forces you to write thorough unit tests. As a second pass, this draws your attention to bits of your code that cannot be tested from the outside. Often this means that the code can be simplified without affecting functionality (as defined by your set of unit tests) -- "simpler code" usually means faster runtime and easier future maintenance. @@ -71,7 +71,7 @@ For minor updates, you can instead choose to create an issue with short snippets * For contributions touching multiple files try and split up the pull request, smaller changes are easier to review and test, as well as being less likely to run into merge issues. -* Create a test cases for your change, it may be a simple addition to an existing test. If you do not know how to do +* Create test cases for your change; it may be a simple addition to an existing test. If you do not know how to do this, ask us and we will help you. * If you run into any merge issues, check out this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues. diff --git a/Cargo.toml b/Cargo.toml index 63f0d999..7aa567d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ version = "0.1.3" # *** Internal Dependencies *** bouncycastle = { path = "./" } bouncycastle-aes = { path = "./crypto/aes" } +bouncycastle-ascon = { path = "./crypto/ascon" } bouncycastle-base64 = { path = "./crypto/base64" } bouncycastle-modes = { path = "./crypto/modes" } bouncycastle-core = { path = "crypto/core" } @@ -46,6 +47,7 @@ edition.workspace = true [dependencies] bouncycastle-aes.workspace = true +bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true bouncycastle-core.workspace = true bouncycastle-factory.workspace = true diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index d5185528..4ce74352 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -5,6 +5,46 @@ * New algorithms added to crypto/ : * SM3 -- the SM3 hash (GB/T 32905-2016 / ISO/IEC 10118-3:2018), ported from bc-java. * AES -- AES-128/192/256, along with its modes AES_ECB, AES_CBC, AES_GCM. + * ASCON -- Ascon-AEAD128, Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 (NIST SP 800-232). + `AsconAead128Encryptor` / `AsconAead128Decryptor` implement the generated-nonce + `AEADCipherEncryptor` / `AEADCipherDecryptor` pair; the inherent `AsconAead128` API keeps the + explicit-nonce, in-place streaming form (`new_encrypting` / `new_decrypting`). + `Ascon_AEAD128
(
+ action: &BlockModeAction,
+ key: &KeyMaterial (
+ key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex,
+ ),
+ 6 => go:: (
+ key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex,
+ ),
+ 8 => go:: (
+ key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex,
+ ),
+ 10 => go:: (
+ key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex,
+ ),
+ 12 => go:: (
+ key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex,
+ ),
+ 14 => go:: (
+ key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex,
+ ),
+ 16 => go:: (
+ key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex,
+ ),
+ _ => unreachable!("tag length was validated before stdin was read"),
+ }
+ };
+ }
+
+ // `load_nonce` has already rejected anything outside 7..=13, so the fall-through is unreachable;
+ // it is spelled out rather than `unreachable!()` so this cannot panic on a future edit.
+ match nonce_bytes.len() {
+ 7 => with_tag_len!(7),
+ 8 => with_tag_len!(8),
+ 9 => with_tag_len!(9),
+ 10 => with_tag_len!(10),
+ 11 => with_tag_len!(11),
+ 12 => with_tag_len!(12),
+ 13 => with_tag_len!(13),
+ other => {
+ eprintln!("Error: nonce is {other} bytes; CCM requires 7 to 13.");
+ exit(-1)
+ }
+ }
+}
+
+/// Reports [`Ccm::new`]'s refusal of a payload past the `q` limit and exits.
+///
+/// The only [`SymmetricCipherError::GenericError`] `new` can return is that limit: A.1's
+/// `p < 2^8q`, where `q = 15 - n`. Both directions hit it -- the decrypt side on the input minus
+/// its tag -- so both report it here, with the numbers, since the fix is a shorter nonce.
+fn payload_past_the_q_limit (
+ msg: &str,
+ payload_len: usize,
+) -> !
+where
+ P: ElectronicCodeBook ::MAX_PAYLOAD_LEN,
+ );
+ eprintln!(" Use a shorter nonce for a larger payload.");
+ exit(-1)
+}
+
+/// One fully-instantiated CCM run.
+///
+/// `input` is processed in place through [`Ccm`]'s own streaming API rather than through the
+/// one-shot [`Ccm::encrypt`]/[`Ccm::decrypt`], which each need a second, freshly allocated buffer
+/// the size of `input`: the declared-length constructor already has everything a one-shot needs,
+/// so there is no second buffer to allocate or copy into.
+fn go (
+ key: &KeyMaterial =
+ Ccm ;
+ type Dec =
+ Ccm ;
+
+ // `run` dispatched on this exact length, so the conversion cannot fail.
+ let Ok(nonce) = <[u8; NONCE_LEN]>::try_from(nonce_bytes) else {
+ eprintln!("Error: internal nonce length mismatch.");
+ exit(-1)
+ };
+
+ if encrypt {
+ match Enc:: ::new(key, &nonce, aad, input.len()) {
+ Ok(mut ccm) => {
+ // `new` already accepted this exact length as `input.len()`, and this is the one
+ // and only call supplying it, so `take_owed` can never see too much and `owed`
+ // can never be left nonzero: neither of these can fail on the path that reaches
+ // them.
+ ccm.do_encrypt_update(&mut input).expect("declared length matches what was sent");
+ let tag = ccm.do_encrypt_final().expect("declared length was fully supplied");
+ helpers::write_bytes_or_hex(&input, output_hex);
+ helpers::write_bytes_or_hex(&tag, output_hex);
+ if output_hex {
+ println!();
+ }
+ }
+ Err(SymmetricCipherError::GenericError(msg)) => {
+ payload_past_the_q_limit:: (msg, input.len())
+ }
+ Err(e) => {
+ eprintln!("Error: AES-CCM encryption failed: {e:?}");
+ exit(-1)
+ }
+ }
+ } else {
+ // `split_last_chunk_mut` is `None` exactly when there is no room for a `TAG_LEN`-byte tag,
+ // which is the same octet-level test (and the same allowance for an empty payload plus its
+ // tag) that `Ccm::decrypt`'s own doc comment explains for Sec 6.2 step 1.
+ let Some((data, tag)) = input.split_last_chunk_mut:: ::new(key, &nonce, aad, data.len()) {
+ Ok(mut ccm) => {
+ // As the encrypt arm above: `data.len()` is exactly the length just declared, and
+ // it is supplied in this one call, so this cannot fail.
+ ccm.do_decrypt_update(data).expect("declared length matches what was sent");
+ match ccm.do_decrypt_final(tag) {
+ Ok(()) => {
+ helpers::write_bytes_or_hex(data, output_hex);
+ if output_hex {
+ println!();
+ }
+ }
+ Err(SymmetricCipherError::AEADTagCheckFailed) => {
+ // Nothing has been written to stdout at this point, which is what
+ // processing in place still buys here: Sec 6.2's "the payload P and the
+ // MAC T shall not be revealed" holds end to end.
+ eprintln!(
+ "Error: AES-CCM authentication failed; the input is not authentic."
+ );
+ exit(-1)
+ }
+ Err(e) => {
+ eprintln!("Error: AES-CCM decryption failed: {e:?}");
+ exit(-1)
+ }
+ }
+ }
+ Err(SymmetricCipherError::GenericError(msg)) => {
+ payload_past_the_q_limit:: (msg, data.len())
+ }
+ Err(e) => {
+ eprintln!("Error: AES-CCM decryption failed: {e:?}");
+ exit(-1)
+ }
+ }
+ }
+}
diff --git a/cli/src/ascon_cmd.rs b/cli/src/ascon_cmd.rs
new file mode 100644
index 00000000..ad37f25e
--- /dev/null
+++ b/cli/src/ascon_cmd.rs
@@ -0,0 +1,290 @@
+use std::io::{self, Read};
+use std::process::exit;
+
+use bouncycastle::ascon::ascon_aead128::{
+ AsconAead128, AsconAead128Decryptor, AsconAead128Encryptor,
+};
+use bouncycastle::ascon::ascon_cxof128::AsconCXof128;
+use bouncycastle::ascon::ascon_hash256::AsconHash256;
+use bouncycastle::ascon::ascon_xof128::AsconXof128;
+use bouncycastle::core::errors::SymmetricCipherError;
+use bouncycastle::core::key_material::{
+ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations,
+};
+use bouncycastle::core::security_strength::SecurityStrength;
+use bouncycastle::core::traits::{
+ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor,
+};
+use bouncycastle::hex;
+
+use crate::helpers;
+
+/// Load a hex string or a binary/hex file into bytes; exits with an error if neither is supplied.
+fn load_bytes(value: &Option