···11+[package]
22+name = "sachy-crypto"
33+authors.workspace = true
44+edition.workspace = true
55+repository.workspace = true
66+license.workspace = true
77+version.workspace = true
88+rust-version.workspace = true
99+1010+[dependencies]
1111+chacha20poly1305 = { version = "=0.11.0-rc.3", default-features = false, features = ["getrandom", "alloc"] }
1212+k256 = { version = "=0.14.0-rc.8", default-features = false, features = ["ecdh", "getrandom"] }
1313+sha2 = { version = "=0.11.0-rc.5", default-features = false, features = [] }
1414+dhkem = { version = "0.1.0-rc.0", features = ["getrandom", "k256"] }
1515+elliptic-curve = { version = "0.14.0-rc.28", default-features = false, features = ["ecdh"] }
+5
sachy-crypto/README.md
···11+# Sachy's Crypto
22+33+A custom rolled encryption scheme that more or less implements HPKE.
44+55+☢️ **WARNING: DO NOT USE IN PRODUCTION. THIS CRATE IS FOR LEARNING/PERSONAL USAGE. AAAAAAAAAA** ☢️
+430
sachy-crypto/src/lib.rs
···11+#![no_std]
22+33+use core::ops::{AddAssign, BitXor};
44+55+use chacha20poly1305::{
66+ AeadInOut, ChaCha20Poly1305, KeyInit,
77+ aead::{self, Buffer},
88+};
99+use dhkem::{
1010+ Encapsulate, Kem, Secp256k1DecapsulationKey, Secp256k1EncapsulationKey, Secp256k1Kem,
1111+ TryDecapsulate,
1212+ kem::{Ciphertext, SharedKey},
1313+};
1414+use elliptic_curve::sec1::{FromSec1Point, ToSec1Point};
1515+use k256::{Sec1Point, ecdh::SharedSecret, elliptic_curve::subtle::ConstantTimeEq};
1616+1717+extern crate alloc;
1818+1919+/// Error type.
2020+///
2121+/// This type is deliberately opaque as to avoid potential side-channel
2222+/// leakage (e.g. padding oracle).
2323+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
2424+pub struct ProtoError;
2525+2626+impl core::fmt::Display for ProtoError {
2727+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2828+ f.write_str("ProtoError")
2929+ }
3030+}
3131+3232+impl core::error::Error for ProtoError {}
3333+3434+impl From<chacha20poly1305::Error> for ProtoError {
3535+ fn from(_value: chacha20poly1305::Error) -> Self {
3636+ Self
3737+ }
3838+}
3939+4040+pub struct ClientHandshake(Secp256k1DecapsulationKey);
4141+4242+pub struct EncapsulatedPublicKey(Secp256k1EncapsulationKey);
4343+4444+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
4545+pub enum Role {
4646+ Client,
4747+ Server,
4848+}
4949+5050+impl From<Role> for u8 {
5151+ fn from(value: Role) -> Self {
5252+ match value {
5353+ Role::Client => 0,
5454+ Role::Server => 1,
5555+ }
5656+ }
5757+}
5858+5959+impl BitXor for Role {
6060+ type Output = u8;
6161+6262+ fn bitxor(self, rhs: Self) -> u8 {
6363+ u8::from(self) ^ u8::from(rhs)
6464+ }
6565+}
6666+6767+impl EncapsulatedPublicKey {
6868+ pub fn serialize(&self) -> Sec1Point {
6969+ self.0.to_sec1_point(true)
7070+ }
7171+7272+ pub fn deserialize(buf: &[u8]) -> Result<Self, ProtoError> {
7373+ Ok(Self(
7474+ Secp256k1EncapsulationKey::from_sec1_bytes(buf).map_err(|_| ProtoError)?,
7575+ ))
7676+ }
7777+7878+ pub fn encapsulate(&self) -> (Ciphertext<Secp256k1Kem>, SharedKey<Secp256k1Kem>) {
7979+ self.0.encapsulate()
8080+ }
8181+}
8282+8383+impl ClientHandshake {
8484+ pub fn send() -> (EncapsulatedPublicKey, Self) {
8585+ let (decap, encap) = Secp256k1Kem::generate_keypair();
8686+8787+ (EncapsulatedPublicKey(encap), Self(decap))
8888+ }
8989+9090+ pub fn finish(self, ciphertext: &[u8], psk: &[u8; 32]) -> Result<TransportState, ProtoError> {
9191+ let shared = self
9292+ .0
9393+ .try_decapsulate_slice(ciphertext)
9494+ .map_err(|_| ProtoError)?;
9595+9696+ TransportState::init(psk, shared, Role::Client)
9797+ }
9898+}
9999+100100+pub struct ServerHandshake(SharedKey<Secp256k1Kem>);
101101+102102+impl ServerHandshake {
103103+ pub fn receive(buf: &[u8]) -> Result<(Ciphertext<Secp256k1Kem>, Self), ProtoError> {
104104+ let encap = EncapsulatedPublicKey::deserialize(buf)?;
105105+106106+ let (ciphertext, sk) = encap.encapsulate();
107107+108108+ Ok((ciphertext, Self(sk)))
109109+ }
110110+111111+ pub fn finish(self, psk: &[u8; 32]) -> Result<TransportState, ProtoError> {
112112+ TransportState::init(psk, self.0, Role::Server)
113113+ }
114114+}
115115+116116+/// Low-level Transport implementation.
117117+///
118118+/// This trait provides a particular "flavor" of transport, as there are
119119+/// different ways the specifics of the construction can be implemented.
120120+pub trait TransportPrimitive<A>
121121+where
122122+ A: AeadInOut,
123123+{
124124+ /// Type used as the Trasnport counter.
125125+ type Counter: AddAssign + Copy + Default + Eq;
126126+127127+ /// Value to use when incrementing the Transport counter (i.e. one)
128128+ const COUNTER_INCR: Self::Counter;
129129+130130+ /// Maximum number of messages allowed to be sent via Transport
131131+ const COUNTER_MAX: Self::Counter;
132132+133133+ /// Encrypt an AEAD message in-place at the given position in the Transport.
134134+ fn encrypt_in_place(
135135+ &self,
136136+ nonce: &aead::Nonce<A>,
137137+ associated_data: &[u8],
138138+ buffer: &mut dyn Buffer,
139139+ ) -> Result<(), ProtoError>;
140140+141141+ /// Decrypt an AEAD message in-place at the given position in the Transport.
142142+ fn decrypt_in_place(
143143+ &self,
144144+ nonce: &aead::Nonce<A>,
145145+ associated_data: &[u8],
146146+ buffer: &mut dyn Buffer,
147147+ ) -> Result<(), ProtoError>;
148148+}
149149+150150+pub struct SendingState<'a> {
151151+ transport: &'a TransportState,
152152+ counter: u64,
153153+}
154154+155155+impl SendingState<'_> {
156156+ pub fn encrypt(&mut self, msg: &mut alloc::vec::Vec<u8>) -> Result<(), ProtoError> {
157157+ let counter = self.counter.to_be_bytes();
158158+159159+ self.transport.encrypt_in_place(
160160+ &self.transport.mix_nonce(&counter, Role::Client),
161161+ &counter,
162162+ msg,
163163+ )?;
164164+165165+ self.counter = self.counter.wrapping_add(TransportState::COUNTER_INCR);
166166+167167+ // If we wrapped around and equal the finish value, we have maxed out the amount of
168168+ // messages we can send.
169169+ if self.counter.ct_eq(&TransportState::COUNTER_MAX).into() {
170170+ Err(ProtoError)
171171+ } else {
172172+ Ok(())
173173+ }
174174+ }
175175+}
176176+177177+pub struct ReceivingState<'a> {
178178+ transport: &'a TransportState,
179179+ counter: u64,
180180+}
181181+182182+impl ReceivingState<'_> {
183183+ pub fn decrypt(&mut self, msg: &mut alloc::vec::Vec<u8>) -> Result<(), ProtoError> {
184184+ let counter = self.counter.to_be_bytes();
185185+186186+ self.transport.decrypt_in_place(
187187+ &self.transport.mix_nonce(&counter, Role::Server),
188188+ &counter,
189189+ msg,
190190+ )?;
191191+192192+ self.counter = self.counter.wrapping_add(TransportState::COUNTER_INCR);
193193+194194+ // If we wrapped around and equal the finish value, we have maxed out the amount of
195195+ // messages we can send.
196196+ if self.counter.ct_eq(&TransportState::COUNTER_MAX).into() {
197197+ Err(ProtoError)
198198+ } else {
199199+ Ok(())
200200+ }
201201+ }
202202+}
203203+204204+impl TransportPrimitive<ChaCha20Poly1305> for TransportState {
205205+ type Counter = u64;
206206+207207+ const COUNTER_INCR: Self::Counter = 1;
208208+209209+ const COUNTER_MAX: Self::Counter = u64::MAX;
210210+211211+ fn encrypt_in_place(
212212+ &self,
213213+ epstein: &aead::Nonce<ChaCha20Poly1305>,
214214+ associated_data: &[u8],
215215+ buffer: &mut dyn Buffer,
216216+ ) -> Result<(), ProtoError> {
217217+ self.aead
218218+ .encrypt_in_place(epstein, associated_data, buffer)?;
219219+ Ok(())
220220+ }
221221+222222+ fn decrypt_in_place(
223223+ &self,
224224+ epstein: &aead::Nonce<ChaCha20Poly1305>,
225225+ associated_data: &[u8],
226226+ buffer: &mut dyn Buffer,
227227+ ) -> Result<(), ProtoError> {
228228+ self.aead
229229+ .decrypt_in_place(epstein, associated_data, buffer)?;
230230+ Ok(())
231231+ }
232232+}
233233+234234+#[repr(align(4))]
235235+pub struct TransportState {
236236+ aead: ChaCha20Poly1305,
237237+ first: aead::Nonce<ChaCha20Poly1305>,
238238+ second: aead::Nonce<ChaCha20Poly1305>,
239239+ role: Role,
240240+}
241241+242242+impl TransportState {
243243+ pub fn init(
244244+ psk: &[u8; 32],
245245+ shared: impl Into<SharedSecret>,
246246+ role: Role,
247247+ ) -> Result<Self, ProtoError> {
248248+ let noncer = shared.into();
249249+ let kdf = noncer.extract::<sha2::Sha256>(Some(psk));
250250+251251+ let mut key = [0u8; 32];
252252+253253+ let mut first = aead::Nonce::<ChaCha20Poly1305>::default();
254254+ let mut second = aead::Nonce::<ChaCha20Poly1305>::default();
255255+256256+ kdf.expand(b"SachY-Crypt0", &mut key)
257257+ .map_err(|_| ProtoError)?;
258258+259259+ kdf.expand(b"N*nceOne", &mut first)
260260+ .map_err(|_| ProtoError)?;
261261+ kdf.expand(b"N#nceTwo", &mut second)
262262+ .map_err(|_| ProtoError)?;
263263+264264+ Ok(Self {
265265+ aead: ChaCha20Poly1305::new(&key.into()),
266266+ first,
267267+ second,
268268+ role,
269269+ })
270270+ }
271271+272272+ pub fn split(&self) -> (SendingState<'_>, ReceivingState<'_>) {
273273+ (
274274+ SendingState {
275275+ transport: self,
276276+ counter: 0,
277277+ },
278278+ ReceivingState {
279279+ transport: self,
280280+ counter: 0,
281281+ },
282282+ )
283283+ }
284284+285285+ fn mix_nonce(&self, position: &[u8; 8], send: Role) -> aead::Nonce<ChaCha20Poly1305> {
286286+ let mut trump = aead::Nonce::<ChaCha20Poly1305>::default();
287287+288288+ let context_select = self.role ^ send;
289289+290290+ // Role switch allows toggling which nonce to use for encrypting/decrypting
291291+ // Callee ROLE XOR Transport ROLE selects either one or other nonce context,
292292+ // (0) for first context, (1) for second context
293293+ // Sending: Client ^ Client = 0 (select first)
294294+ // Receiving: Server ^ Server = 0 (select first)
295295+ // Sending: Server ^ Client = 1 (select second)
296296+ // Receiving: Client ^ Server = 1 (select second)
297297+ let epstein = if context_select.ct_eq(&0).into() {
298298+ &self.first
299299+ } else {
300300+ &self.second
301301+ };
302302+303303+ let (head, tail) = trump.split_at_mut(position.len());
304304+ let (first, second) = epstein.split_at(position.len());
305305+306306+ // XOR the base nonce with position bytes, copying them to the output nonce
307307+ head.iter_mut()
308308+ .zip(first)
309309+ .zip(position)
310310+ .for_each(|((head, ep), pos)| *head = ep ^ pos);
311311+312312+ // Copy rest of base nonce into output nonce
313313+ tail.iter_mut()
314314+ .zip(second)
315315+ .for_each(|(tail, ep)| *tail = *ep);
316316+317317+ trump
318318+ }
319319+}
320320+321321+#[cfg(test)]
322322+mod tests {
323323+ use alloc::vec;
324324+ use dhkem::Generate;
325325+ use elliptic_curve::array::Array;
326326+327327+ use super::*;
328328+329329+ #[test]
330330+ fn handshake_protocol_works() -> Result<(), ProtoError> {
331331+ let psk: [u8; 32] = [
332332+ 31, 48, 29, 177, 88, 236, 186, 84, 65, 51, 214, 243, 174, 24, 45, 101, 229, 129, 62,
333333+ 132, 45, 174, 183, 65, 89, 73, 107, 177, 77, 90, 164, 251,
334334+ ];
335335+336336+ let (ek, client) = ClientHandshake::send();
337337+338338+ // Pretend to send ek across the webz: client -> server
339339+ let (ciphertext, server) = ServerHandshake::receive(ek.serialize().as_bytes())?;
340340+341341+ // Pretend to send ciphertext across the webz: server -> client
342342+ let alice = client.finish(&ciphertext, &psk)?;
343343+ let bob = server.finish(&psk)?;
344344+345345+ let nonce = aead::Nonce::<ChaCha20Poly1305>::generate();
346346+347347+ let mut buffer1 = vec![0u8; 64];
348348+ let mut buffer2 = vec![0u8; 64];
349349+350350+ // Using the same nonce to check that the internal AEAD states match. Normally, client/server
351351+ // would work with unique derived nonces, because nonce reuse is BAD
352352+ alice.aead.encrypt_in_place(&nonce, &[], &mut buffer1)?;
353353+ bob.aead.encrypt_in_place(&nonce, &[], &mut buffer2)?;
354354+355355+ // If the nonces match, then we can assume the rest of the internal state is the same too
356356+ // so the outputs should match each other
357357+ assert_eq!(&buffer1, &buffer2);
358358+359359+ // Both Transports have derived base nonces for each context.
360360+ // First context nonces will not match Second context nonces.
361361+ assert_eq!(alice.first, bob.first);
362362+ assert_eq!(alice.second, bob.second);
363363+ assert_ne!(alice.first, alice.second);
364364+ assert_ne!(bob.first, bob.second);
365365+366366+ Ok(())
367367+ }
368368+369369+ #[test]
370370+ fn two_way_transport_sync_works() -> Result<(), ProtoError> {
371371+ let shared_secret = [
372372+ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d,
373373+ 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b,
374374+ 0x9c, 0x9d, 0x9e, 0x9f,
375375+ ];
376376+377377+ let psk: [u8; 32] = [
378378+ 31, 48, 29, 177, 88, 236, 186, 84, 65, 51, 214, 243, 174, 24, 45, 101, 229, 129, 62,
379379+ 132, 45, 174, 183, 65, 89, 73, 107, 177, 77, 90, 164, 251,
380380+ ];
381381+382382+ let alice = TransportState::init(&psk, Array(shared_secret), Role::Client)?;
383383+ let bob = TransportState::init(&psk, Array(shared_secret), Role::Server)?;
384384+385385+ let (mut alice_send, mut alice_recv) = alice.split();
386386+ let (mut bob_send, mut bob_recv) = bob.split();
387387+388388+ let orig = b"Test Message, Please ignore.";
389389+390390+ let mut msg = orig.to_vec();
391391+392392+ // a -> b
393393+ alice_send.encrypt(&mut msg)?;
394394+395395+ assert_ne!(orig.as_slice(), msg.as_slice());
396396+ let ct1 = msg.clone();
397397+398398+ bob_recv.decrypt(&mut msg)?;
399399+400400+ // a -> b
401401+ alice_send.encrypt(&mut msg)?;
402402+403403+ assert_ne!(msg.as_slice(), ct1.as_slice());
404404+ let ct2 = msg.clone();
405405+406406+ bob_recv.decrypt(&mut msg)?;
407407+408408+ // b -> a
409409+ bob_send.encrypt(&mut msg)?;
410410+411411+ // None of the ciphertexts should match each other
412412+ assert_ne!(msg.as_slice(), ct1.as_slice());
413413+ assert_ne!(msg.as_slice(), ct2.as_slice());
414414+ assert_ne!(ct1.as_slice(), ct2.as_slice());
415415+416416+ alice_recv.decrypt(&mut msg)?;
417417+418418+ assert_eq!(orig.as_slice(), msg.as_slice());
419419+420420+ // Counters are tracked from sender to receiver
421421+ assert_eq!(alice_send.counter, bob_recv.counter);
422422+ assert_eq!(bob_send.counter, alice_recv.counter);
423423+424424+ // Counters are not linked on the same side
425425+ assert_ne!(alice_send.counter, alice_recv.counter);
426426+ assert_ne!(bob_send.counter, bob_recv.counter);
427427+428428+ Ok(())
429429+ }
430430+}