Repo of no-std crates for my personal embedded projects

Sachy's crypto scheme lmao #13

open opened by sachy.dev targeting main from sachy-crypto
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:usjm3ynnir6y4inkcdovrfei/sh.tangled.repo.pull/3mhklndgukc22
+160 -125
Interdiff #3 #4
Cargo.lock

This file has not been changed.

Cargo.toml

This file has not been changed.

sachy-crypto/Cargo.toml

This file has not been changed.

+160 -125
sachy-crypto/src/lib.rs
··· 3 3 use core::ops::{AddAssign, Sub}; 4 4 5 5 use chacha20poly1305::{ 6 - AeadCore, AeadInOut, ChaCha20Poly1305, KeyInit, 6 + AeadCore, AeadInOut, KeyInit, XChaCha20Poly1305, 7 7 aead::{ 8 8 self, Buffer, 9 9 array::{Array, ArraySize}, ··· 12 12 consts::U8, 13 13 }; 14 14 use dhkem::{ 15 - Encapsulate, Kem, Secp256k1DecapsulationKey, Secp256k1EncapsulationKey, Secp256k1Kem, 15 + Encapsulate, Generate, Kem, Secp256k1DecapsulationKey, Secp256k1EncapsulationKey, Secp256k1Kem, 16 16 TryDecapsulate, 17 17 kem::{Ciphertext, SharedKey}, 18 18 }; ··· 21 21 22 22 extern crate alloc; 23 23 24 + /// Error type. 25 + /// 26 + /// This type is deliberately opaque as to avoid potential side-channel 27 + /// leakage (e.g. padding oracle). 24 28 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 25 - pub struct HandshakeError; 29 + pub struct ProtoError; 26 30 27 - impl core::fmt::Display for HandshakeError { 31 + impl core::fmt::Display for ProtoError { 28 32 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 29 - f.write_str("aead::Error") 33 + f.write_str("ProtoError") 30 34 } 31 35 } 32 36 33 - impl core::error::Error for HandshakeError {} 37 + impl core::error::Error for ProtoError {} 38 + 39 + impl From<chacha20poly1305::Error> for ProtoError { 40 + fn from(_value: chacha20poly1305::Error) -> Self { 41 + Self 42 + } 43 + } 34 44 35 45 pub struct ClientHandshake(Secp256k1DecapsulationKey); 36 46 ··· 41 51 self.0.to_sec1_point(true) 42 52 } 43 53 44 - pub fn deserialize(buf: &[u8]) -> Result<Self, HandshakeError> { 54 + pub fn deserialize(buf: &[u8]) -> Result<Self, ProtoError> { 45 55 Ok(Self( 46 - Secp256k1EncapsulationKey::from_sec1_bytes(buf).map_err(|_| HandshakeError)?, 56 + Secp256k1EncapsulationKey::from_sec1_bytes(buf).map_err(|_| ProtoError)?, 47 57 )) 48 58 } 49 59 ··· 59 69 (EncapsulatedPublicKey(encap), Self(decap)) 60 70 } 61 71 62 - pub fn finish( 63 - self, 64 - ciphertext: &[u8], 65 - psk: &[u8; 32], 66 - ) -> Result<TransportState, HandshakeError> { 72 + pub fn finish(self, ciphertext: &[u8], psk: &[u8; 32]) -> Result<TransportState, ProtoError> { 67 73 let shared = self 68 74 .0 69 75 .try_decapsulate_slice(ciphertext) 70 - .map_err(|_| HandshakeError)?; 76 + .map_err(|_| ProtoError)?; 71 77 72 78 TransportState::init(psk, shared) 73 79 } ··· 76 82 pub struct ServerHandshake(SharedKey<Secp256k1Kem>); 77 83 78 84 impl ServerHandshake { 79 - pub fn receive(buf: &[u8]) -> Result<(Ciphertext<Secp256k1Kem>, Self), HandshakeError> { 85 + pub fn receive(buf: &[u8]) -> Result<(Ciphertext<Secp256k1Kem>, Self), ProtoError> { 80 86 let encap = EncapsulatedPublicKey::deserialize(buf)?; 81 87 82 88 let (ciphertext, sk) = encap.encapsulate(); ··· 84 90 Ok((ciphertext, Self(sk))) 85 91 } 86 92 87 - pub fn finish(self, psk: &[u8; 32]) -> Result<TransportState, HandshakeError> { 93 + pub fn finish(self, psk: &[u8; 32]) -> Result<TransportState, ProtoError> { 88 94 TransportState::init(psk, self.0) 89 95 } 90 96 } 91 97 92 - /// Error type. 93 - /// 94 - /// This type is deliberately opaque as to avoid potential side-channel 95 - /// leakage (e.g. padding oracle). 96 - #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 97 - pub struct AeadError; 98 - 99 - impl core::fmt::Display for AeadError { 100 - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 101 - f.write_str("aead::Error") 102 - } 103 - } 104 - 105 - impl core::error::Error for AeadError {} 106 - 107 - impl From<chacha20poly1305::Error> for AeadError { 108 - fn from(_value: chacha20poly1305::Error) -> Self { 109 - AeadError 110 - } 111 - } 112 - 113 98 /// Nonce as used by a given AEAD construction and STREAM primitive. 114 99 pub type Nonce<A, S> = chacha20poly1305::aead::array::Array<u8, NonceSize<A, S>>; 115 100 116 101 /// Size of a nonce as used by a STREAM construction, sans the overhead of 117 102 /// the STREAM protocol itself. 118 103 pub type NonceSize<A, S> = 119 - <<A as AeadCore>::NonceSize as Sub<<S as StreamPrimitive<A>>::NonceOverhead>>::Output; 104 + <<A as AeadCore>::NonceSize as Sub<<S as TransportPrimitive<A>>::NonceOverhead>>::Output; 120 105 121 106 /// Low-level STREAM implementation. 122 107 /// ··· 124 109 /// different ways the specifics of the construction can be implemented. 125 110 /// 126 111 /// Deliberately immutable and stateless to permit parallel operation. 127 - pub trait StreamPrimitive<A> 112 + pub trait TransportPrimitive<A> 128 113 where 129 114 A: AeadInOut, 130 115 A::NonceSize: Sub<Self::NonceOverhead>, ··· 145 130 /// Encrypt an AEAD message in-place at the given position in the STREAM. 146 131 fn encrypt_in_place( 147 132 &self, 148 - position: Self::Counter, 133 + nonce: &aead::Nonce<A>, 149 134 associated_data: &[u8], 150 135 buffer: &mut dyn Buffer, 151 - ) -> Result<(), AeadError>; 136 + ) -> Result<(), ProtoError>; 152 137 153 138 /// Decrypt an AEAD message in-place at the given position in the STREAM. 154 139 fn decrypt_in_place( 155 140 &self, 156 - position: Self::Counter, 141 + nonce: &aead::Nonce<A>, 157 142 associated_data: &[u8], 158 143 buffer: &mut dyn Buffer, 159 - ) -> Result<(), AeadError>; 144 + ) -> Result<(), ProtoError>; 160 145 } 161 146 162 147 pub struct TransportState { 163 - aead: ChaCha20Poly1305, 164 - epstein: Nonce<ChaCha20Poly1305, Self>, 148 + aead: XChaCha20Poly1305, 149 + } 150 + 151 + pub struct SendingState<'a> { 152 + transport: &'a TransportState, 153 + epstein: Nonce<XChaCha20Poly1305, TransportState>, 165 154 counter: u64, 166 155 } 167 156 168 - impl StreamPrimitive<ChaCha20Poly1305> for TransportState { 157 + impl SendingState<'_> { 158 + fn aead_nonce(&self, position: u64) -> aead::Nonce<XChaCha20Poly1305> { 159 + let mut result = Array::default(); 160 + 161 + let (prefix, tail) = 162 + result.split_at_mut(NonceSize::<XChaCha20Poly1305, TransportState>::to_usize()); 163 + 164 + prefix.copy_from_slice(&self.epstein); 165 + 166 + tail[..8].copy_from_slice(&position.to_be_bytes()); 167 + 168 + result 169 + } 170 + 171 + pub fn encrypt(&mut self, msg: &mut alloc::vec::Vec<u8>) -> Result<(), ProtoError> { 172 + let counter = self.counter.to_le_bytes(); 173 + 174 + let epstein = self.aead_nonce(self.counter); 175 + 176 + self.transport.encrypt_in_place(&epstein, &counter, msg)?; 177 + 178 + msg.extend(epstein); 179 + 180 + self.counter += TransportState::COUNTER_INCR; 181 + 182 + if self.counter.ct_eq(&TransportState::COUNTER_MAX).into() { 183 + Err(ProtoError) 184 + } else { 185 + Ok(()) 186 + } 187 + } 188 + } 189 + 190 + pub struct ReceivingState<'a> { 191 + transport: &'a TransportState, 192 + counter: u64, 193 + } 194 + 195 + impl ReceivingState<'_> { 196 + pub fn decrypt(&mut self, msg: &mut alloc::vec::Vec<u8>) -> Result<(), ProtoError> { 197 + let counter = self.counter.to_le_bytes(); 198 + 199 + let index = msg.len() - <XChaCha20Poly1305 as AeadCore>::NonceSize::to_usize(); 200 + 201 + // Extract the nonce from the payload as this does not need to be decrypted 202 + let epstein = Array::try_from_iter(msg.drain(index..)).map_err(|_| ProtoError)?; 203 + 204 + self.transport.decrypt_in_place(&epstein, &counter, msg)?; 205 + 206 + self.counter += TransportState::COUNTER_INCR; 207 + 208 + if self.counter.ct_eq(&TransportState::COUNTER_MAX).into() { 209 + Err(ProtoError) 210 + } else { 211 + Ok(()) 212 + } 213 + } 214 + } 215 + 216 + impl TransportPrimitive<XChaCha20Poly1305> for TransportState { 169 217 type NonceOverhead = U8; 170 218 171 219 type Counter = u64; ··· 176 224 177 225 fn encrypt_in_place( 178 226 &self, 179 - position: Self::Counter, 227 + epstein: &aead::Nonce<XChaCha20Poly1305>, 180 228 associated_data: &[u8], 181 229 buffer: &mut dyn Buffer, 182 - ) -> Result<(), AeadError> { 183 - let epstein = self.aead_nonce(position); 230 + ) -> Result<(), ProtoError> { 184 231 self.aead 185 - .encrypt_in_place(&epstein, associated_data, buffer)?; 232 + .encrypt_in_place(epstein, associated_data, buffer)?; 186 233 Ok(()) 187 234 } 188 235 189 236 fn decrypt_in_place( 190 237 &self, 191 - position: Self::Counter, 238 + epstein: &aead::Nonce<XChaCha20Poly1305>, 192 239 associated_data: &[u8], 193 240 buffer: &mut dyn Buffer, 194 - ) -> Result<(), AeadError> { 195 - let epstein = self.aead_nonce(position); 241 + ) -> Result<(), ProtoError> { 196 242 self.aead 197 - .decrypt_in_place(&epstein, associated_data, buffer)?; 243 + .decrypt_in_place(epstein, associated_data, buffer)?; 198 244 Ok(()) 199 245 } 200 246 } 201 247 202 248 impl TransportState { 203 - fn aead_nonce(&self, position: u64) -> aead::Nonce<ChaCha20Poly1305> { 204 - let mut result = Array::default(); 205 - 206 - let (prefix, tail) = result.split_at_mut(NonceSize::<ChaCha20Poly1305, Self>::to_usize()); 207 - 208 - prefix.copy_from_slice(&self.epstein); 209 - 210 - tail[..8].copy_from_slice(&position.to_be_bytes()); 211 - 212 - result 213 - } 214 - 215 - pub fn init(psk: &[u8; 32], shared: impl Into<SharedSecret>) -> Result<Self, HandshakeError> { 216 - let kdf = shared.into().extract::<sha2::Sha256>(Some(psk)); 249 + pub fn init(psk: &[u8; 32], shared: impl Into<SharedSecret>) -> Result<Self, ProtoError> { 250 + let noncer = shared.into(); 251 + let kdf = noncer.extract::<sha2::Sha256>(Some(psk)); 217 252 218 253 let mut key = [0u8; 32]; 219 254 220 255 kdf.expand(b"sachy-crypto", &mut key) 221 - .map_err(|_| HandshakeError)?; 222 - 223 - let mut epstein = Nonce::<ChaCha20Poly1305, Self>::default(); 224 - 225 - kdf.expand(b"noncer", &mut epstein) 226 - .map_err(|_| HandshakeError)?; 256 + .map_err(|_| ProtoError)?; 227 257 228 258 Ok(Self { 229 - aead: ChaCha20Poly1305::new(&key.into()), 230 - epstein, 231 - counter: 0, 259 + aead: XChaCha20Poly1305::new(&key.into()), 232 260 }) 233 261 } 234 262 235 - pub fn encrypt(&mut self, msg: &mut alloc::vec::Vec<u8>) -> Result<(), AeadError> { 236 - let counter_data = self.counter.to_be_bytes(); 237 - 238 - self.encrypt_in_place(self.counter, &counter_data, msg)?; 239 - 240 - self.counter += Self::COUNTER_INCR; 241 - 242 - if self.counter.ct_eq(&Self::COUNTER_MAX).into() { 243 - Err(AeadError) 244 - } else { 245 - Ok(()) 246 - } 247 - } 248 - 249 - pub fn decrypt(&mut self, msg: &mut alloc::vec::Vec<u8>) -> Result<(), AeadError> { 250 - let counter_data = self.counter.to_be_bytes(); 251 - 252 - self.decrypt_in_place(self.counter, &counter_data, msg)?; 253 - 254 - self.counter += Self::COUNTER_INCR; 255 - 256 - if self.counter.ct_eq(&Self::COUNTER_MAX).into() { 257 - Err(AeadError) 258 - } else { 259 - Ok(()) 260 - } 263 + pub fn split(&self) -> (SendingState<'_>, ReceivingState<'_>) { 264 + ( 265 + SendingState { 266 + transport: self, 267 + epstein: Nonce::<XChaCha20Poly1305, Self>::generate(), 268 + counter: 0, 269 + }, 270 + ReceivingState { 271 + transport: self, 272 + counter: 0, 273 + }, 274 + ) 261 275 } 262 276 } 263 277 264 278 #[cfg(test)] 265 279 mod tests { 280 + use alloc::vec; 281 + 266 282 use super::*; 267 283 268 284 #[test] 269 - fn handshake_protocol_works() -> Result<(), HandshakeError> { 285 + fn handshake_protocol_works() -> Result<(), ProtoError> { 270 286 let psk: [u8; 32] = [ 271 287 31, 48, 29, 177, 88, 236, 186, 84, 65, 51, 214, 243, 174, 24, 45, 101, 229, 129, 62, 272 288 132, 45, 174, 183, 65, 89, 73, 107, 177, 77, 90, 164, 251, ··· 281 297 let client_transport = client.finish(&ciphertext, &psk)?; 282 298 let server_transport = server.finish(&psk)?; 283 299 284 - // If the nonces match, then we can assume the rest of the internal state is correct 285 - assert_eq!( 286 - client_transport.epstein.as_slice(), 287 - server_transport.epstein.as_slice() 288 - ); 300 + let nonce = aead::Nonce::<XChaCha20Poly1305>::generate(); 301 + 302 + let mut buffer1 = vec![0u8; 64]; 303 + let mut buffer2 = vec![0u8; 64]; 304 + 305 + // Using the same nonce to check that the internal states match. Normally, client/server 306 + // would work with different nonces, because nonce reuse is BAD 307 + client_transport 308 + .aead 309 + .encrypt_in_place(&nonce, &[], &mut buffer1)?; 310 + server_transport 311 + .aead 312 + .encrypt_in_place(&nonce, &[], &mut buffer2)?; 313 + 314 + // If the nonces match, then we can assume the rest of the internal state is the same too 315 + // so the outputs should match each other 316 + assert_eq!(&buffer1, &buffer2); 289 317 290 318 Ok(()) 291 319 } 292 320 293 321 #[test] 294 - fn two_way_transport_sync_works() -> Result<(), AeadError> { 322 + fn two_way_transport_sync_works() -> Result<(), ProtoError> { 295 323 let shared_secret = [ 296 324 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 297 325 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, ··· 303 331 132, 45, 174, 183, 65, 89, 73, 107, 177, 77, 90, 164, 251, 304 332 ]; 305 333 306 - let mut alice = TransportState::init(&psk, Array(shared_secret)).map_err(|_| AeadError)?; 307 - let mut bob = TransportState::init(&psk, Array(shared_secret)).map_err(|_| AeadError)?; 334 + let alice = TransportState::init(&psk, Array(shared_secret))?; 335 + let bob = TransportState::init(&psk, Array(shared_secret))?; 336 + 337 + let (mut alice_send, mut alice_recv) = alice.split(); 338 + let (mut bob_send, mut bob_recv) = bob.split(); 339 + 340 + // Confirm that both send channels have different nonces. 341 + assert_ne!(alice_send.epstein.as_slice(), bob_send.epstein.as_slice()); 308 342 309 343 let orig = b"Test Message, Please ignore.".to_vec(); 310 344 311 345 let mut msg = orig.clone(); 312 346 313 347 // a -> b 314 - alice.encrypt(&mut msg)?; 348 + alice_send.encrypt(&mut msg)?; 315 349 316 350 assert_ne!(orig.as_slice(), msg.as_slice()); 317 - let a1 = msg.clone(); 351 + let ct1 = msg.clone(); 318 352 319 - bob.decrypt(&mut msg)?; 353 + bob_recv.decrypt(&mut msg)?; 320 354 321 355 // a -> b 322 - alice.encrypt(&mut msg)?; 356 + alice_send.encrypt(&mut msg)?; 323 357 324 - assert_ne!(msg.as_slice(), a1.as_slice()); 325 - let a2 = msg.clone(); 358 + assert_ne!(msg.as_slice(), ct1.as_slice()); 359 + let ct2 = msg.clone(); 326 360 327 - bob.decrypt(&mut msg)?; 361 + bob_recv.decrypt(&mut msg)?; 328 362 329 363 // b -> a 330 - bob.encrypt(&mut msg)?; 364 + bob_send.encrypt(&mut msg)?; 331 365 332 366 // None of the ciphertexts should match each other 333 - assert_ne!(msg.as_slice(), a1.as_slice()); 334 - assert_ne!(msg.as_slice(), a2.as_slice()); 335 - assert_ne!(a1.as_slice(), a2.as_slice()); 367 + assert_ne!(msg.as_slice(), ct1.as_slice()); 368 + assert_ne!(msg.as_slice(), ct2.as_slice()); 369 + assert_ne!(ct1.as_slice(), ct2.as_slice()); 336 370 337 - alice.decrypt(&mut msg)?; 371 + alice_recv.decrypt(&mut msg)?; 338 372 339 373 assert_eq!(orig.as_slice(), msg.as_slice()); 340 - assert_eq!(alice.counter, bob.counter); 374 + assert_eq!(alice_send.counter, bob_recv.counter); 375 + assert_eq!(bob_send.counter, alice_recv.counter); 341 376 342 377 Ok(()) 343 378 }

History

8 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
Sachy's crypto scheme lmao
2/2 success
expand
no conflicts, ready to merge
expand 0 comments
1 commit
expand
Sachy's crypto scheme lmao
2/2 failed
expand
expand 0 comments
1 commit
expand
Sachy's crypto scheme lmao
2/2 success
expand
expand 0 comments
sachy.dev submitted #4
1 commit
expand
Sachy's crypto scheme lmao
2/2 success
expand
expand 0 comments
1 commit
expand
Sachy's crypto scheme lmao
2/2 success
expand
expand 0 comments
1 commit
expand
Sachy's crypto scheme lmao
2/2 success
expand
expand 0 comments
1 commit
expand
Sachy's crypto scheme lmao
2/2 success
expand
expand 0 comments
sachy.dev submitted #0
1 commit
expand
Sachy's crypto scheme lmao
2/2 success
expand
expand 0 comments