Skip to content

Commit 76b1131

Browse files
feat(bindings): expose X-Wing post-quantum KEM in all wrappers
Expose the X-Wing hybrid KEM (ML-KEM-768 + X25519) added to the Rust core (SEC-15167) through every binding. This is opt-in: the default key and ciphertext versions are unchanged. - FFI: GenerateKeyPairWithVersion, GenerateKeyPairPrivateSize, GenerateKeyPairPublicSize, Encapsulate, EncapsulateSize, Decapsulate. Existing signatures are unchanged. EncryptAsymmetricSize accepts V3 and EncryptAsymmetric returns InvalidOutputLength instead of panicking on a buffer size mismatch. - uniffi (Python, Kotlin, Swift): generateKeypair(version), encryptAsymmetric(..., version), encapsulate, decapsulate, EncapsulationResult. - WASM: encapsulate, decapsulate, EncapsulationResult. - C#: KeyVersion / KeyEncapsulationVersion enums, CipherTextVersion.V3, DataType.EncapsulatedKey, GenerateKeyPair(KeyVersion) overload, Encapsulate / Decapsulate. - CLI: generate-keypair --version, encapsulate, decapsulate. - Fuzz: key_encapsulation targets. - Angular demo: X-Wing key generation, V3 encryption, encapsulate and decapsulate, and inspector support. - Conformity vectors replicated in the Python, WASM, C#, Kotlin and Swift test suites. The Kotlin and Swift tests have not been run locally. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 9c501f5 commit 76b1131

25 files changed

Lines changed: 1727 additions & 26 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1212

1313
### Added
1414

15-
- Post-quantum hybrid public-key cryptography using X-Wing (ML-KEM-768 + X25519): `KeyVersion::V2` key pairs, `CiphertextVersion::V3` asymmetric ciphertexts, and a new `key_encapsulation` module (`encapsulate` / `decapsulate`, `DataType::EncapsulatedKey`). This is opt-in: `Latest` still selects the existing versions.
15+
- Post-quantum hybrid public-key cryptography using X-Wing (ML-KEM-768 + X25519): `KeyVersion::V2` key pairs, `CiphertextVersion::V3` asymmetric ciphertexts, and a new `key_encapsulation` module (`encapsulate` / `decapsulate`, `DataType::EncapsulatedKey`). Available in the Rust, FFI/C#, uniffi (Python, Kotlin, Swift), WASM and CLI wrappers. This is opt-in: `Latest` still selects the existing versions.
1616

1717
## [0.10.2] - 2026-09-14
1818

‎cli/src/main.rs‎

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ enum Commands {
1919
Generate,
2020

2121
/// Generate a random keypair
22-
GenerateKeypair,
22+
GenerateKeypair {
23+
/// The key version. Use 2 for an X-Wing post-quantum keypair.
24+
#[arg(short, long)]
25+
version: Option<u16>,
26+
},
2327

2428
/// Generate secret key with parts shared accross multiple parties.
2529
#[command(arg_required_else_help = true)]
@@ -120,6 +124,27 @@ enum Commands {
120124
key: String,
121125
},
122126

127+
/// Encapsulate a fresh shared secret to a version 2 (X-Wing) public key
128+
#[command(arg_required_else_help = true)]
129+
Encapsulate {
130+
/// The public key to encapsulate to
131+
key: String,
132+
133+
/// The encapsulation version to use
134+
#[arg(short, long)]
135+
version: Option<u16>,
136+
},
137+
138+
/// Recover a shared secret from an encapsulated key using a version 2 (X-Wing) private key
139+
#[command(arg_required_else_help = true)]
140+
Decapsulate {
141+
/// The encapsulated key
142+
data: String,
143+
144+
/// The private key to use
145+
key: String,
146+
},
147+
123148
/// Hash a password
124149
#[command(arg_required_else_help = true)]
125150
HashPassword {
@@ -196,7 +221,7 @@ fn main() {
196221

197222
match cli.command {
198223
Commands::Generate => generate_key(),
199-
Commands::GenerateKeypair => generate_keypair(),
224+
Commands::GenerateKeypair { version } => generate_keypair(version),
200225
Commands::GenerateArgon2Parameters {
201226
memory,
202227
lanes,
@@ -219,6 +244,8 @@ fn main() {
219244
}
220245
Commands::Decrypt { data, key } => decrypt(data, key),
221246
Commands::DecryptAsymmetric { data, key } => decrypt_asymmetric(data, key),
247+
Commands::Encapsulate { key, version } => encapsulate(key, version),
248+
Commands::Decapsulate { data, key } => decapsulate(data, key),
222249
Commands::HashPassword { password, params } => hash_password(password, params),
223250
Commands::VerifyPassword { password, hash } => verify_password(hash, password),
224251
Commands::MixKeyExchange { private, public } => mix_key_exchange(private, public),
@@ -354,6 +381,7 @@ fn detect_dc_type(bytes: &[u8]) -> String {
354381
Ok(DataType::Signature) => "Signature".to_string(),
355382
Ok(DataType::KeyDerivation) => "DerivationParameters".to_string(),
356383
Ok(DataType::KdfEncryptedData) => "KdfEncryptedData".to_string(),
384+
Ok(DataType::EncapsulatedKey) => "EncapsulatedKey".to_string(),
357385
_ => "unknown".to_string(),
358386
}
359387
}
@@ -504,8 +532,13 @@ fn verify_password(hash: String, password: String) {
504532
println!("{}", hash.verify_password(password.as_bytes()));
505533
}
506534

507-
fn generate_keypair() {
508-
let keypair = devolutions_crypto::key::generate_keypair(Default::default());
535+
fn generate_keypair(version: Option<u16>) {
536+
let version = devolutions_crypto::key::KeyVersion::try_from(version.unwrap_or(0))
537+
.unwrap_or_else(|_| {
538+
eprintln!("Error: 'version' - unknown key version.");
539+
std::process::exit(1);
540+
});
541+
let keypair = devolutions_crypto::key::generate_keypair(version);
509542

510543
println!(
511544
"Private Key: {}\nPublic Key: {}",
@@ -514,6 +547,66 @@ fn generate_keypair() {
514547
);
515548
}
516549

550+
fn encapsulate(key: String, version: Option<u16>) {
551+
use devolutions_crypto::key::PublicKey;
552+
use devolutions_crypto::key_encapsulation::{KeyEncapsulationVersion, encapsulate};
553+
554+
let key_bytes = decode_base64_arg("key", &key);
555+
let key = PublicKey::try_from(key_bytes.as_slice()).unwrap_or_else(|_| {
556+
eprintln!(
557+
"Error: 'key' - expected PublicKey, received {}.",
558+
detect_dc_type(&key_bytes)
559+
);
560+
std::process::exit(1);
561+
});
562+
563+
let version = KeyEncapsulationVersion::try_from(version.unwrap_or(0)).unwrap_or_else(|_| {
564+
eprintln!("Error: 'version' - unknown encapsulation version.");
565+
std::process::exit(1);
566+
});
567+
568+
let (encapsulated, secret) = encapsulate(&key, version).unwrap_or_else(|e| {
569+
eprintln!("Error: encapsulation failed: {}.", e);
570+
std::process::exit(1);
571+
});
572+
573+
println!(
574+
"Encapsulated Key: {}\nSecret Key: {}",
575+
base64_encode(&Vec::<u8>::from(encapsulated)),
576+
base64_encode(&Vec::<u8>::from(secret))
577+
);
578+
}
579+
580+
fn decapsulate(data: String, key: String) {
581+
use devolutions_crypto::key::PrivateKey;
582+
use devolutions_crypto::key_encapsulation::{EncapsulatedKey, decapsulate};
583+
584+
let data_bytes = decode_base64_arg("data", &data);
585+
let encapsulated = EncapsulatedKey::try_from(data_bytes.as_slice()).unwrap_or_else(|_| {
586+
eprintln!(
587+
"Error: 'data' - expected EncapsulatedKey, received {}.",
588+
detect_dc_type(&data_bytes)
589+
);
590+
std::process::exit(1);
591+
});
592+
593+
let key_bytes = decode_base64_arg("key", &key);
594+
let key = PrivateKey::try_from(key_bytes.as_slice()).unwrap_or_else(|_| {
595+
eprintln!(
596+
"Error: 'key' - expected PrivateKey, received {}.",
597+
detect_dc_type(&key_bytes)
598+
);
599+
std::process::exit(1);
600+
});
601+
602+
let secret: devolutions_crypto::key::SecretKey = decapsulate(&key, &encapsulated)
603+
.unwrap_or_else(|e| {
604+
eprintln!("Error: decapsulation failed: {}.", e);
605+
std::process::exit(1);
606+
});
607+
println!("{}", base64_encode(&Vec::<u8>::from(secret)));
608+
}
609+
517610
fn mix_key_exchange(private: String, public: String) {
518611
let private =
519612
devolutions_crypto::key::PrivateKey::try_from(base64_decode(&private).unwrap().as_slice())

0 commit comments

Comments
 (0)