ratchetkit 1.2.0__py3-none-any.whl

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 (71) hide show
  1. ratchetkit/__init__.py +3 -0
  2. ratchetkit/algorithms/__init__.py +1 -0
  3. ratchetkit/algorithms/cipher/__init__.py +17 -0
  4. ratchetkit/algorithms/cipher/aes128.py +15 -0
  5. ratchetkit/algorithms/cipher/aes256.py +15 -0
  6. ratchetkit/algorithms/cipher/base.py +65 -0
  7. ratchetkit/algorithms/cipher/camellia128.py +18 -0
  8. ratchetkit/algorithms/cipher/camellia256.py +18 -0
  9. ratchetkit/algorithms/cipher/chacha20.py +17 -0
  10. ratchetkit/algorithms/hash/__init__.py +17 -0
  11. ratchetkit/algorithms/hash/base.py +30 -0
  12. ratchetkit/algorithms/hash/blake2b.py +26 -0
  13. ratchetkit/algorithms/hash/sha256.py +26 -0
  14. ratchetkit/algorithms/hash/sha3256.py +26 -0
  15. ratchetkit/algorithms/hash/sha384.py +26 -0
  16. ratchetkit/algorithms/hash/sha512.py +26 -0
  17. ratchetkit/algorithms/kem/__init__.py +12 -0
  18. ratchetkit/algorithms/kem/base.py +63 -0
  19. ratchetkit/algorithms/kem/ec_base.py +104 -0
  20. ratchetkit/algorithms/kem/mlkem1024.py +15 -0
  21. ratchetkit/algorithms/kem/mlkem768.py +15 -0
  22. ratchetkit/algorithms/kem/mlkem_base.py +97 -0
  23. ratchetkit/algorithms/kem/rsa2048.py +10 -0
  24. ratchetkit/algorithms/kem/rsa4096.py +10 -0
  25. ratchetkit/algorithms/kem/rsa_base.py +151 -0
  26. ratchetkit/algorithms/kem/secp256r1.py +12 -0
  27. ratchetkit/algorithms/kem/secp384r1.py +12 -0
  28. ratchetkit/algorithms/kem/secp521r1.py +12 -0
  29. ratchetkit/algorithms/kem/x25519.py +107 -0
  30. ratchetkit/algorithms/kem/x448.py +107 -0
  31. ratchetkit/errors.py +61 -0
  32. ratchetkit/handshake/__init__.py +15 -0
  33. ratchetkit/handshake/message.py +177 -0
  34. ratchetkit/handshake/pqxdh.py +251 -0
  35. ratchetkit/handshake/x3dh.py +301 -0
  36. ratchetkit/kdf.py +70 -0
  37. ratchetkit/keys/__init__.py +5 -0
  38. ratchetkit/keys/chain.py +57 -0
  39. ratchetkit/keys/identity.py +36 -0
  40. ratchetkit/keys/keypair.py +98 -0
  41. ratchetkit/keys/message.py +87 -0
  42. ratchetkit/keys/prekey.py +372 -0
  43. ratchetkit/keys/root.py +102 -0
  44. ratchetkit/packing.py +154 -0
  45. ratchetkit/py.typed +0 -0
  46. ratchetkit/ratchet/__init__.py +22 -0
  47. ratchetkit/ratchet/chains.py +68 -0
  48. ratchetkit/ratchet/header.py +51 -0
  49. ratchetkit/ratchet/limits.py +35 -0
  50. ratchetkit/ratchet/message/__init__.py +11 -0
  51. ratchetkit/ratchet/message/base.py +44 -0
  52. ratchetkit/ratchet/message/double.py +65 -0
  53. ratchetkit/ratchet/message/triple.py +91 -0
  54. ratchetkit/ratchet/protocol/__init__.py +11 -0
  55. ratchetkit/ratchet/protocol/base.py +49 -0
  56. ratchetkit/ratchet/protocol/double.py +504 -0
  57. ratchetkit/ratchet/protocol/triple.py +258 -0
  58. ratchetkit/ratchet/skipped.py +123 -0
  59. ratchetkit/ratchet/spqr.py +555 -0
  60. ratchetkit/scka/__init__.py +18 -0
  61. ratchetkit/scka/chunk.py +124 -0
  62. ratchetkit/scka/kem_scka.py +410 -0
  63. ratchetkit/session.py +235 -0
  64. ratchetkit/suite/__init__.py +33 -0
  65. ratchetkit/suite/algs.py +94 -0
  66. ratchetkit/suite/double.py +135 -0
  67. ratchetkit/suite/triple.py +118 -0
  68. ratchetkit-1.2.0.dist-info/METADATA +268 -0
  69. ratchetkit-1.2.0.dist-info/RECORD +71 -0
  70. ratchetkit-1.2.0.dist-info/WHEEL +4 -0
  71. ratchetkit-1.2.0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,10 @@
1
+ """RSA-2048 OAEP encapsulation + RSA-PSS signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ratchetkit.algorithms.kem.rsa_base import RsaKem
6
+
7
+
8
+ class Rsa2048(RsaKem):
9
+ name = "rsa2048"
10
+ key_size = 2048
@@ -0,0 +1,10 @@
1
+ """RSA-4096 OAEP encapsulation + RSA-PSS signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ratchetkit.algorithms.kem.rsa_base import RsaKem
6
+
7
+
8
+ class Rsa4096(RsaKem):
9
+ name = "rsa4096"
10
+ key_size = 4096
@@ -0,0 +1,151 @@
1
+ """Shared RSA-OAEP encapsulation + RSA-PSS signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TYPE_CHECKING
7
+
8
+ from cryptography.hazmat.primitives import hashes, serialization
9
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa, utils
10
+ from cryptography.hazmat.primitives.serialization import (
11
+ Encoding,
12
+ NoEncryption,
13
+ PrivateFormat,
14
+ PublicFormat,
15
+ )
16
+
17
+ from ratchetkit.errors import InvalidKey, InvalidSignature
18
+ from ratchetkit.algorithms.kem.base import KemBackend
19
+ from ratchetkit.keys.keypair import (
20
+ KeyPair,
21
+ PrivateKey,
22
+ PublicKey,
23
+ SigningKeyPair,
24
+ SigningPrivateKey,
25
+ SigningPublicKey,
26
+ )
27
+
28
+ if TYPE_CHECKING:
29
+ from ratchetkit.algorithms.hash.base import HashAlgorithm
30
+
31
+
32
+ class RsaKem(KemBackend):
33
+ """RSA-OAEP encapsulation + RSA-PSS signatures backend base."""
34
+
35
+ name: str
36
+ key_size: int
37
+ supports_dh = False
38
+ secret_size = 32
39
+
40
+ def generate_keypair(self) -> KeyPair:
41
+ private = rsa.generate_private_key(
42
+ public_exponent=65537,
43
+ key_size=self.key_size,
44
+ )
45
+ return KeyPair(
46
+ public=PublicKey(self.encode_public(private.public_key()), self.name),
47
+ private=PrivateKey(self.encode_private(private), self.name),
48
+ )
49
+
50
+ def encapsulate(self, their_public: PublicKey) -> tuple[bytes, bytes]:
51
+ public = self.decode_public(their_public.data)
52
+ secret = os.urandom(self.secret_size)
53
+ ciphertext = public.encrypt(
54
+ secret,
55
+ padding.OAEP(
56
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
57
+ algorithm=hashes.SHA256(),
58
+ label=None,
59
+ ),
60
+ )
61
+ return secret, ciphertext
62
+
63
+ def decapsulate(self, our_private: PrivateKey, ciphertext: bytes) -> bytes:
64
+ private = self.decode_private(our_private.data)
65
+ try:
66
+ return private.decrypt(
67
+ ciphertext,
68
+ padding.OAEP(
69
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
70
+ algorithm=hashes.SHA256(),
71
+ label=None,
72
+ ),
73
+ )
74
+ except Exception as exc:
75
+ raise InvalidKey("RSA-OAEP decapsulation failed") from exc
76
+
77
+ def generate_signing_keypair(self) -> SigningKeyPair:
78
+ pair = self.generate_keypair()
79
+ return SigningKeyPair(
80
+ public=SigningPublicKey(pair.public.data, self.name),
81
+ private=SigningPrivateKey(pair.private.data, self.name),
82
+ )
83
+
84
+ def sign(
85
+ self,
86
+ private: SigningPrivateKey,
87
+ message: bytes,
88
+ digest: type[HashAlgorithm],
89
+ ) -> bytes:
90
+ key = self.decode_private(private.data)
91
+ algorithm = digest.hazmat()
92
+ hasher = hashes.Hash(algorithm)
93
+ hasher.update(message)
94
+ return key.sign(
95
+ hasher.finalize(),
96
+ padding.PSS(
97
+ mgf=padding.MGF1(algorithm),
98
+ salt_length=padding.PSS.MAX_LENGTH,
99
+ ),
100
+ utils.Prehashed(algorithm),
101
+ )
102
+
103
+ def verify(
104
+ self,
105
+ public: SigningPublicKey,
106
+ message: bytes,
107
+ signature: bytes,
108
+ digest: type[HashAlgorithm],
109
+ ) -> None:
110
+ key = self.decode_public(public.data)
111
+ algorithm = digest.hazmat()
112
+ hasher = hashes.Hash(algorithm)
113
+ hasher.update(message)
114
+ try:
115
+ key.verify(
116
+ signature,
117
+ hasher.finalize(),
118
+ padding.PSS(
119
+ mgf=padding.MGF1(algorithm),
120
+ salt_length=padding.PSS.MAX_LENGTH,
121
+ ),
122
+ utils.Prehashed(algorithm),
123
+ )
124
+ except Exception as exc:
125
+ raise InvalidSignature("RSA-PSS verification failed") from exc
126
+
127
+ @staticmethod
128
+ def encode_public(key: rsa.RSAPublicKey) -> bytes:
129
+ return key.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
130
+
131
+ @staticmethod
132
+ def encode_private(key: rsa.RSAPrivateKey) -> bytes:
133
+ return key.private_bytes(
134
+ Encoding.DER,
135
+ PrivateFormat.PKCS8,
136
+ NoEncryption(),
137
+ )
138
+
139
+ @staticmethod
140
+ def decode_public(data: bytes) -> rsa.RSAPublicKey:
141
+ key = serialization.load_der_public_key(data)
142
+ if not isinstance(key, rsa.RSAPublicKey):
143
+ raise InvalidKey("expected RSA public key")
144
+ return key
145
+
146
+ @staticmethod
147
+ def decode_private(data: bytes) -> rsa.RSAPrivateKey:
148
+ key = serialization.load_der_private_key(data, password=None)
149
+ if not isinstance(key, rsa.RSAPrivateKey):
150
+ raise InvalidKey("expected RSA private key")
151
+ return key
@@ -0,0 +1,12 @@
1
+ """SECP256R1 ECDH-as-KEM + ECDSA signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cryptography.hazmat.primitives.asymmetric import ec
6
+
7
+ from ratchetkit.algorithms.kem.ec_base import EcKem
8
+
9
+
10
+ class Secp256r1(EcKem):
11
+ name = "secp256r1"
12
+ curve = ec.SECP256R1()
@@ -0,0 +1,12 @@
1
+ """SECP384R1 ECDH-as-KEM + ECDSA signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cryptography.hazmat.primitives.asymmetric import ec
6
+
7
+ from ratchetkit.algorithms.kem.ec_base import EcKem
8
+
9
+
10
+ class Secp384r1(EcKem):
11
+ name = "secp384r1"
12
+ curve = ec.SECP384R1()
@@ -0,0 +1,12 @@
1
+ """SECP521R1 ECDH-as-KEM + ECDSA signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cryptography.hazmat.primitives.asymmetric import ec
6
+
7
+ from ratchetkit.algorithms.kem.ec_base import EcKem
8
+
9
+
10
+ class Secp521r1(EcKem):
11
+ name = "secp521r1"
12
+ curve = ec.SECP521R1()
@@ -0,0 +1,107 @@
1
+ """X25519 DH-as-KEM + Ed25519 signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from cryptography.hazmat.primitives.asymmetric import ed25519, x25519
8
+ from cryptography.hazmat.primitives.serialization import (
9
+ Encoding,
10
+ NoEncryption,
11
+ PrivateFormat,
12
+ PublicFormat,
13
+ )
14
+
15
+ from ratchetkit.errors import InvalidKey, InvalidSignature
16
+ from ratchetkit.algorithms.kem.base import KemBackend
17
+ from ratchetkit.keys.keypair import (
18
+ KeyPair,
19
+ PrivateKey,
20
+ PublicKey,
21
+ SigningKeyPair,
22
+ SigningPrivateKey,
23
+ SigningPublicKey,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from ratchetkit.algorithms.hash.base import HashAlgorithm
28
+
29
+
30
+ class X25519(KemBackend):
31
+ name = "x25519"
32
+ supports_dh = True
33
+
34
+ def generate_keypair(self) -> KeyPair:
35
+ private = x25519.X25519PrivateKey.generate()
36
+ return KeyPair(
37
+ public=PublicKey(
38
+ private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw),
39
+ self.name,
40
+ ),
41
+ private=PrivateKey(
42
+ private.private_bytes(
43
+ Encoding.Raw,
44
+ PrivateFormat.Raw,
45
+ NoEncryption(),
46
+ ),
47
+ self.name,
48
+ ),
49
+ )
50
+
51
+ def encapsulate(self, their_public: PublicKey) -> tuple[bytes, bytes]:
52
+ ephemeral = self.generate_keypair()
53
+ secret = self.dh(ephemeral.private, their_public)
54
+ return secret, ephemeral.public.data
55
+
56
+ def decapsulate(self, our_private: PrivateKey, ciphertext: bytes) -> bytes:
57
+ return self.dh(our_private, PublicKey(ciphertext, self.name))
58
+
59
+ def dh(self, our_private: PrivateKey, their_public: PublicKey) -> bytes:
60
+ try:
61
+ private = x25519.X25519PrivateKey.from_private_bytes(our_private.data)
62
+ public = x25519.X25519PublicKey.from_public_bytes(their_public.data)
63
+ shared = private.exchange(public)
64
+ except Exception as exc:
65
+ raise InvalidKey("X25519 DH failed (invalid or low-order public key)") from exc
66
+ if not any(shared):
67
+ raise InvalidKey("X25519 DH produced an all-zero shared secret")
68
+ return shared
69
+
70
+ def generate_signing_keypair(self) -> SigningKeyPair:
71
+ private = ed25519.Ed25519PrivateKey.generate()
72
+ return SigningKeyPair(
73
+ public=SigningPublicKey(
74
+ private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw),
75
+ self.name,
76
+ ),
77
+ private=SigningPrivateKey(
78
+ private.private_bytes(
79
+ Encoding.Raw,
80
+ PrivateFormat.Raw,
81
+ NoEncryption(),
82
+ ),
83
+ self.name,
84
+ ),
85
+ )
86
+
87
+ def sign(
88
+ self,
89
+ private: SigningPrivateKey,
90
+ message: bytes,
91
+ digest: type[HashAlgorithm],
92
+ ) -> bytes:
93
+ key = ed25519.Ed25519PrivateKey.from_private_bytes(private.data)
94
+ return key.sign(message)
95
+
96
+ def verify(
97
+ self,
98
+ public: SigningPublicKey,
99
+ message: bytes,
100
+ signature: bytes,
101
+ digest: type[HashAlgorithm],
102
+ ) -> None:
103
+ key = ed25519.Ed25519PublicKey.from_public_bytes(public.data)
104
+ try:
105
+ key.verify(signature, message)
106
+ except Exception as exc:
107
+ raise InvalidSignature("Ed25519 verification failed") from exc
@@ -0,0 +1,107 @@
1
+ """X448 DH-as-KEM + Ed448 signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from cryptography.hazmat.primitives.asymmetric import ed448, x448
8
+ from cryptography.hazmat.primitives.serialization import (
9
+ Encoding,
10
+ NoEncryption,
11
+ PrivateFormat,
12
+ PublicFormat,
13
+ )
14
+
15
+ from ratchetkit.errors import InvalidKey, InvalidSignature
16
+ from ratchetkit.algorithms.kem.base import KemBackend
17
+ from ratchetkit.keys.keypair import (
18
+ KeyPair,
19
+ PrivateKey,
20
+ PublicKey,
21
+ SigningKeyPair,
22
+ SigningPrivateKey,
23
+ SigningPublicKey,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from ratchetkit.algorithms.hash.base import HashAlgorithm
28
+
29
+
30
+ class X448(KemBackend):
31
+ name = "x448"
32
+ supports_dh = True
33
+
34
+ def generate_keypair(self) -> KeyPair:
35
+ private = x448.X448PrivateKey.generate()
36
+ return KeyPair(
37
+ public=PublicKey(
38
+ private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw),
39
+ self.name,
40
+ ),
41
+ private=PrivateKey(
42
+ private.private_bytes(
43
+ Encoding.Raw,
44
+ PrivateFormat.Raw,
45
+ NoEncryption(),
46
+ ),
47
+ self.name,
48
+ ),
49
+ )
50
+
51
+ def encapsulate(self, their_public: PublicKey) -> tuple[bytes, bytes]:
52
+ ephemeral = self.generate_keypair()
53
+ secret = self.dh(ephemeral.private, their_public)
54
+ return secret, ephemeral.public.data
55
+
56
+ def decapsulate(self, our_private: PrivateKey, ciphertext: bytes) -> bytes:
57
+ return self.dh(our_private, PublicKey(ciphertext, self.name))
58
+
59
+ def dh(self, our_private: PrivateKey, their_public: PublicKey) -> bytes:
60
+ try:
61
+ private = x448.X448PrivateKey.from_private_bytes(our_private.data)
62
+ public = x448.X448PublicKey.from_public_bytes(their_public.data)
63
+ shared = private.exchange(public)
64
+ except Exception as exc:
65
+ raise InvalidKey("X448 DH failed (invalid or low-order public key)") from exc
66
+ if not any(shared):
67
+ raise InvalidKey("X448 DH produced an all-zero shared secret")
68
+ return shared
69
+
70
+ def generate_signing_keypair(self) -> SigningKeyPair:
71
+ private = ed448.Ed448PrivateKey.generate()
72
+ return SigningKeyPair(
73
+ public=SigningPublicKey(
74
+ private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw),
75
+ self.name,
76
+ ),
77
+ private=SigningPrivateKey(
78
+ private.private_bytes(
79
+ Encoding.Raw,
80
+ PrivateFormat.Raw,
81
+ NoEncryption(),
82
+ ),
83
+ self.name,
84
+ ),
85
+ )
86
+
87
+ def sign(
88
+ self,
89
+ private: SigningPrivateKey,
90
+ message: bytes,
91
+ digest: type[HashAlgorithm],
92
+ ) -> bytes:
93
+ key = ed448.Ed448PrivateKey.from_private_bytes(private.data)
94
+ return key.sign(message)
95
+
96
+ def verify(
97
+ self,
98
+ public: SigningPublicKey,
99
+ message: bytes,
100
+ signature: bytes,
101
+ digest: type[HashAlgorithm],
102
+ ) -> None:
103
+ key = ed448.Ed448PublicKey.from_public_bytes(public.data)
104
+ try:
105
+ key.verify(signature, message)
106
+ except Exception as exc:
107
+ raise InvalidSignature("Ed448 verification failed") from exc
ratchetkit/errors.py ADDED
@@ -0,0 +1,61 @@
1
+ """Protocol and cryptographic error types."""
2
+
3
+
4
+ class RatchetError(Exception):
5
+ """Base error for ratchetkit."""
6
+
7
+
8
+ class InvalidMessage(RatchetError):
9
+ """Ciphertext is malformed, truncated, or fails authentication."""
10
+
11
+
12
+ class DuplicateMessage(RatchetError):
13
+ """Message key was already consumed (replay or already-delivered)."""
14
+
15
+ def __init__(self, chain_index: int, counter: int):
16
+ self.chain_index = chain_index
17
+ self.counter = counter
18
+ super().__init__(
19
+ f"duplicate message: chain_index={chain_index}, counter={counter}"
20
+ )
21
+
22
+
23
+ class TooFarInFuture(RatchetError):
24
+ """Message counter exceeds the allowed forward-jump limit."""
25
+
26
+ def __init__(self, chain_index: int, counter: int, limit: int):
27
+ self.chain_index = chain_index
28
+ self.counter = counter
29
+ self.limit = limit
30
+ super().__init__(
31
+ f"message too far ahead: index={chain_index}, "
32
+ f"counter={counter}, limit={limit}"
33
+ )
34
+
35
+
36
+ class TooManySkippedKeys(RatchetError):
37
+ """Skipped-message cache would exceed its hard size cap.
38
+
39
+ Raising (instead of silently dropping keys) avoids permanent loss of
40
+ delayed messages and matches Double Ratchet §3.1 / §8.4 guidance that
41
+ MKSKIPPED must bound stored elements.
42
+ """
43
+
44
+ def __init__(self, size: int, limit: int):
45
+ self.size = size
46
+ self.limit = limit
47
+ super().__init__(
48
+ f"too many skipped message keys: size={size}, limit={limit}"
49
+ )
50
+
51
+
52
+ class InvalidKey(RatchetError):
53
+ """Key material is missing, wrong length, or fails verification."""
54
+
55
+
56
+ class InvalidSignature(RatchetError):
57
+ """Signed prekey or initiator signature failed verification."""
58
+
59
+
60
+ class SerializationError(RatchetError):
61
+ """Blob is truncated, version-mismatched, or otherwise unreadable."""
@@ -0,0 +1,15 @@
1
+ """X3DH and PQXDH key agreement."""
2
+
3
+ from ratchetkit.handshake.message import InitialMessage
4
+ from ratchetkit.handshake.pqxdh import PostQuantumExtendedDiffieHellman
5
+ from ratchetkit.handshake.x3dh import (
6
+ ExtendedTripleDiffieHellman,
7
+ ExtendedTripleDiffieHellmanResult,
8
+ )
9
+
10
+ __all__ = [
11
+ "InitialMessage",
12
+ "ExtendedTripleDiffieHellman",
13
+ "ExtendedTripleDiffieHellmanResult",
14
+ "PostQuantumExtendedDiffieHellman",
15
+ ]
@@ -0,0 +1,177 @@
1
+ """Initial handshake message carried from initiator to responder (X3DH / PQXDH)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+ from dataclasses import dataclass
7
+
8
+ from ratchetkit.errors import SerializationError
9
+ from ratchetkit.keys.keypair import PublicKey, SigningPublicKey
10
+ from ratchetkit.packing import (
11
+ ConstructorId,
12
+ expect_constructor,
13
+ pack_constructor,
14
+ )
15
+
16
+
17
+ @dataclass
18
+ class InitialMessage:
19
+ """First message of an X3DH / PQXDH handshake (public half only)."""
20
+
21
+ identity_signing: SigningPublicKey
22
+ identity_agreement: PublicKey | None
23
+ ephemeral_public: PublicKey
24
+ # For KEM-only suites: encapsulations to SPK / OPK (optional trailing).
25
+ kem_ciphertexts: tuple[bytes, ...]
26
+ signed_prekey_id: int
27
+ one_time_prekey_id: int | None
28
+ signature: bytes
29
+ # Optional first ratchet public used to seed the Double Ratchet.
30
+ ratchet_public: bytes | None = None
31
+ # PQXDH: encapsulation to the peer's signed PQ prekey.
32
+ pq_prekey_id: int | None = None
33
+ pq_ciphertext: bytes | None = None
34
+
35
+ CONSTRUCTOR_ID = ConstructorId.INITIAL_MESSAGE
36
+
37
+ def to_bytes(self) -> bytes:
38
+ parts = [
39
+ pack_constructor(self.CONSTRUCTOR_ID),
40
+ self.encode_field(self.identity_signing.data),
41
+ self.encode_field(self.identity_signing.algorithm.encode("ascii")),
42
+ ]
43
+ if self.identity_agreement is not None:
44
+ parts.extend(
45
+ [
46
+ b"\x01",
47
+ self.encode_field(self.identity_agreement.data),
48
+ self.encode_field(
49
+ self.identity_agreement.algorithm.encode("ascii")
50
+ ),
51
+ ]
52
+ )
53
+ else:
54
+ parts.append(b"\x00")
55
+ parts.extend(
56
+ [
57
+ self.encode_field(self.ephemeral_public.data),
58
+ self.encode_field(self.ephemeral_public.algorithm.encode("ascii")),
59
+ bytes([len(self.kem_ciphertexts)]),
60
+ ]
61
+ )
62
+ for ct in self.kem_ciphertexts:
63
+ parts.append(self.encode_field(ct))
64
+ parts.append(struct.pack(">I", self.signed_prekey_id))
65
+ if self.one_time_prekey_id is None:
66
+ parts.append(b"\x00")
67
+ else:
68
+ parts.extend([b"\x01", struct.pack(">I", self.one_time_prekey_id)])
69
+ parts.append(self.encode_field(self.signature))
70
+ if self.ratchet_public is None:
71
+ parts.append(b"\x00")
72
+ else:
73
+ parts.extend([b"\x01", self.encode_field(self.ratchet_public)])
74
+ if self.pq_ciphertext is None:
75
+ parts.append(b"\x00")
76
+ else:
77
+ parts.extend(
78
+ [
79
+ b"\x01",
80
+ struct.pack(">I", self.pq_prekey_id or 0),
81
+ self.encode_field(self.pq_ciphertext),
82
+ ]
83
+ )
84
+ return b"".join(parts)
85
+
86
+ @classmethod
87
+ def from_bytes(cls, data: bytes) -> InitialMessage:
88
+ view = memoryview(data)
89
+ if not view:
90
+ raise SerializationError("empty initial message")
91
+ offset = expect_constructor(view, cls.CONSTRUCTOR_ID)
92
+
93
+ identity_data, offset = cls.read_field(view, offset)
94
+ identity_alg, offset = cls.read_field(view, offset)
95
+ identity_alg = identity_alg.decode("ascii")
96
+ has_agreement = view[offset]
97
+ offset += 1
98
+ identity_agreement = None
99
+ if has_agreement:
100
+ agreement_data, offset = cls.read_field(view, offset)
101
+ agreement_alg, offset = cls.read_field(view, offset)
102
+ identity_agreement = PublicKey(
103
+ agreement_data,
104
+ agreement_alg.decode("ascii"),
105
+ )
106
+ ephemeral_data, offset = cls.read_field(view, offset)
107
+ ephemeral_alg, offset = cls.read_field(view, offset)
108
+ ephemeral_alg = ephemeral_alg.decode("ascii")
109
+ ct_count = view[offset]
110
+ offset += 1
111
+ ciphertexts = []
112
+ for index in range(ct_count):
113
+ ct, offset = cls.read_field(view, offset)
114
+ ciphertexts.append(ct)
115
+ if offset + 4 > len(view):
116
+ raise SerializationError("truncated signed prekey id")
117
+ signed_prekey_id = struct.unpack_from(">I", view, offset)[0]
118
+ offset += 4
119
+ has_opk = view[offset]
120
+ offset += 1
121
+ one_time_prekey_id = None
122
+ if has_opk:
123
+ if offset + 4 > len(view):
124
+ raise SerializationError("truncated one-time prekey id")
125
+ one_time_prekey_id = struct.unpack_from(">I", view, offset)[0]
126
+ offset += 4
127
+ signature, offset = cls.read_field(view, offset)
128
+ if offset >= len(view):
129
+ raise SerializationError("truncated initial message ratchet flag")
130
+ has_ratchet = view[offset]
131
+ offset += 1
132
+ ratchet_public = None
133
+ if has_ratchet:
134
+ ratchet_public, offset = cls.read_field(view, offset)
135
+ pq_prekey_id = None
136
+ pq_ciphertext = None
137
+ # Older initial messages omit the PQ flag entirely.
138
+ if offset < len(view):
139
+ has_pq = view[offset]
140
+ offset += 1
141
+ if has_pq:
142
+ if offset + 4 > len(view):
143
+ raise SerializationError("truncated PQ prekey id")
144
+ pq_prekey_id = struct.unpack_from(">I", view, offset)[0]
145
+ offset += 4
146
+ pq_ciphertext, offset = cls.read_field(view, offset)
147
+ if offset != len(view):
148
+ raise SerializationError("trailing bytes in initial message")
149
+ return cls(
150
+ identity_signing=SigningPublicKey(identity_data, identity_alg),
151
+ identity_agreement=identity_agreement,
152
+ ephemeral_public=PublicKey(ephemeral_data, ephemeral_alg),
153
+ kem_ciphertexts=tuple(ciphertexts),
154
+ signed_prekey_id=signed_prekey_id,
155
+ one_time_prekey_id=one_time_prekey_id,
156
+ signature=signature,
157
+ ratchet_public=ratchet_public,
158
+ pq_prekey_id=pq_prekey_id,
159
+ pq_ciphertext=pq_ciphertext,
160
+ )
161
+
162
+ @staticmethod
163
+ def encode_field(value: bytes) -> bytes:
164
+ if len(value) > 0xFFFF:
165
+ raise SerializationError("field too large")
166
+ return struct.pack(">H", len(value)) + value
167
+
168
+ @staticmethod
169
+ def read_field(view: memoryview, offset: int) -> tuple[bytes, int]:
170
+ if offset + 2 > len(view):
171
+ raise SerializationError("truncated initial message field")
172
+ length = struct.unpack_from(">H", view, offset)[0]
173
+ offset += 2
174
+ if offset + length > len(view):
175
+ raise SerializationError("truncated initial message field body")
176
+ value = bytes(view[offset:offset + length])
177
+ return value, offset + length