1use core::fmt;
10
11use crate::public_key::bigint::{BigUint, MontgomeryCtx};
12use crate::public_key::io::{
13 decode_biguints, encode_biguints, pem_unwrap, pem_wrap, xml_unwrap, xml_wrap,
14};
15use crate::public_key::primes::{
16 is_probable_prime, lcm, mod_inverse, mod_pow, random_probable_prime,
17};
18use crate::Csprng;
19
20const SCHMIDT_SAMOA_PUBLIC_LABEL: &str = "CRYPTOGRAPHY SCHMIDT-SAMOA PUBLIC KEY";
21const SCHMIDT_SAMOA_PRIVATE_LABEL: &str = "CRYPTOGRAPHY SCHMIDT-SAMOA PRIVATE KEY";
22
23#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct SchmidtSamoaPublicKey {
26 n: BigUint,
27 n_ctx: Option<MontgomeryCtx>,
28}
29
30#[derive(Clone, Eq, PartialEq)]
32pub struct SchmidtSamoaPrivateKey {
33 d: BigUint,
34 gamma: BigUint,
35 gamma_ctx: Option<MontgomeryCtx>,
36}
37
38pub struct SchmidtSamoa;
40
41impl SchmidtSamoaPublicKey {
42 #[must_use]
44 pub fn modulus(&self) -> &BigUint {
45 &self.n
46 }
47
48 #[must_use]
54 pub fn max_plaintext_exclusive(&self) -> BigUint {
55 self.n.sqrt_floor()
56 }
57
58 #[must_use]
64 pub fn encrypt_raw(&self, message: &BigUint) -> BigUint {
65 if let Some(ctx) = &self.n_ctx {
66 ctx.pow(message, &self.n)
67 } else {
68 mod_pow(message, &self.n, &self.n)
69 }
70 }
71
72 #[must_use]
74 pub fn encrypt(&self, message: &[u8]) -> Option<BigUint> {
75 let message_int = BigUint::from_be_bytes(message);
76 if message_int >= self.max_plaintext_exclusive() {
77 return None;
78 }
79 Some(self.encrypt_raw(&message_int))
80 }
81
82 #[must_use]
84 pub fn encrypt_bytes(&self, message: &[u8]) -> Option<Vec<u8>> {
85 let ciphertext = self.encrypt(message)?;
86 Some(encode_biguints(&[&ciphertext]))
87 }
88
89 #[must_use]
91 pub fn to_key_blob(&self) -> Vec<u8> {
92 encode_biguints(&[&self.n])
93 }
94
95 #[must_use]
97 pub fn from_key_blob(blob: &[u8]) -> Option<Self> {
98 let mut fields = decode_biguints(blob)?.into_iter();
99 let n = fields.next()?;
100 if fields.next().is_some() || n <= BigUint::one() {
101 return None;
102 }
103 let n_ctx = MontgomeryCtx::new(&n);
104 Some(Self { n, n_ctx })
105 }
106
107 #[must_use]
109 pub fn to_pem(&self) -> String {
110 pem_wrap(SCHMIDT_SAMOA_PUBLIC_LABEL, &self.to_key_blob())
111 }
112
113 #[must_use]
115 pub fn to_xml(&self) -> String {
116 xml_wrap("SchmidtSamoaPublicKey", &[("n", &self.n)])
117 }
118
119 #[must_use]
121 pub fn from_pem(pem: &str) -> Option<Self> {
122 let blob = pem_unwrap(SCHMIDT_SAMOA_PUBLIC_LABEL, pem)?;
123 Self::from_key_blob(&blob)
124 }
125
126 #[must_use]
128 pub fn from_xml(xml: &str) -> Option<Self> {
129 let mut fields = xml_unwrap("SchmidtSamoaPublicKey", &["n"], xml)?.into_iter();
130 let n = fields.next()?;
131 if fields.next().is_some() || n <= BigUint::one() {
132 return None;
133 }
134 let n_ctx = MontgomeryCtx::new(&n);
135 Some(Self { n, n_ctx })
136 }
137}
138
139impl SchmidtSamoaPrivateKey {
140 #[must_use]
142 pub fn exponent(&self) -> &BigUint {
143 &self.d
144 }
145
146 #[must_use]
148 pub fn gamma(&self) -> &BigUint {
149 &self.gamma
150 }
151
152 #[must_use]
157 pub fn decrypt_raw(&self, ciphertext: &BigUint) -> BigUint {
158 if let Some(ctx) = &self.gamma_ctx {
159 ctx.pow(ciphertext, &self.d)
160 } else {
161 mod_pow(ciphertext, &self.d, &self.gamma)
162 }
163 }
164
165 #[must_use]
168 pub fn decrypt(&self, ciphertext: &BigUint) -> Vec<u8> {
169 self.decrypt_raw(ciphertext).to_be_bytes()
170 }
171
172 #[must_use]
174 pub fn decrypt_bytes(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
175 let mut fields = decode_biguints(ciphertext)?.into_iter();
176 let value = fields.next()?;
177 if fields.next().is_some() {
178 return None;
179 }
180 Some(self.decrypt(&value))
181 }
182
183 #[must_use]
185 pub fn to_key_blob(&self) -> Vec<u8> {
186 encode_biguints(&[&self.d, &self.gamma])
187 }
188
189 #[must_use]
191 pub fn from_key_blob(blob: &[u8]) -> Option<Self> {
192 let mut fields = decode_biguints(blob)?.into_iter();
193 let d = fields.next()?;
194 let gamma = fields.next()?;
195 if fields.next().is_some() || d.is_zero() || gamma <= BigUint::one() {
196 return None;
197 }
198 let gamma_ctx = MontgomeryCtx::new(&gamma);
199 Some(Self {
200 d,
201 gamma,
202 gamma_ctx,
203 })
204 }
205
206 #[must_use]
208 pub fn to_pem(&self) -> String {
209 pem_wrap(SCHMIDT_SAMOA_PRIVATE_LABEL, &self.to_key_blob())
210 }
211
212 #[must_use]
214 pub fn to_xml(&self) -> String {
215 xml_wrap(
216 "SchmidtSamoaPrivateKey",
217 &[("d", &self.d), ("gamma", &self.gamma)],
218 )
219 }
220
221 #[must_use]
223 pub fn from_pem(pem: &str) -> Option<Self> {
224 let blob = pem_unwrap(SCHMIDT_SAMOA_PRIVATE_LABEL, pem)?;
225 Self::from_key_blob(&blob)
226 }
227
228 #[must_use]
230 pub fn from_xml(xml: &str) -> Option<Self> {
231 let mut fields = xml_unwrap("SchmidtSamoaPrivateKey", &["d", "gamma"], xml)?.into_iter();
232 let d = fields.next()?;
233 let gamma = fields.next()?;
234 if fields.next().is_some() || d.is_zero() || gamma <= BigUint::one() {
235 return None;
236 }
237 let gamma_ctx = MontgomeryCtx::new(&gamma);
238 Some(Self {
239 d,
240 gamma,
241 gamma_ctx,
242 })
243 }
244}
245
246impl fmt::Debug for SchmidtSamoaPrivateKey {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 f.write_str("SchmidtSamoaPrivateKey(<redacted>)")
249 }
250}
251
252impl SchmidtSamoa {
253 #[must_use]
258 pub fn from_primes(
259 p: &BigUint,
260 q: &BigUint,
261 ) -> Option<(SchmidtSamoaPublicKey, SchmidtSamoaPrivateKey)> {
262 if p == q || !is_probable_prime(p) || !is_probable_prime(q) {
263 return None;
264 }
265
266 let p_minus_one = p.sub_ref(&BigUint::one());
267 let q_minus_one = q.sub_ref(&BigUint::one());
268 if q_minus_one.modulo(p).is_zero() || p_minus_one.modulo(q).is_zero() {
272 return None;
273 }
274
275 let gamma = p.mul_ref(q);
276 let lambda = lcm(&p_minus_one, &q_minus_one);
277 let p_squared = p.mul_ref(p);
278 let n = p_squared.mul_ref(q);
279 let d = mod_inverse(&n, &lambda)?;
280
281 let n_ctx = MontgomeryCtx::new(&n);
282 let gamma_ctx = MontgomeryCtx::new(&gamma);
283 Some((
284 SchmidtSamoaPublicKey { n, n_ctx },
285 SchmidtSamoaPrivateKey {
286 d,
287 gamma,
288 gamma_ctx,
289 },
290 ))
291 }
292
293 #[must_use]
295 pub fn generate<R: Csprng>(
296 rng: &mut R,
297 bits: usize,
298 ) -> Option<(SchmidtSamoaPublicKey, SchmidtSamoaPrivateKey)> {
299 if bits < 8 {
302 return None;
303 }
304
305 let p_bits = bits / 3;
306 let q_bits = bits.saturating_sub(2 * p_bits);
307 let p_bits = p_bits.max(2);
308 let q_bits = q_bits.max(2);
309 loop {
310 let p = random_probable_prime(rng, p_bits)?;
311 let q = random_probable_prime(rng, q_bits)?;
312 if let Some(keypair) = Self::from_primes(&p, &q) {
313 return Some(keypair);
314 }
315 }
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::{SchmidtSamoa, SchmidtSamoaPrivateKey, SchmidtSamoaPublicKey};
322 use crate::public_key::bigint::BigUint;
323 use crate::CtrDrbgAes256;
324
325 #[test]
326 fn derive_small_reference_key() {
327 let p = BigUint::from_u64(3);
328 let q = BigUint::from_u64(5);
329 let (public, private) = SchmidtSamoa::from_primes(&p, &q).expect("valid Schmidt-Samoa key");
330 assert_eq!(public.modulus(), &BigUint::from_u64(45));
331 assert_eq!(private.exponent(), &BigUint::from_u64(1));
334 assert_eq!(private.gamma(), &BigUint::from_u64(15));
335 }
336
337 #[test]
338 fn roundtrip_small_messages() {
339 let p = BigUint::from_u64(3);
340 let q = BigUint::from_u64(5);
341 let (public, private) = SchmidtSamoa::from_primes(&p, &q).expect("valid Schmidt-Samoa key");
342
343 for msg in [0u64, 1, 2, 7, 14] {
344 let message = BigUint::from_u64(msg);
345 let ciphertext = public.encrypt_raw(&message);
346 let plaintext = private.decrypt_raw(&ciphertext);
347 assert_eq!(plaintext, message);
348 }
349 }
350
351 #[test]
352 fn exact_small_ciphertext_matches_reference() {
353 let p = BigUint::from_u64(3);
354 let q = BigUint::from_u64(5);
355 let (public, private) = SchmidtSamoa::from_primes(&p, &q).expect("valid Schmidt-Samoa key");
356 let message = BigUint::from_u64(7);
357 let ciphertext = public.encrypt_raw(&message);
358 assert_eq!(ciphertext, BigUint::from_u64(37));
359 assert_eq!(private.decrypt_raw(&ciphertext), message);
360 }
361
362 #[test]
363 fn rejects_invalid_parameters() {
364 let p = BigUint::from_u64(3);
365 let q = BigUint::from_u64(7);
366 assert!(SchmidtSamoa::from_primes(&p, &q).is_none());
367
368 let p = BigUint::from_u64(3);
369 let composite = BigUint::from_u64(21);
370 assert!(SchmidtSamoa::from_primes(&p, &composite).is_none());
371
372 let p = BigUint::from_u64(5);
373 assert!(SchmidtSamoa::from_primes(&p, &p).is_none());
374 }
375
376 #[test]
377 fn byte_wrapper_roundtrip() {
378 let p = BigUint::from_u64(3);
379 let q = BigUint::from_u64(5);
380 let (public, private) = SchmidtSamoa::from_primes(&p, &q).expect("valid Schmidt-Samoa key");
381 let ciphertext = public.encrypt(&[0x05]).expect("message fits");
382 assert_eq!(private.decrypt(&ciphertext), vec![0x05]);
383 }
384
385 #[test]
386 fn generate_keypair_roundtrip() {
387 let mut drbg = CtrDrbgAes256::new(&[0x71; 48]);
388 let (public, private) =
389 SchmidtSamoa::generate(&mut drbg, 48).expect("Schmidt-Samoa key generation");
390 let ciphertext = public.encrypt(&[0x2a]).expect("message fits");
391 assert_eq!(private.decrypt(&ciphertext), vec![0x2a]);
392 }
393
394 #[test]
395 fn generate_rejects_too_few_bits() {
396 let mut drbg = CtrDrbgAes256::new(&[0x93; 48]);
397 assert!(SchmidtSamoa::generate(&mut drbg, 7).is_none());
398 }
399
400 #[test]
401 fn key_serialization_roundtrip() {
402 let mut drbg = CtrDrbgAes256::new(&[0xb3; 48]);
403 let (public, private) =
404 SchmidtSamoa::generate(&mut drbg, 48).expect("Schmidt-Samoa key generation");
405
406 let public_blob = public.to_key_blob();
407 let private_blob = private.to_key_blob();
408 assert_eq!(
409 SchmidtSamoaPublicKey::from_key_blob(&public_blob),
410 Some(public.clone())
411 );
412 assert_eq!(
413 SchmidtSamoaPrivateKey::from_key_blob(&private_blob),
414 Some(private.clone())
415 );
416
417 let public_pem = public.to_pem();
418 let private_pem = private.to_pem();
419 let public_xml = public.to_xml();
420 let private_xml = private.to_xml();
421 assert_eq!(
422 SchmidtSamoaPublicKey::from_pem(&public_pem),
423 Some(public.clone())
424 );
425 assert_eq!(
426 SchmidtSamoaPrivateKey::from_pem(&private_pem),
427 Some(private.clone())
428 );
429 assert_eq!(SchmidtSamoaPublicKey::from_xml(&public_xml), Some(public));
430 assert_eq!(
431 SchmidtSamoaPrivateKey::from_xml(&private_xml),
432 Some(private)
433 );
434 }
435
436 #[test]
437 fn byte_ciphertext_roundtrip() {
438 let p = BigUint::from_u64(3);
439 let q = BigUint::from_u64(5);
440 let (public, private) = SchmidtSamoa::from_primes(&p, &q).expect("valid Schmidt-Samoa key");
441 let ciphertext = public.encrypt_bytes(&[0x05]).expect("message fits");
442 assert_eq!(private.decrypt_bytes(&ciphertext), Some(vec![0x05]));
443 }
444}