beaconcrypt 0.3.2__tar.gz → 0.3.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/.github/workflows/python.yml +5 -0
  2. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/Cargo.lock +1 -1
  3. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/Cargo.toml +1 -1
  4. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/PKG-INFO +1 -1
  5. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/README.md +15 -0
  6. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/beaconcrypt.go +24 -1
  7. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/beaconcrypt_test.go +173 -15
  8. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/bindings.h +8 -0
  9. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/pyproject.toml +4 -0
  10. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/cnsa2.rs +1 -0
  11. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/lib.rs +47 -2
  12. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/pqxdh.rs +8 -8
  13. beaconcrypt-0.3.5/tests/protocol.rs +119 -0
  14. beaconcrypt-0.3.5/tests/test_encryption.py +168 -0
  15. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/tests/test_registration.py +17 -2
  16. beaconcrypt-0.3.2/tests/test_encryption.py +0 -92
  17. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/.cargo/config.toml +0 -0
  18. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/.github/workflows/go.yml +0 -0
  19. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/.github/workflows/release.yml +0 -0
  20. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/.github/workflows/rust.yml +0 -0
  21. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/.gitignore +0 -0
  22. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/LICENSE +0 -0
  23. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/build.rs +0 -0
  24. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/clippy.toml +0 -0
  25. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/doc/protocol.md +0 -0
  26. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/doc/rationale.md +0 -0
  27. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/doc/threat_model.md +0 -0
  28. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/go.mod +0 -0
  29. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/rustfmt.toml +0 -0
  30. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/beacon.rs +0 -0
  31. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/error.rs +0 -0
  32. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/schema/cryptoframe.capnp +0 -0
  33. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/schema/phase1.capnp +0 -0
  34. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/schema/phase2.capnp +0 -0
  35. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/schema/protogram.capnp +0 -0
  36. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/server.rs +0 -0
  37. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/src/shared.rs +0 -0
  38. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/tests/beacon.rs +0 -0
  39. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/tests/server.rs +0 -0
  40. {beaconcrypt-0.3.2 → beaconcrypt-0.3.5}/uv.lock +0 -0
@@ -31,6 +31,11 @@ jobs:
31
31
  run: |
32
32
  sudo apt install capnproto
33
33
 
34
+ - name: Run Python tests
35
+ run: |
36
+ uv run --group dev maturin develop --bindings pyo3 --features pybinds
37
+ uv run --group dev pytest tests
38
+
34
39
  - name: Build source distribution
35
40
  run: uvx maturin sdist --out dist
36
41
 
@@ -84,7 +84,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
84
84
 
85
85
  [[package]]
86
86
  name = "beaconcrypt"
87
- version = "0.3.2"
87
+ version = "0.3.5"
88
88
  dependencies = [
89
89
  "capnp",
90
90
  "capnpc",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "beaconcrypt"
3
- version = "0.3.2"
3
+ version = "0.3.5"
4
4
  edition = "2024"
5
5
  license-file = "LICENSE"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beaconcrypt
3
- Version: 0.3.2
3
+ Version: 0.3.5
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -46,14 +46,29 @@ Test the C interface
46
46
  # Reference implementation
47
47
  I don't use rust a lot, so the code is probably fairly naive. It provides both a beacon and server implementation with C bindings through `cbindgen`. Ideally more bindings would be built on top of that so it can be used in the mythic server-side.
48
48
 
49
+ The reference implementation expects that all beacons are compiled with the server's public key, and that beaconcrypt is initialized with it.
50
+
51
+ The server is currently not very usable as it doesn't support saving the state of any individual beacon. This means that if your server goes down, you will not be able to communicate with any previously-registered beacons anymore. The server doesn't support being initialized with an Ed25519 seed (32 random bytes). Users wishing to use the server in practical cases should use this interface to ensure their server keeps its identity across reboots.
52
+
49
53
  ## Building
50
54
  You will need [Capn'Proto](https://capnproto.org/install.html) (just the binaries) and a recent version of rust for every build.
51
55
 
52
56
  For windows, I prefer building with stable-gnu for normal usage, and nightly-gnu for release builds. You can find the exact arguments I use to the the static library as small as possible in [release.yml](/.github\workflows\release.yml). The MSVC toolchain is expected to work just as well, I just like mingw.
53
57
 
58
+ Build and run all tests:
59
+ ```bash
60
+ cargo test
61
+ cargo build --target x86_64-pc-windows-gnu
62
+ go test ./...
63
+ uv run maturin build --features pybinds --uv
64
+ uv run --no-sync pytest tests
65
+ ```
66
+
54
67
  ## Profiles
55
68
  The reference implementation has two profiles: `PQXDH` and `CNSA2`. Profiles are controlled by cargo features. The CNSA2 profile only exists as a test for now. It uses a simple ML-KEM encapsulation for key exchange and the underlying libraries are not FIPS-approved. It is experimental and is likely broken. PQXDH is the intended target and the default.
56
69
 
70
+ **CNSA2 is currently completely broken**
71
+
57
72
  ## Usage
58
73
  The reference implementation is a library that can currently be used either from rust or through C FFI. The C interface is currently not tested.
59
74
 
@@ -33,7 +33,9 @@ beaconcrypt_go_buffer beaconcrypt_go_process_initial_message(void *handle, const
33
33
  beaconcrypt_go_buffer beaconcrypt_go_encrypt_to_beacon(void *handle, uint64_t key_id, const uint8_t *ptr, uintptr_t len);
34
34
  beaconcrypt_go_buffer beaconcrypt_go_encrypt_to_beacon_signed(void *handle, uint64_t key_id, const uint8_t *ptr, uintptr_t len);
35
35
  beaconcrypt_go_buffer beaconcrypt_go_decrypt_beacon_message(void *handle, uint64_t key_id, const uint8_t *ptr, uintptr_t len);
36
+ beaconcrypt_go_buffer beaconcrypt_go_decrypt_beacon_message_signed(void *handle, const uint8_t *ptr, uintptr_t len);
36
37
  beaconcrypt_go_buffer beaconcrypt_go_encrypt_to_server(void *handle, const uint8_t *ptr, uintptr_t len);
38
+ beaconcrypt_go_buffer beaconcrypt_go_encrypt_to_server_signed(void *handle, const uint8_t *ptr, uintptr_t len);
37
39
  beaconcrypt_go_buffer beaconcrypt_go_decrypt_server_message(void *handle, const uint8_t *ptr, uintptr_t len);
38
40
  beaconcrypt_go_buffer beaconcrypt_go_decrypt_server_message_signed(void *handle, const uint8_t *ptr, uintptr_t len);
39
41
  */
@@ -203,6 +205,15 @@ func (s *Server) DecryptBeaconMessage(keyID uint64, ciphertext []byte) ([]byte,
203
205
  })
204
206
  }
205
207
 
208
+ func (s *Server) DecryptBeaconMessageSigned(ciphertext []byte) ([]byte, error) {
209
+ if s == nil || s.handle == nil {
210
+ return nil, ErrClosed
211
+ }
212
+ return callUnary(ciphertext, func(ptr *C.uint8_t, len C.uintptr_t) C.beaconcrypt_go_buffer {
213
+ return C.beaconcrypt_go_decrypt_beacon_message_signed(s.handle, ptr, len)
214
+ })
215
+ }
216
+
206
217
  func (b *Beacon) EncryptToServer(plaintext []byte) ([]byte, error) {
207
218
  if b == nil || b.handle == nil {
208
219
  return nil, ErrClosed
@@ -212,6 +223,15 @@ func (b *Beacon) EncryptToServer(plaintext []byte) ([]byte, error) {
212
223
  })
213
224
  }
214
225
 
226
+ func (b *Beacon) EncryptToServerSigned(plaintext []byte) ([]byte, error) {
227
+ if b == nil || b.handle == nil {
228
+ return nil, ErrClosed
229
+ }
230
+ return callUnary(plaintext, func(ptr *C.uint8_t, len C.uintptr_t) C.beaconcrypt_go_buffer {
231
+ return C.beaconcrypt_go_encrypt_to_server_signed(b.handle, ptr, len)
232
+ })
233
+ }
234
+
215
235
  func (b *Beacon) DecryptServerMessage(ciphertext []byte) ([]byte, error) {
216
236
  if b == nil || b.handle == nil {
217
237
  return nil, ErrClosed
@@ -240,10 +260,13 @@ func callUnary(data []byte, call func(*C.uint8_t, C.uintptr_t) C.beaconcrypt_go_
240
260
  }
241
261
 
242
262
  func copyBuffer(buffer C.beaconcrypt_go_buffer) ([]byte, error) {
243
- if buffer.ptr == nil || buffer.len == 0 {
263
+ if buffer.ptr == nil {
244
264
  return nil, ErrCrypto
245
265
  }
246
266
  defer C.beaconcrypt_go_free_buffer(buffer)
267
+ if buffer.len == 0 {
268
+ return []byte{}, nil
269
+ }
247
270
  return C.GoBytes(unsafe.Pointer(buffer.ptr), C.int(buffer.len)), nil
248
271
  }
249
272
 
@@ -11,27 +11,34 @@ func registerBeacon(t *testing.T, server *Server, beacon *Beacon) []byte {
11
11
  t.Helper()
12
12
 
13
13
  message := bytes.Repeat([]byte{0xff}, 32)
14
- phase1, err := beacon.GenerateRegistration()
14
+ regOut := registerBeaconWithInitial(t, server, beacon, message)
15
+
16
+ phase2, err := beacon.ProcessInitialMessage(regOut.Serialized)
15
17
  if err != nil {
16
18
  t.Fatal(err)
17
19
  }
20
+ if !bytes.Equal(phase2, message) {
21
+ t.Fatalf("initial message mismatch: got %x want %x", phase2, message)
22
+ }
23
+ return phase2
24
+ }
18
25
 
19
- regOut, err := server.RegisterBeacon(phase1, message)
26
+ func registerBeaconWithInitial(t *testing.T, server *Server, beacon *Beacon, message []byte) *RegistrationResponse {
27
+ t.Helper()
28
+
29
+ phase1, err := beacon.GenerateRegistration()
20
30
  if err != nil {
21
31
  t.Fatal(err)
22
32
  }
23
- if regOut.KeyID == 0 {
24
- t.Fatal("expected non-zero beacon key id")
25
- }
26
33
 
27
- phase2, err := beacon.ProcessInitialMessage(regOut.Serialized)
34
+ regOut, err := server.RegisterBeacon(phase1, message)
28
35
  if err != nil {
29
36
  t.Fatal(err)
30
37
  }
31
- if !bytes.Equal(phase2, message) {
32
- t.Fatalf("initial message mismatch: got %x want %x", phase2, message)
38
+ if regOut.KeyID == 0 {
39
+ t.Fatal("expected non-zero beacon key id")
33
40
  }
34
- return phase2
41
+ return regOut
35
42
  }
36
43
 
37
44
  func newServer(t *testing.T) *Server {
@@ -58,17 +65,43 @@ func newBeacon(t *testing.T, serverPK []byte) *Beacon {
58
65
 
59
66
  func TestRegister(t *testing.T) {
60
67
  server := newServer(t)
61
- beacon := newBeacon(t, nil)
68
+ serverPK, err := server.IdentityPK()
69
+ if err != nil {
70
+ t.Fatal(err)
71
+ }
72
+ beacon := newBeacon(t, serverPK)
62
73
 
63
74
  if registerBeacon(t, server, beacon) == nil {
64
75
  t.Fatal("expected registration to return initial message")
65
76
  }
66
77
  }
67
78
 
79
+ func TestRegisterWithoutInitialMessage(t *testing.T) {
80
+ server := newServer(t)
81
+ serverPK, err := server.IdentityPK()
82
+ if err != nil {
83
+ t.Fatal(err)
84
+ }
85
+ beacon := newBeacon(t, serverPK)
86
+
87
+ regOut := registerBeaconWithInitial(t, server, beacon, nil)
88
+ phase2, err := beacon.ProcessInitialMessage(regOut.Serialized)
89
+ if err != nil {
90
+ t.Fatal(err)
91
+ }
92
+ if len(phase2) != 0 {
93
+ t.Fatalf("expected empty initial message, got %x", phase2)
94
+ }
95
+ }
96
+
68
97
  func TestEncryptToMultiple(t *testing.T) {
69
98
  server := newServer(t)
70
- b1 := newBeacon(t, nil)
71
- b2 := newBeacon(t, nil)
99
+ serverPK, err := server.IdentityPK()
100
+ if err != nil {
101
+ t.Fatal(err)
102
+ }
103
+ b1 := newBeacon(t, serverPK)
104
+ b2 := newBeacon(t, serverPK)
72
105
  message := bytes.Repeat([]byte{0x01}, 32)
73
106
 
74
107
  b1Initial := registerBeacon(t, server, b1)
@@ -92,7 +125,11 @@ func TestEncryptToMultiple(t *testing.T) {
92
125
 
93
126
  func TestEncryptMultiple(t *testing.T) {
94
127
  server := newServer(t)
95
- b1 := newBeacon(t, nil)
128
+ serverPK, err := server.IdentityPK()
129
+ if err != nil {
130
+ t.Fatal(err)
131
+ }
132
+ b1 := newBeacon(t, serverPK)
96
133
  message := bytes.Repeat([]byte{0x01}, 32)
97
134
 
98
135
  registerBeacon(t, server, b1)
@@ -112,7 +149,11 @@ func TestEncryptMultiple(t *testing.T) {
112
149
 
113
150
  func TestDecryptMultiple(t *testing.T) {
114
151
  server := newServer(t)
115
- beacon := newBeacon(t, nil)
152
+ serverPK, err := server.IdentityPK()
153
+ if err != nil {
154
+ t.Fatal(err)
155
+ }
156
+ beacon := newBeacon(t, serverPK)
116
157
  message := bytes.Repeat([]byte{0x01}, 32)
117
158
 
118
159
  registerBeacon(t, server, beacon)
@@ -143,7 +184,11 @@ func TestDecryptMultiple(t *testing.T) {
143
184
 
144
185
  func TestDecryptMultipleSigned(t *testing.T) {
145
186
  server := newServer(t)
146
- beacon := newBeacon(t, nil)
187
+ serverPK, err := server.IdentityPK()
188
+ if err != nil {
189
+ t.Fatal(err)
190
+ }
191
+ beacon := newBeacon(t, serverPK)
147
192
  message := bytes.Repeat([]byte{0x01}, 32)
148
193
 
149
194
  registerBeacon(t, server, beacon)
@@ -206,3 +251,116 @@ func TestDecryptCatchUp(t *testing.T) {
206
251
  t.Fatalf("catch-up decrypt mismatch: got %x and %x want %x", plain1, plain2, message)
207
252
  }
208
253
  }
254
+
255
+ func TestBeaconEncryptsToServer(t *testing.T) {
256
+ server := newServer(t)
257
+ serverPK, err := server.IdentityPK()
258
+ if err != nil {
259
+ t.Fatal(err)
260
+ }
261
+ beacon := newBeacon(t, serverPK)
262
+ message := []byte("beacon to server")
263
+
264
+ registerBeacon(t, server, beacon)
265
+ ciphertext, err := beacon.EncryptToServer(message)
266
+ if err != nil {
267
+ t.Fatal(err)
268
+ }
269
+ plaintext, err := server.DecryptBeaconMessage(1, ciphertext)
270
+ if err != nil {
271
+ t.Fatal(err)
272
+ }
273
+ if !bytes.Equal(plaintext, message) {
274
+ t.Fatalf("beacon-to-server decrypt mismatch: got %x want %x", plaintext, message)
275
+ }
276
+ }
277
+
278
+ func TestBeaconEncryptsToServerSigned(t *testing.T) {
279
+ server := newServer(t)
280
+ serverPK, err := server.IdentityPK()
281
+ if err != nil {
282
+ t.Fatal(err)
283
+ }
284
+ beacon := newBeacon(t, serverPK)
285
+ message := []byte("signed beacon to server")
286
+
287
+ registerBeacon(t, server, beacon)
288
+ signed, err := beacon.EncryptToServerSigned(message)
289
+ if err != nil {
290
+ t.Fatal(err)
291
+ }
292
+ plaintext, err := server.DecryptBeaconMessageSigned(signed)
293
+ if err != nil {
294
+ t.Fatal(err)
295
+ }
296
+ if !bytes.Equal(plaintext, message) {
297
+ t.Fatalf("signed beacon-to-server decrypt mismatch: got %x want %x", plaintext, message)
298
+ }
299
+ }
300
+
301
+ func TestSignedServerMessageRejectsTampering(t *testing.T) {
302
+ server := newServer(t)
303
+ serverPK, err := server.IdentityPK()
304
+ if err != nil {
305
+ t.Fatal(err)
306
+ }
307
+ beacon := newBeacon(t, serverPK)
308
+
309
+ registerBeacon(t, server, beacon)
310
+ signed, err := server.EncryptToBeaconSigned(1, []byte("server to beacon"))
311
+ if err != nil {
312
+ t.Fatal(err)
313
+ }
314
+ signed[len(signed)-1] ^= 0x01
315
+
316
+ if _, err := beacon.DecryptServerMessageSigned(signed); err == nil {
317
+ t.Fatal("expected tampered signed message to be rejected")
318
+ }
319
+ }
320
+
321
+ func TestBeaconCannotDecryptMessageForDifferentBeacon(t *testing.T) {
322
+ server := newServer(t)
323
+ serverPK, err := server.IdentityPK()
324
+ if err != nil {
325
+ t.Fatal(err)
326
+ }
327
+ b1 := newBeacon(t, serverPK)
328
+ b2 := newBeacon(t, serverPK)
329
+
330
+ registerBeacon(t, server, b1)
331
+ registerBeacon(t, server, b2)
332
+ ciphertext, err := server.EncryptToBeacon(1, []byte("for b1 only"))
333
+ if err != nil {
334
+ t.Fatal(err)
335
+ }
336
+
337
+ if _, err := b2.DecryptServerMessage(ciphertext); err == nil {
338
+ t.Fatal("expected message for another beacon to be rejected")
339
+ }
340
+ }
341
+
342
+ func TestCiphertextCannotBeReplayed(t *testing.T) {
343
+ server := newServer(t)
344
+ serverPK, err := server.IdentityPK()
345
+ if err != nil {
346
+ t.Fatal(err)
347
+ }
348
+ beacon := newBeacon(t, serverPK)
349
+ message := []byte("one shot")
350
+
351
+ registerBeacon(t, server, beacon)
352
+ ciphertext, err := server.EncryptToBeacon(1, message)
353
+ if err != nil {
354
+ t.Fatal(err)
355
+ }
356
+ plaintext, err := beacon.DecryptServerMessage(ciphertext)
357
+ if err != nil {
358
+ t.Fatal(err)
359
+ }
360
+ if !bytes.Equal(plaintext, message) {
361
+ t.Fatalf("replay setup decrypt mismatch: got %x want %x", plaintext, message)
362
+ }
363
+ if _, err := beacon.DecryptServerMessage(ciphertext); err == nil {
364
+ t.Fatal("expected replayed ciphertext to be rejected")
365
+ }
366
+ }
@@ -455,10 +455,18 @@ struct beaconcrypt_GoBuffer beaconcrypt_go_decrypt_beacon_message(struct beaconc
455
455
  const uint8_t *ptr,
456
456
  uintptr_t len);
457
457
 
458
+ struct beaconcrypt_GoBuffer beaconcrypt_go_decrypt_beacon_message_signed(struct beaconcrypt_BeaconCryptPqxdh *handle,
459
+ const uint8_t *ptr,
460
+ uintptr_t len);
461
+
458
462
  struct beaconcrypt_GoBuffer beaconcrypt_go_encrypt_to_server(struct beaconcrypt_BeaconCryptPqxdh *handle,
459
463
  const uint8_t *ptr,
460
464
  uintptr_t len);
461
465
 
466
+ struct beaconcrypt_GoBuffer beaconcrypt_go_encrypt_to_server_signed(struct beaconcrypt_BeaconCryptPqxdh *handle,
467
+ const uint8_t *ptr,
468
+ uintptr_t len);
469
+
462
470
  struct beaconcrypt_GoBuffer beaconcrypt_go_decrypt_server_message(struct beaconcrypt_BeaconCryptPqxdh *handle,
463
471
  const uint8_t *ptr,
464
472
  uintptr_t len);
@@ -12,6 +12,10 @@ classifiers = [
12
12
  ]
13
13
  dynamic = ["version"]
14
14
 
15
+ [tool.maturin]
16
+ bindings = "pyo3"
17
+ features = ["pybinds"]
18
+
15
19
  [dependency-groups]
16
20
  dev = [
17
21
  "black>=26.5.1",
@@ -320,6 +320,7 @@ impl ProviderBeacon for BeaconCryptCnsa2 {
320
320
  self.init_ratchets(&derived_secret, &info_str, true, srv_key_id);
321
321
 
322
322
  match response.get_app_cipher_text() {
323
+ Ok(ciphertext) if ciphertext.is_empty() => Some(vec![]),
323
324
  Ok(ciphertext) => match self.decrypt_message(ciphertext, srv_key_id, true) {
324
325
  Some(plaintext) => Some(plaintext),
325
326
  None => None,
@@ -296,6 +296,28 @@ mod gobinds {
296
296
  decrypt(handle, ptr, len, key_id, false)
297
297
  }
298
298
 
299
+ #[unsafe(no_mangle)]
300
+ pub extern "C" fn beaconcrypt_go_decrypt_beacon_message_signed(
301
+ handle: *mut BeaconCryptPqxdh,
302
+ ptr: *const u8,
303
+ len: usize,
304
+ ) -> GoBuffer {
305
+ if handle.is_null() {
306
+ return empty_buffer();
307
+ }
308
+ let Some(data) = (unsafe { input(ptr, len) }) else {
309
+ return empty_buffer();
310
+ };
311
+ let provider = unsafe { &mut *handle };
312
+ match provider.verify_signature(data) {
313
+ Some(verified) => provider
314
+ .decrypt_message(&verified.data, verified.key_id, false)
315
+ .map(into_buffer)
316
+ .unwrap_or_else(empty_buffer),
317
+ None => empty_buffer(),
318
+ }
319
+ }
320
+
299
321
  #[unsafe(no_mangle)]
300
322
  pub extern "C" fn beaconcrypt_go_encrypt_to_server(
301
323
  handle: *mut BeaconCryptPqxdh,
@@ -309,6 +331,29 @@ mod gobinds {
309
331
  encrypt(handle, ptr, len, false, provider.server_kid())
310
332
  }
311
333
 
334
+ #[unsafe(no_mangle)]
335
+ pub extern "C" fn beaconcrypt_go_encrypt_to_server_signed(
336
+ handle: *mut BeaconCryptPqxdh,
337
+ ptr: *const u8,
338
+ len: usize,
339
+ ) -> GoBuffer {
340
+ if handle.is_null() {
341
+ return empty_buffer();
342
+ }
343
+ let Some(data) = (unsafe { input(ptr, len) }) else {
344
+ return empty_buffer();
345
+ };
346
+ let provider = unsafe { &mut *handle };
347
+ let srv_kid = provider.server_kid();
348
+ match provider.encrypt_message(data, false, srv_kid) {
349
+ Some(ciphertext) => provider
350
+ .sign_message(ciphertext.as_slice())
351
+ .map(into_buffer)
352
+ .unwrap_or_else(empty_buffer),
353
+ None => empty_buffer(),
354
+ }
355
+ }
356
+
312
357
  #[unsafe(no_mangle)]
313
358
  pub extern "C" fn beaconcrypt_go_decrypt_server_message(
314
359
  handle: *mut BeaconCryptPqxdh,
@@ -475,9 +520,9 @@ pub mod beaconcrypt_py {
475
520
  #[pymethods]
476
521
  impl Beacon {
477
522
  #[new]
478
- fn new(server_kid: u64, server_id_pk: Option<&[u8]>) -> Self {
523
+ fn new(server_kid: u64, server_id_pk: &[u8]) -> Self {
479
524
  Self {
480
- _0: BeaconCryptPqxdh::new(true, server_kid, server_id_pk, None),
525
+ _0: BeaconCryptPqxdh::new(true, server_kid, Some(server_id_pk), None),
481
526
  }
482
527
  }
483
528
 
@@ -143,7 +143,7 @@ impl CryptoProvider for BeaconCryptPqxdh {
143
143
  let signed = crypto_sign::sign(data, self.identity_sk()).ok()?;
144
144
  builder.set_data(&signed);
145
145
  let mut buffer = vec![];
146
- capnp::serialize_packed::write_message(&mut buffer, t_builder.borrow_inner()).unwrap();
146
+ capnp::serialize_packed::write_message(&mut buffer, t_builder.borrow_inner()).ok()?;
147
147
  Some(buffer)
148
148
  }
149
149
 
@@ -315,7 +315,7 @@ impl ProviderBeacon for BeaconCryptPqxdh {
315
315
  bundle.set_pq_key(&pq_sig);
316
316
 
317
317
  let mut buffer = vec![];
318
- capnp::serialize::write_message(&mut buffer, msg.borrow_inner()).unwrap();
318
+ capnp::serialize::write_message(&mut buffer, msg.borrow_inner()).ok()?;
319
319
  Some(buffer)
320
320
  }
321
321
 
@@ -365,6 +365,7 @@ impl ProviderBeacon for BeaconCryptPqxdh {
365
365
  self.init_ratchets(&derived_secret, &info_str, true, srv_key_id);
366
366
 
367
367
  match response.get_app_cipher_text() {
368
+ Ok(ciphertext) if ciphertext.is_empty() => Some(vec![]),
368
369
  Ok(ciphertext) => self.decrypt_message(ciphertext, srv_key_id, true),
369
370
  Err(_) => Some(vec![0u8; 0]),
370
371
  }
@@ -374,17 +375,16 @@ impl ProviderBeacon for BeaconCryptPqxdh {
374
375
  #[cfg(feature = "server")]
375
376
  impl ProviderServer for BeaconCryptPqxdh {
376
377
  fn get_shared_secret(&mut self, buffer: &[u8]) -> Option<RegistrationOutput> {
377
- let reader = capnp::serialize::read_message(buffer, ReaderOptions::new()).unwrap();
378
+ let reader = capnp::serialize::read_message(buffer, ReaderOptions::new()).ok()?;
378
379
  let typed_reader = TypedReader::<_, phase1_capnp::init_kex::Owned>::new(reader);
379
- let registration = typed_reader.get().unwrap();
380
+ let registration = typed_reader.get().ok()?;
380
381
 
381
382
  let decoded_beacon_id = decode_sign(registration.get_identity_key().ok()?).ok()?;
382
383
  let remote_id = crypto_sign::PublicKey::from_bytes(&decoded_beacon_id).ok()?;
383
- let pq_verified = crypto_sign::verify(registration.get_pq_key().ok()?, &remote_id).unwrap();
384
- let prekey_verified =
385
- crypto_sign::verify(registration.get_pre_key().ok()?, &remote_id).unwrap();
384
+ let pq_verified = crypto_sign::verify(registration.get_pq_key().ok()?, &remote_id)?;
385
+ let prekey_verified = crypto_sign::verify(registration.get_pre_key().ok()?, &remote_id)?;
386
386
  let onetime_verified =
387
- crypto_sign::verify(registration.get_one_time_key().ok()?, &remote_id).unwrap();
387
+ crypto_sign::verify(registration.get_one_time_key().ok()?, &remote_id)?;
388
388
 
389
389
  let beacon_prekey =
390
390
  crypto_kx::PublicKey::from_bytes(&decode_kem(&prekey_verified).ok()?).ok()?;
@@ -0,0 +1,119 @@
1
+ use beaconcrypt::*;
2
+
3
+ const SERVER_KID: u64 = 0;
4
+
5
+ fn new_pair() -> (BeaconCryptPqxdh, BeaconCryptPqxdh) {
6
+ let server = BeaconCryptPqxdh::new(false, SERVER_KID, None, None);
7
+ let server_id = server.identity_pk().to_owned();
8
+ let beacon = BeaconCryptPqxdh::new(true, SERVER_KID, Some(server_id.as_bytes()), None);
9
+ (server, beacon)
10
+ }
11
+
12
+ fn register_beacon(
13
+ server: &mut BeaconCryptPqxdh,
14
+ beacon: &mut BeaconCryptPqxdh,
15
+ initial_message: Option<&[u8]>,
16
+ ) -> RegResponse {
17
+ let phase_1 = beacon.get_registration_bundle().unwrap();
18
+ let reg_out = server.get_shared_secret(&phase_1).unwrap();
19
+ let phase_2 = server
20
+ .build_registration_response(reg_out, initial_message)
21
+ .unwrap();
22
+ let plaintext = beacon.finish_registration(&phase_2.serialized).unwrap();
23
+ assert_eq!(plaintext, initial_message.unwrap_or(&[]));
24
+ phase_2
25
+ }
26
+
27
+ #[test]
28
+ fn registration_can_omit_initial_message() {
29
+ let (mut server, mut beacon) = new_pair();
30
+
31
+ let response = register_beacon(&mut server, &mut beacon, None);
32
+
33
+ assert_eq!(response.kid, 1);
34
+ }
35
+
36
+ #[test]
37
+ fn beacon_can_encrypt_to_server() {
38
+ let (mut server, mut beacon) = new_pair();
39
+ let response = register_beacon(&mut server, &mut beacon, Some(&[0xFF; 32]));
40
+ let message = b"beacon to server";
41
+
42
+ let ciphertext = beacon
43
+ .encrypt_message(message.as_slice(), false, SERVER_KID)
44
+ .unwrap();
45
+ let plaintext = server
46
+ .decrypt_message(&ciphertext, response.kid, false)
47
+ .unwrap();
48
+
49
+ assert_eq!(plaintext, message);
50
+ }
51
+
52
+ #[test]
53
+ fn beacon_can_encrypt_to_server_signed() {
54
+ let (mut server, mut beacon) = new_pair();
55
+ let response = register_beacon(&mut server, &mut beacon, Some(&[0xFF; 32]));
56
+ let message = b"signed beacon to server";
57
+
58
+ let ciphertext = beacon
59
+ .encrypt_message(message.as_slice(), false, SERVER_KID)
60
+ .unwrap();
61
+ let signed = beacon.sign_message(&ciphertext).unwrap();
62
+ let verified = server.verify_signature(&signed).unwrap();
63
+ let plaintext = server
64
+ .decrypt_message(&verified.data, verified.key_id, false)
65
+ .unwrap();
66
+
67
+ assert_eq!(verified.key_id, response.kid);
68
+ assert_eq!(plaintext, message);
69
+ }
70
+
71
+ #[test]
72
+ fn signed_server_message_rejects_tampering() {
73
+ let (mut server, mut beacon) = new_pair();
74
+ let response = register_beacon(&mut server, &mut beacon, Some(&[0xFF; 32]));
75
+
76
+ let ciphertext = server
77
+ .encrypt_message(b"server to beacon", true, response.kid)
78
+ .unwrap();
79
+ let mut signed = server.sign_message(&ciphertext).unwrap();
80
+ let last = signed.len() - 1;
81
+ signed[last] ^= 0x01;
82
+
83
+ assert!(beacon.verify_signature(&signed).is_none());
84
+ }
85
+
86
+ #[test]
87
+ fn beacon_cannot_decrypt_message_for_different_beacon() {
88
+ let mut server = BeaconCryptPqxdh::new(false, SERVER_KID, None, None);
89
+ let server_id = server.identity_pk().to_owned();
90
+ let mut b1 = BeaconCryptPqxdh::new(true, SERVER_KID, Some(server_id.as_bytes()), None);
91
+ let mut b2 = BeaconCryptPqxdh::new(true, SERVER_KID, Some(server_id.as_bytes()), None);
92
+ let b1_response = register_beacon(&mut server, &mut b1, Some(&[0xFF; 32]));
93
+ let _ = register_beacon(&mut server, &mut b2, Some(&[0xFF; 32]));
94
+
95
+ let ciphertext = server
96
+ .encrypt_message(b"for b1 only", true, b1_response.kid)
97
+ .unwrap();
98
+
99
+ assert!(b2.decrypt_message(&ciphertext, SERVER_KID, true).is_none());
100
+ }
101
+
102
+ #[test]
103
+ fn ciphertext_cannot_be_replayed() {
104
+ let (mut server, mut beacon) = new_pair();
105
+ let response = register_beacon(&mut server, &mut beacon, Some(&[0xFF; 32]));
106
+ let message = b"one shot";
107
+
108
+ let ciphertext = server.encrypt_message(message, true, response.kid).unwrap();
109
+ let first = beacon
110
+ .decrypt_message(&ciphertext, SERVER_KID, true)
111
+ .unwrap();
112
+
113
+ assert_eq!(first, message);
114
+ assert!(
115
+ beacon
116
+ .decrypt_message(&ciphertext, SERVER_KID, true)
117
+ .is_none()
118
+ );
119
+ }
@@ -0,0 +1,168 @@
1
+ from beaconcrypt import BeaconCryptBeacon, BeaconCryptServer
2
+
3
+
4
+ def register_beacon(
5
+ server: BeaconCryptServer,
6
+ beacon: BeaconCryptBeacon,
7
+ ) -> bytes | None:
8
+ message = bytes([0xFF]) * 32
9
+ phase_1 = beacon.generate_registration()
10
+ assert phase_1 is not None
11
+ reg_out = server.register_beacon(phase_1, message)
12
+ assert reg_out is not None
13
+ phase2 = beacon.process_initial_message(reg_out.serialized())
14
+ assert phase2 is not None
15
+ assert phase2 == message
16
+ return phase2
17
+
18
+
19
+ def test_encrypt_to_multiple():
20
+ server = BeaconCryptServer(0, None)
21
+ server_pk = server.id_pk()
22
+ b1 = BeaconCryptBeacon(0, server_pk)
23
+ b2 = BeaconCryptBeacon(0, server_pk)
24
+ message = bytes([0x1]) * 32
25
+
26
+ b1_initial = register_beacon(server, b1)
27
+ b2_initial = register_beacon(server, b2)
28
+ assert b2_initial == b1_initial
29
+
30
+ b1_m1 = server.encrypt_to_beacon(message, 1)
31
+ b2_m1 = server.encrypt_to_beacon(message, 2)
32
+ assert b1_m1 is not None and b2_m1 is not None and b1_m1 != b2_m1
33
+
34
+
35
+ def test_encrypt_multiple():
36
+ server = BeaconCryptServer(0, None)
37
+ server_pk = server.id_pk()
38
+ b1 = BeaconCryptBeacon(0, server_pk)
39
+ message = bytes([0x1]) * 32
40
+
41
+ _ = register_beacon(server, b1)
42
+
43
+ b1_m1 = server.encrypt_to_beacon(message, 1)
44
+ b1_m2 = server.encrypt_to_beacon(message, 1)
45
+ assert b1_m1 is not None and b1_m2 is not None and b1_m1 != b1_m2
46
+
47
+
48
+ def test_decrypt_multiple():
49
+ server = BeaconCryptServer(0, None)
50
+ server_pk = server.id_pk()
51
+ beacon = BeaconCryptBeacon(0, server_pk)
52
+ message = bytes([0x1]) * 32
53
+
54
+ _ = register_beacon(server, beacon)
55
+ m1 = server.encrypt_to_beacon(message, 1)
56
+ m2 = server.encrypt_to_beacon(message, 1)
57
+ assert m1 != m2
58
+
59
+ plain1 = beacon.decrypt_server_message(m1)
60
+ plain2 = beacon.decrypt_server_message(m2)
61
+ assert plain2 is not None and plain1 is not None
62
+ assert plain2 == plain1 == message
63
+
64
+
65
+ def test_decrypt_multiple_signed():
66
+ server = BeaconCryptServer(0, None)
67
+ server_pk = server.id_pk()
68
+ beacon = BeaconCryptBeacon(0, server_pk)
69
+ message = bytes([0x1]) * 32
70
+
71
+ _ = register_beacon(server, beacon)
72
+ m1 = server.encrypt_to_beacon_signed(message, 1)
73
+ m2 = server.encrypt_to_beacon_signed(message, 1)
74
+ assert m1 != m2
75
+
76
+ plain1 = beacon.decrypt_server_message_signed(m1)
77
+ plain2 = beacon.decrypt_server_message_signed(m2)
78
+ assert plain2 is not None and plain1 is not None
79
+ assert plain2 == plain1 == message
80
+
81
+
82
+ def test_decrypt_catch_up():
83
+ server = BeaconCryptServer(0, None)
84
+ server_pk = server.id_pk()
85
+ beacon = BeaconCryptBeacon(0, server_pk)
86
+ message = bytes([0x1]) * 32
87
+
88
+ _ = register_beacon(server, beacon)
89
+ m1 = server.encrypt_to_beacon(message, 1)
90
+ m2 = server.encrypt_to_beacon(message, 1)
91
+ assert m1 != m2
92
+
93
+ plain2 = beacon.decrypt_server_message(m2)
94
+ plain1 = beacon.decrypt_server_message(m1)
95
+ assert plain2 is not None and plain1 is not None
96
+ assert plain2 == plain1 == message
97
+
98
+
99
+ def test_beacon_encrypts_to_server():
100
+ server = BeaconCryptServer(0, None)
101
+ server_pk = server.id_pk()
102
+ beacon = BeaconCryptBeacon(0, server_pk)
103
+ message = b"beacon to server"
104
+
105
+ _ = register_beacon(server, beacon)
106
+ ciphertext = beacon.encrypt_message_to_server(message)
107
+ assert ciphertext is not None
108
+
109
+ plaintext = server.decrypt_beacon_message(ciphertext, 1)
110
+ assert plaintext == message
111
+
112
+
113
+ def test_beacon_encrypts_to_server_signed():
114
+ server = BeaconCryptServer(0, None)
115
+ server_pk = server.id_pk()
116
+ beacon = BeaconCryptBeacon(0, server_pk)
117
+ message = b"signed beacon to server"
118
+
119
+ _ = register_beacon(server, beacon)
120
+ signed = beacon.encrypt_to_server_signed(message)
121
+ assert signed is not None
122
+
123
+ plaintext = server.decrypt_beacon_message_signed(signed)
124
+ assert plaintext == message
125
+
126
+
127
+ def test_signed_server_message_rejects_tampering():
128
+ server = BeaconCryptServer(0, None)
129
+ server_pk = server.id_pk()
130
+ beacon = BeaconCryptBeacon(0, server_pk)
131
+ message = b"server to beacon"
132
+
133
+ _ = register_beacon(server, beacon)
134
+ signed = server.encrypt_to_beacon_signed(message, 1)
135
+ assert signed is not None
136
+ tampered = bytearray(signed)
137
+ tampered[-1] ^= 0x01
138
+
139
+ assert beacon.decrypt_server_message_signed(bytes(tampered)) is None
140
+
141
+
142
+ def test_beacon_cannot_decrypt_message_for_different_beacon():
143
+ server = BeaconCryptServer(0, None)
144
+ server_pk = server.id_pk()
145
+ b1 = BeaconCryptBeacon(0, server_pk)
146
+ b2 = BeaconCryptBeacon(0, server_pk)
147
+ message = b"for b1 only"
148
+
149
+ _ = register_beacon(server, b1)
150
+ _ = register_beacon(server, b2)
151
+ ciphertext = server.encrypt_to_beacon(message, 1)
152
+ assert ciphertext is not None
153
+
154
+ assert b2.decrypt_server_message(ciphertext) is None
155
+
156
+
157
+ def test_ciphertext_cannot_be_replayed():
158
+ server = BeaconCryptServer(0, None)
159
+ server_pk = server.id_pk()
160
+ beacon = BeaconCryptBeacon(0, server_pk)
161
+ message = b"one shot"
162
+
163
+ _ = register_beacon(server, beacon)
164
+ ciphertext = server.encrypt_to_beacon(message, 1)
165
+ assert ciphertext is not None
166
+
167
+ assert beacon.decrypt_server_message(ciphertext) == message
168
+ assert beacon.decrypt_server_message(ciphertext) is None
@@ -5,7 +5,7 @@ def register_beacon(
5
5
  server: BeaconCryptServer,
6
6
  beacon: BeaconCryptBeacon,
7
7
  ) -> bytes | None:
8
- message = bytes(0xFF * 32)
8
+ message = bytes([0xFF]) * 32
9
9
  phase_1 = beacon.generate_registration()
10
10
  assert phase_1 is not None
11
11
  reg_out = server.register_beacon(phase_1, message)
@@ -18,6 +18,21 @@ def register_beacon(
18
18
 
19
19
  def test_register():
20
20
  server = BeaconCryptServer(0, None)
21
- beacon = BeaconCryptBeacon(0, None)
21
+ server_pk = server.id_pk()
22
+ beacon = BeaconCryptBeacon(0, server_pk)
22
23
 
23
24
  assert register_beacon(server, beacon) is not None
25
+
26
+
27
+ def test_register_without_initial_message():
28
+ server = BeaconCryptServer(0, None)
29
+ server_pk = server.id_pk()
30
+ beacon = BeaconCryptBeacon(0, server_pk)
31
+
32
+ phase_1 = beacon.generate_registration()
33
+ assert phase_1 is not None
34
+ reg_out = server.register_beacon(phase_1, None)
35
+ assert reg_out is not None
36
+
37
+ phase_2 = beacon.process_initial_message(reg_out.serialized())
38
+ assert phase_2 == b""
@@ -1,92 +0,0 @@
1
- from beaconcrypt import BeaconCryptBeacon, BeaconCryptServer
2
-
3
-
4
- def register_beacon(
5
- server: BeaconCryptServer,
6
- beacon: BeaconCryptBeacon,
7
- ) -> bytes | None:
8
- message = bytes(0xFF * 32)
9
- phase_1 = beacon.generate_registration()
10
- assert phase_1 is not None
11
- reg_out = server.register_beacon(phase_1, message)
12
- assert reg_out is not None
13
- phase2 = beacon.process_initial_message(reg_out.serialized())
14
- assert phase2 is not None
15
- assert phase2 == message
16
- return phase2
17
-
18
-
19
- def test_encrypt_to_multiple():
20
- server = BeaconCryptServer(0, None)
21
- b1 = BeaconCryptBeacon(0, None)
22
- b2 = BeaconCryptBeacon(0, None)
23
- message = bytes(0x1 * 32)
24
-
25
- b1_initial = register_beacon(server, b1)
26
- b2_initial = register_beacon(server, b2)
27
- assert b2_initial == b1_initial
28
-
29
- b1_m1 = server.encrypt_to_beacon(message, 1)
30
- b2_m1 = server.encrypt_to_beacon(message, 2)
31
- assert b1_m1 is not None and b2_m1 is not None and b1_m1 != b2_m1
32
-
33
-
34
- def test_encrypt_multiple():
35
- server = BeaconCryptServer(0, None)
36
- b1 = BeaconCryptBeacon(0, None)
37
- message = bytes(0x1 * 32)
38
-
39
- _ = register_beacon(server, b1)
40
-
41
- b1_m1 = server.encrypt_to_beacon(message, 1)
42
- b1_m2 = server.encrypt_to_beacon(message, 1)
43
- assert b1_m1 is not None and b1_m2 is not None and b1_m1 != b1_m2
44
-
45
-
46
- def test_decrypt_multiple():
47
- server = BeaconCryptServer(0, None)
48
- beacon = BeaconCryptBeacon(0, None)
49
- message = bytes(0x1 * 32)
50
-
51
- _ = register_beacon(server, beacon)
52
- m1 = server.encrypt_to_beacon(message, 1)
53
- m2 = server.encrypt_to_beacon(message, 1)
54
- assert m1 != m2
55
-
56
- plain1 = beacon.decrypt_server_message(m1)
57
- plain2 = beacon.decrypt_server_message(m2)
58
- assert plain2 is not None and plain1 is not None
59
- assert plain2 == plain1 == message
60
-
61
-
62
- def test_decrypt_multiple_signed():
63
- server = BeaconCryptServer(0, None)
64
- beacon = BeaconCryptBeacon(0, None)
65
- message = bytes(0x1 * 32)
66
-
67
- _ = register_beacon(server, beacon)
68
- m1 = server.encrypt_to_beacon_signed(message, 1)
69
- m2 = server.encrypt_to_beacon_signed(message, 1)
70
- assert m1 != m2
71
-
72
- plain1 = beacon.decrypt_server_message_signed(m1)
73
- plain2 = beacon.decrypt_server_message_signed(m2)
74
- assert plain2 is not None and plain1 is not None
75
- assert plain2 == plain1 == message
76
-
77
-
78
- def test_decrypt_catch_up():
79
- server = BeaconCryptServer(0, None)
80
- server_pk = server.id_pk()
81
- beacon = BeaconCryptBeacon(0, server_pk)
82
- message = bytes(0x1 * 32)
83
-
84
- _ = register_beacon(server, beacon)
85
- m1 = server.encrypt_to_beacon(message, 1)
86
- m2 = server.encrypt_to_beacon(message, 1)
87
- assert m1 != m2
88
-
89
- plain2 = beacon.decrypt_server_message(m2)
90
- plain1 = beacon.decrypt_server_message(m1)
91
- assert plain2 is not None and plain1 is not None
92
- assert plain2 == plain1 == message
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes