cairn-p2p 0.2.0__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 (62) hide show
  1. cairn_p2p-0.2.0/.gitignore +49 -0
  2. cairn_p2p-0.2.0/PKG-INFO +24 -0
  3. cairn_p2p-0.2.0/README.md +52 -0
  4. cairn_p2p-0.2.0/pyproject.toml +40 -0
  5. cairn_p2p-0.2.0/src/cairn/__init__.py +55 -0
  6. cairn_p2p-0.2.0/src/cairn/channel.py +179 -0
  7. cairn_p2p-0.2.0/src/cairn/config.py +175 -0
  8. cairn_p2p-0.2.0/src/cairn/crypto/__init__.py +79 -0
  9. cairn_p2p-0.2.0/src/cairn/crypto/aead.py +58 -0
  10. cairn_p2p-0.2.0/src/cairn/crypto/identity.py +185 -0
  11. cairn_p2p-0.2.0/src/cairn/crypto/kdf.py +42 -0
  12. cairn_p2p-0.2.0/src/cairn/crypto/noise.py +445 -0
  13. cairn_p2p-0.2.0/src/cairn/crypto/ratchet.py +311 -0
  14. cairn_p2p-0.2.0/src/cairn/crypto/spake2_pake.py +32 -0
  15. cairn_p2p-0.2.0/src/cairn/crypto/storage.py +206 -0
  16. cairn_p2p-0.2.0/src/cairn/discovery/__init__.py +39 -0
  17. cairn_p2p-0.2.0/src/cairn/discovery/mdns.py +660 -0
  18. cairn_p2p-0.2.0/src/cairn/discovery/rendezvous.py +158 -0
  19. cairn_p2p-0.2.0/src/cairn/errors.py +167 -0
  20. cairn_p2p-0.2.0/src/cairn/mesh/__init__.py +32 -0
  21. cairn_p2p-0.2.0/src/cairn/mesh/relay.py +76 -0
  22. cairn_p2p-0.2.0/src/cairn/mesh/router.py +207 -0
  23. cairn_p2p-0.2.0/src/cairn/node.py +498 -0
  24. cairn_p2p-0.2.0/src/cairn/pairing/__init__.py +57 -0
  25. cairn_p2p-0.2.0/src/cairn/pairing/adapter.py +46 -0
  26. cairn_p2p-0.2.0/src/cairn/pairing/link.py +114 -0
  27. cairn_p2p-0.2.0/src/cairn/pairing/payload.py +83 -0
  28. cairn_p2p-0.2.0/src/cairn/pairing/pin.py +84 -0
  29. cairn_p2p-0.2.0/src/cairn/pairing/qr.py +74 -0
  30. cairn_p2p-0.2.0/src/cairn/pairing/rate_limit.py +132 -0
  31. cairn_p2p-0.2.0/src/cairn/pairing/sas.py +44 -0
  32. cairn_p2p-0.2.0/src/cairn/protocol/__init__.py +96 -0
  33. cairn_p2p-0.2.0/src/cairn/protocol/envelope.py +120 -0
  34. cairn_p2p-0.2.0/src/cairn/protocol/types.py +75 -0
  35. cairn_p2p-0.2.0/src/cairn/protocol/version.py +101 -0
  36. cairn_p2p-0.2.0/src/cairn/server/__init__.py +55 -0
  37. cairn_p2p-0.2.0/src/cairn/server/forward.py +317 -0
  38. cairn_p2p-0.2.0/src/cairn/server/management.py +562 -0
  39. cairn_p2p-0.2.0/src/cairn/session.py +211 -0
  40. cairn_p2p-0.2.0/src/cairn/transport/__init__.py +55 -0
  41. cairn_p2p-0.2.0/src/cairn/transport/chain.py +316 -0
  42. cairn_p2p-0.2.0/src/cairn/transport/heartbeat.py +293 -0
  43. cairn_p2p-0.2.0/src/cairn/transport/nat.py +282 -0
  44. cairn_p2p-0.2.0/src/cairn/transport/tcp.py +70 -0
  45. cairn_p2p-0.2.0/tests/__init__.py +0 -0
  46. cairn_p2p-0.2.0/tests/test_cbor_vectors.py +161 -0
  47. cairn_p2p-0.2.0/tests/test_config_api.py +528 -0
  48. cairn_p2p-0.2.0/tests/test_crypto_primitives.py +312 -0
  49. cairn_p2p-0.2.0/tests/test_crypto_vectors.py +331 -0
  50. cairn_p2p-0.2.0/tests/test_discovery.py +424 -0
  51. cairn_p2p-0.2.0/tests/test_double_ratchet.py +154 -0
  52. cairn_p2p-0.2.0/tests/test_integration.py +430 -0
  53. cairn_p2p-0.2.0/tests/test_key_storage.py +193 -0
  54. cairn_p2p-0.2.0/tests/test_mesh.py +271 -0
  55. cairn_p2p-0.2.0/tests/test_noise_spake2.py +269 -0
  56. cairn_p2p-0.2.0/tests/test_pairing.py +255 -0
  57. cairn_p2p-0.2.0/tests/test_pairing_extras.py +322 -0
  58. cairn_p2p-0.2.0/tests/test_reconnection.py +332 -0
  59. cairn_p2p-0.2.0/tests/test_server.py +920 -0
  60. cairn_p2p-0.2.0/tests/test_sessions.py +466 -0
  61. cairn_p2p-0.2.0/tests/test_transport.py +399 -0
  62. cairn_p2p-0.2.0/tests/test_wire_protocol.py +302 -0
@@ -0,0 +1,49 @@
1
+ target/
2
+
3
+ # Dependencies
4
+ node_modules/
5
+ vendor/
6
+ .venv/
7
+ composer.lock
8
+
9
+ # Build artifacts
10
+ dist/
11
+
12
+ # Caches
13
+ __pycache__/
14
+ .phpunit.cache/
15
+ *.pyc
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+ *.swo
22
+ *~
23
+
24
+ # OS
25
+ .DS_Store
26
+ Thumbs.db
27
+
28
+ # Coverage reports
29
+ coverage/
30
+ htmlcov/
31
+ *.lcov
32
+ .nyc_output/
33
+
34
+ # PHP
35
+ .phpunit.result.cache
36
+ composer.phar
37
+
38
+ # Python
39
+ *.egg-info/
40
+ *.egg
41
+ .eggs/
42
+
43
+ # Go
44
+ *.test
45
+ cairn-conformance-runner-go
46
+
47
+ # Environment / secrets
48
+ .env
49
+ .env.*
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: cairn-p2p
3
+ Version: 0.2.0
4
+ Summary: P2P connectivity library
5
+ Project-URL: Repository, https://github.com/moukrea/cairn
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: base58>=2.1
9
+ Requires-Dist: cbor2>=5.6
10
+ Requires-Dist: cryptography>=42.0
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: pyyaml>=6.0
13
+ Requires-Dist: qrcode>=7.4
14
+ Requires-Dist: spake2>=0.9
15
+ Requires-Dist: websockets>=12.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
18
+ Requires-Dist: pytest>=8.0; extra == 'dev'
19
+ Requires-Dist: ruff>=0.3; extra == 'dev'
20
+ Provides-Extra: discovery
21
+ Requires-Dist: kademlia>=2.2; extra == 'discovery'
22
+ Requires-Dist: zeroconf>=0.131; extra == 'discovery'
23
+ Provides-Extra: libp2p
24
+ Requires-Dist: libp2p; extra == 'libp2p'
@@ -0,0 +1,52 @@
1
+ # cairn-py
2
+
3
+ Python implementation of the cairn P2P connectivity library.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install cairn
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - Python 3.11+
14
+
15
+ ## Quick Start
16
+
17
+ ```python
18
+ import asyncio
19
+ from cairn import CairnNode
20
+
21
+ async def main():
22
+ node = await CairnNode.create()
23
+ peer = await node.pair_with_pin("123456")
24
+ await peer.send(b"hello")
25
+
26
+ asyncio.run(main())
27
+ ```
28
+
29
+ ## API Overview
30
+
31
+ - `CairnNode` -- Main entry point, manages identity, sessions, and discovery
32
+ - `Session` -- Persistent encrypted session with a peer
33
+ - `PeerIdentity` -- Ed25519 identity with Peer ID derivation
34
+ - `CairnConfig` -- Configuration with tier presets
35
+
36
+ ## Key Dependencies
37
+
38
+ - `cryptography` -- AEAD encryption, key derivation
39
+ - `spake2` -- Password-authenticated key exchange for pairing
40
+ - `cbor2` -- CBOR wire protocol encoding
41
+ - `websockets` -- WebSocket transport
42
+ - `httpx` -- HTTP client for signaling
43
+
44
+ ## Optional Dependencies
45
+
46
+ - `libp2p` -- libp2p transport integration
47
+ - `zeroconf` -- mDNS LAN discovery
48
+ - `kademlia` -- DHT discovery
49
+
50
+ ## License
51
+
52
+ Licensed under the [MIT License](../../LICENSE).
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cairn-p2p"
7
+ version = "0.2.0"
8
+ description = "P2P connectivity library"
9
+ license = "MIT"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "base58>=2.1",
13
+ "cbor2>=5.6",
14
+ "cryptography>=42.0",
15
+ "spake2>=0.9",
16
+ "qrcode>=7.4",
17
+ "websockets>=12.0",
18
+ "httpx>=0.27",
19
+ "pyyaml>=6.0",
20
+ ]
21
+
22
+ [project.urls]
23
+ Repository = "https://github.com/moukrea/cairn"
24
+
25
+ [project.optional-dependencies]
26
+ libp2p = ["libp2p"]
27
+ discovery = ["zeroconf>=0.131", "kademlia>=2.2"]
28
+ dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "ruff>=0.3"]
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/cairn"]
32
+
33
+ [tool.pytest.ini_options]
34
+ asyncio_mode = "auto"
35
+
36
+ [tool.ruff]
37
+ target-version = "py311"
38
+
39
+ [tool.ruff.lint]
40
+ select = ["E", "F", "W", "I"]
@@ -0,0 +1,55 @@
1
+ """cairn - P2P connectivity library."""
2
+
3
+ from cairn.config import (
4
+ CairnConfig,
5
+ MeshSettings,
6
+ ReconnectionPolicy,
7
+ TurnServer,
8
+ )
9
+ from cairn.errors import (
10
+ AuthenticationFailedError,
11
+ CairnError,
12
+ ErrorBehavior,
13
+ MeshRouteNotFoundError,
14
+ PairingExpiredError,
15
+ PairingRejectedError,
16
+ PeerUnreachableError,
17
+ SessionExpiredError,
18
+ TransportExhaustedError,
19
+ VersionMismatchError,
20
+ )
21
+ from cairn.node import (
22
+ Channel,
23
+ NetworkInfo,
24
+ Node,
25
+ NodeEvent,
26
+ NodeEventType,
27
+ Session,
28
+ create,
29
+ create_server,
30
+ )
31
+
32
+ __all__ = [
33
+ "AuthenticationFailedError",
34
+ "CairnConfig",
35
+ "CairnError",
36
+ "Channel",
37
+ "ErrorBehavior",
38
+ "MeshRouteNotFoundError",
39
+ "MeshSettings",
40
+ "NetworkInfo",
41
+ "Node",
42
+ "NodeEvent",
43
+ "NodeEventType",
44
+ "PairingExpiredError",
45
+ "PairingRejectedError",
46
+ "PeerUnreachableError",
47
+ "ReconnectionPolicy",
48
+ "Session",
49
+ "SessionExpiredError",
50
+ "TransportExhaustedError",
51
+ "TurnServer",
52
+ "VersionMismatchError",
53
+ "create",
54
+ "create_server",
55
+ ]
@@ -0,0 +1,179 @@
1
+ """Channel multiplexing over sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum, auto
7
+
8
+ import cbor2
9
+
10
+ RESERVED_CHANNEL_PREFIX: str = "__cairn_"
11
+ CHANNEL_FORWARD: str = "__cairn_forward"
12
+ CHANNEL_INIT_TYPE: int = 0x0303
13
+
14
+
15
+ def validate_channel_name(name: str) -> None:
16
+ """Validate that a channel name is not reserved.
17
+
18
+ Raises ValueError if the name is empty or uses the reserved prefix.
19
+ """
20
+ if not name:
21
+ raise ValueError("channel name must not be empty")
22
+ if name.startswith(RESERVED_CHANNEL_PREFIX):
23
+ raise ValueError(
24
+ f"channel name '{name}' uses reserved prefix "
25
+ f"'{RESERVED_CHANNEL_PREFIX}'"
26
+ )
27
+
28
+
29
+ class ChannelState(Enum):
30
+ """Channel lifecycle states."""
31
+
32
+ OPENING = auto()
33
+ OPEN = auto()
34
+ REJECTED = auto()
35
+ CLOSED = auto()
36
+
37
+
38
+ class Channel:
39
+ """A named channel multiplexed over a session stream."""
40
+
41
+ def __init__(
42
+ self,
43
+ name: str,
44
+ stream_id: int,
45
+ metadata: bytes | None = None,
46
+ ) -> None:
47
+ self.name = name
48
+ self.stream_id = stream_id
49
+ self.state = ChannelState.OPENING
50
+ self.metadata = metadata
51
+
52
+ @property
53
+ def is_open(self) -> bool:
54
+ return self.state == ChannelState.OPEN
55
+
56
+ def accept(self) -> None:
57
+ """Transition to Open state."""
58
+ if self.state != ChannelState.OPENING:
59
+ raise ValueError(
60
+ f"cannot accept channel '{self.name}' "
61
+ f"in state {self.state.name}"
62
+ )
63
+ self.state = ChannelState.OPEN
64
+
65
+ def reject(self) -> None:
66
+ """Transition to Rejected state."""
67
+ if self.state != ChannelState.OPENING:
68
+ raise ValueError(
69
+ f"cannot reject channel '{self.name}' "
70
+ f"in state {self.state.name}"
71
+ )
72
+ self.state = ChannelState.REJECTED
73
+
74
+ def close(self) -> None:
75
+ """Transition to Closed state."""
76
+ if self.state == ChannelState.CLOSED:
77
+ raise ValueError(
78
+ f"channel '{self.name}' is already closed"
79
+ )
80
+ self.state = ChannelState.CLOSED
81
+
82
+
83
+ @dataclass
84
+ class ChannelInit:
85
+ """First message sent on a newly opened stream."""
86
+
87
+ channel_name: str
88
+ metadata: bytes | None = None
89
+
90
+ def to_cbor(self) -> bytes:
91
+ """Encode to CBOR."""
92
+ m: dict[str, object] = {
93
+ "channel_name": self.channel_name
94
+ }
95
+ if self.metadata is not None:
96
+ m["metadata"] = self.metadata
97
+ return cbor2.dumps(m)
98
+
99
+ @classmethod
100
+ def from_cbor(cls, data: bytes) -> ChannelInit:
101
+ """Decode from CBOR."""
102
+ m = cbor2.loads(data)
103
+ return cls(
104
+ channel_name=m["channel_name"],
105
+ metadata=m.get("metadata"),
106
+ )
107
+
108
+
109
+ class ChannelManager:
110
+ """Manages channels within a session."""
111
+
112
+ def __init__(self) -> None:
113
+ self._channels: dict[int, Channel] = {}
114
+
115
+ def open_channel(
116
+ self,
117
+ name: str,
118
+ stream_id: int,
119
+ metadata: bytes | None = None,
120
+ ) -> ChannelInit:
121
+ """Open a new channel on a given stream.
122
+
123
+ Returns the ChannelInit payload to send.
124
+ """
125
+ validate_channel_name(name)
126
+ if stream_id in self._channels:
127
+ raise ValueError(
128
+ f"stream {stream_id} already has a channel"
129
+ )
130
+
131
+ channel = Channel(name, stream_id, metadata)
132
+ self._channels[stream_id] = channel
133
+ return ChannelInit(
134
+ channel_name=name, metadata=metadata
135
+ )
136
+
137
+ def handle_channel_init(
138
+ self, stream_id: int, init: ChannelInit
139
+ ) -> None:
140
+ """Handle an incoming ChannelInit from a remote peer."""
141
+ if stream_id in self._channels:
142
+ raise ValueError(
143
+ f"stream {stream_id} already has a channel"
144
+ )
145
+ channel = Channel(
146
+ init.channel_name, stream_id, init.metadata
147
+ )
148
+ self._channels[stream_id] = channel
149
+
150
+ def accept_channel(self, stream_id: int) -> None:
151
+ """Accept an incoming channel."""
152
+ channel = self._get_channel(stream_id)
153
+ channel.accept()
154
+
155
+ def reject_channel(self, stream_id: int) -> None:
156
+ """Reject an incoming channel."""
157
+ channel = self._get_channel(stream_id)
158
+ channel.reject()
159
+
160
+ def close_channel(self, stream_id: int) -> None:
161
+ """Close a channel."""
162
+ channel = self._get_channel(stream_id)
163
+ channel.close()
164
+
165
+ def get_channel(self, stream_id: int) -> Channel | None:
166
+ """Get a channel by stream ID."""
167
+ return self._channels.get(stream_id)
168
+
169
+ @property
170
+ def channel_count(self) -> int:
171
+ return len(self._channels)
172
+
173
+ def _get_channel(self, stream_id: int) -> Channel:
174
+ channel = self._channels.get(stream_id)
175
+ if channel is None:
176
+ raise ValueError(
177
+ f"no channel on stream {stream_id}"
178
+ )
179
+ return channel
@@ -0,0 +1,175 @@
1
+ """Configuration builder with tier presets (spec 11, section 1)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ # ---------------------------------------------------------------------------
8
+ # Default STUN servers
9
+ # ---------------------------------------------------------------------------
10
+
11
+ DEFAULT_STUN_SERVERS: list[str] = [
12
+ "stun:stun.l.google.com:19302",
13
+ "stun:stun1.l.google.com:19302",
14
+ "stun:stun.cloudflare.com:3478",
15
+ ]
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Supporting dataclasses
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ @dataclass
24
+ class TurnServer:
25
+ """TURN relay server credentials."""
26
+
27
+ url: str = ""
28
+ username: str = ""
29
+ credential: str = ""
30
+
31
+
32
+ @dataclass
33
+ class ReconnectionPolicy:
34
+ """Reconnection and timeout policy (spec section 2.2)."""
35
+
36
+ connect_timeout: float = 30.0
37
+ transport_timeout: float = 10.0
38
+ reconnect_max_duration: float = 3600.0
39
+ reconnect_backoff_initial: float = 1.0
40
+ reconnect_backoff_max: float = 60.0
41
+ reconnect_backoff_factor: float = 2.0
42
+ rendezvous_poll_interval: float = 30.0
43
+ session_expiry: float = 86400.0
44
+ pairing_payload_expiry: float = 300.0
45
+
46
+
47
+ @dataclass
48
+ class MeshSettings:
49
+ """Mesh routing settings (spec section 1.2)."""
50
+
51
+ mesh_enabled: bool = False
52
+ max_hops: int = 3
53
+ relay_willing: bool = False
54
+ relay_capacity: int = 10
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # CairnConfig
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ @dataclass
63
+ class CairnConfig:
64
+ """Top-level configuration (spec section 1.1).
65
+
66
+ Every field has a sensible default, enabling zero-config usage (Tier 0).
67
+ """
68
+
69
+ stun_servers: list[str] = field(
70
+ default_factory=lambda: list(DEFAULT_STUN_SERVERS)
71
+ )
72
+ turn_servers: list[TurnServer] = field(
73
+ default_factory=list
74
+ )
75
+ signaling_servers: list[str] = field(
76
+ default_factory=list
77
+ )
78
+ tracker_urls: list[str] = field(default_factory=list)
79
+ bootstrap_nodes: list[str] = field(default_factory=list)
80
+ reconnection_policy: ReconnectionPolicy = field(
81
+ default_factory=ReconnectionPolicy
82
+ )
83
+ mesh_settings: MeshSettings = field(
84
+ default_factory=MeshSettings
85
+ )
86
+ server_mode: bool = False
87
+
88
+ # --- Tier presets ---
89
+
90
+ @classmethod
91
+ def tier0(cls) -> CairnConfig:
92
+ """Tier 0: fully decentralized, zero-config."""
93
+ return cls()
94
+
95
+ @classmethod
96
+ def tier1(
97
+ cls,
98
+ signaling_servers: list[str] | None = None,
99
+ turn_servers: list[TurnServer] | None = None,
100
+ ) -> CairnConfig:
101
+ """Tier 1: add signaling server and optional TURN relay."""
102
+ return cls(
103
+ signaling_servers=signaling_servers or [],
104
+ turn_servers=turn_servers or [],
105
+ )
106
+
107
+ @classmethod
108
+ def tier2(
109
+ cls,
110
+ signaling_servers: list[str] | None = None,
111
+ turn_servers: list[TurnServer] | None = None,
112
+ tracker_urls: list[str] | None = None,
113
+ bootstrap_nodes: list[str] | None = None,
114
+ ) -> CairnConfig:
115
+ """Tier 2: self-hosted infrastructure."""
116
+ return cls(
117
+ signaling_servers=signaling_servers or [],
118
+ turn_servers=turn_servers or [],
119
+ tracker_urls=tracker_urls or [],
120
+ bootstrap_nodes=bootstrap_nodes or [],
121
+ )
122
+
123
+ @classmethod
124
+ def tier3(
125
+ cls,
126
+ signaling_servers: list[str] | None = None,
127
+ turn_servers: list[TurnServer] | None = None,
128
+ tracker_urls: list[str] | None = None,
129
+ bootstrap_nodes: list[str] | None = None,
130
+ mesh_settings: MeshSettings | None = None,
131
+ ) -> CairnConfig:
132
+ """Tier 3: fully self-hosted with mesh routing."""
133
+ return cls(
134
+ signaling_servers=signaling_servers or [],
135
+ turn_servers=turn_servers or [],
136
+ tracker_urls=tracker_urls or [],
137
+ bootstrap_nodes=bootstrap_nodes or [],
138
+ mesh_settings=mesh_settings or MeshSettings(
139
+ mesh_enabled=True
140
+ ),
141
+ )
142
+
143
+ @classmethod
144
+ def default_server(cls) -> CairnConfig:
145
+ """Default server-mode config."""
146
+ return cls(
147
+ server_mode=True,
148
+ reconnection_policy=ReconnectionPolicy(
149
+ session_expiry=7 * 86400.0,
150
+ ),
151
+ mesh_settings=MeshSettings(
152
+ relay_willing=True,
153
+ ),
154
+ )
155
+
156
+ def validate(self) -> None:
157
+ """Validate configuration. Raises ValueError on invalid settings."""
158
+ if not self.stun_servers and not self.turn_servers:
159
+ raise ValueError(
160
+ "stun_servers must not be empty unless "
161
+ "turn_servers are configured"
162
+ )
163
+
164
+ if self.reconnection_policy.reconnect_backoff_factor <= 1.0:
165
+ raise ValueError(
166
+ "reconnect_backoff_factor must be greater than 1.0"
167
+ )
168
+
169
+ if (
170
+ self.mesh_settings.max_hops < 1
171
+ or self.mesh_settings.max_hops > 10
172
+ ):
173
+ raise ValueError(
174
+ "max_hops must be between 1 and 10"
175
+ )
@@ -0,0 +1,79 @@
1
+ """Cryptographic primitives: identity, key exchange, AEAD, HKDF, Noise, SPAKE2."""
2
+
3
+ from cairn.crypto.aead import (
4
+ AES_GCM_TAG_SIZE,
5
+ CHACHA_TAG_SIZE,
6
+ KEY_SIZE,
7
+ NONCE_SIZE,
8
+ CipherSuite,
9
+ aead_decrypt,
10
+ aead_encrypt,
11
+ )
12
+ from cairn.crypto.identity import (
13
+ IdentityKeypair,
14
+ PeerId,
15
+ X25519Keypair,
16
+ peer_id_from_public_key,
17
+ verify_signature,
18
+ )
19
+ from cairn.crypto.kdf import (
20
+ HKDF_INFO_CHAIN_KEY,
21
+ HKDF_INFO_MESSAGE_KEY,
22
+ HKDF_INFO_RENDEZVOUS,
23
+ HKDF_INFO_SAS,
24
+ HKDF_INFO_SESSION_KEY,
25
+ hkdf_sha256,
26
+ )
27
+ from cairn.crypto.noise import (
28
+ EMOJI_TABLE,
29
+ HandshakeResult,
30
+ NoiseXXHandshake,
31
+ Role,
32
+ derive_emoji_sas,
33
+ derive_numeric_sas,
34
+ )
35
+ from cairn.crypto.ratchet import (
36
+ DoubleRatchet,
37
+ RatchetConfig,
38
+ RatchetHeader,
39
+ )
40
+ from cairn.crypto.spake2_pake import Spake2Session
41
+ from cairn.crypto.storage import (
42
+ FilesystemKeyStorage,
43
+ InMemoryKeyStorage,
44
+ KeyStorage,
45
+ )
46
+
47
+ __all__ = [
48
+ "AES_GCM_TAG_SIZE",
49
+ "CHACHA_TAG_SIZE",
50
+ "CipherSuite",
51
+ "DoubleRatchet",
52
+ "EMOJI_TABLE",
53
+ "HKDF_INFO_CHAIN_KEY",
54
+ "HKDF_INFO_MESSAGE_KEY",
55
+ "HKDF_INFO_RENDEZVOUS",
56
+ "HKDF_INFO_SAS",
57
+ "HKDF_INFO_SESSION_KEY",
58
+ "HandshakeResult",
59
+ "IdentityKeypair",
60
+ "KEY_SIZE",
61
+ "NONCE_SIZE",
62
+ "NoiseXXHandshake",
63
+ "PeerId",
64
+ "RatchetConfig",
65
+ "RatchetHeader",
66
+ "Role",
67
+ "Spake2Session",
68
+ "X25519Keypair",
69
+ "FilesystemKeyStorage",
70
+ "InMemoryKeyStorage",
71
+ "KeyStorage",
72
+ "aead_decrypt",
73
+ "aead_encrypt",
74
+ "derive_emoji_sas",
75
+ "derive_numeric_sas",
76
+ "hkdf_sha256",
77
+ "peer_id_from_public_key",
78
+ "verify_signature",
79
+ ]