Cargo.lock
Cargo.lock
This file has not been changed.
Cargo.toml
Cargo.toml
This file has not been changed.
sachy-crypto/Cargo.toml
sachy-crypto/Cargo.toml
This file has not been changed.
+160
-125
sachy-crypto/src/lib.rs
+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
1 commit
expand
collapse
Sachy's crypto scheme lmao
2/2 success
expand
collapse
no conflicts, ready to merge
expand 0 comments
1 commit
expand
collapse
Sachy's crypto scheme lmao
2/2 failed
expand
collapse
expand 0 comments
1 commit
expand
collapse
Sachy's crypto scheme lmao
2/2 success
expand
collapse
expand 0 comments
1 commit
expand
collapse
Sachy's crypto scheme lmao
2/2 success
expand
collapse
expand 0 comments
1 commit
expand
collapse
Sachy's crypto scheme lmao
2/2 success
expand
collapse
expand 0 comments
1 commit
expand
collapse
Sachy's crypto scheme lmao
2/2 success
expand
collapse
expand 0 comments
1 commit
expand
collapse
Sachy's crypto scheme lmao
2/2 success
expand
collapse
expand 0 comments
1 commit
expand
collapse
Sachy's crypto scheme lmao