pamoja-session 0.1.18__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.
- pamoja_session-0.1.18/.gitignore +13 -0
- pamoja_session-0.1.18/LICENSE-MIT +21 -0
- pamoja_session-0.1.18/PKG-INFO +99 -0
- pamoja_session-0.1.18/README.md +81 -0
- pamoja_session-0.1.18/pamoja/session/__init__.py +112 -0
- pamoja_session-0.1.18/pamoja/session/py.typed +0 -0
- pamoja_session-0.1.18/pyproject.toml +30 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anthony Wiedman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pamoja-session
|
|
3
|
+
Version: 0.1.18
|
|
4
|
+
Summary: X25519 key agreement, HKDF, and ChaCha20-Poly1305 with an anti-replay window, with no TLS stack.
|
|
5
|
+
Project-URL: Repository, https://github.com/molexxxx/pamoja
|
|
6
|
+
Project-URL: Documentation, https://pamoja.molex.cloud/docs/guides/session.html
|
|
7
|
+
Author: molexxxx
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE-MIT
|
|
10
|
+
Keywords: iot,pamoja,robotics,session
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: pamoja-native==0.1.18
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# pamoja-session
|
|
20
|
+
|
|
21
|
+
X25519 key agreement, HKDF, and ChaCha20-Poly1305 with an anti-replay window, with no TLS stack. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
|
|
22
|
+
|
|
23
|
+
[](https://pamoja.molex.cloud/docs/guides/session.html)
|
|
24
|
+
[](https://pamoja.molex.cloud/docs/)
|
|
25
|
+
[](https://pamoja.molex.cloud/docs/reference/python/pamoja/session.html)
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install pamoja-session
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from pamoja import session
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This pulls in `pamoja-native`, the compiled engine. `pip install pamoja` is the whole framework in one package.
|
|
38
|
+
|
|
39
|
+
## Example
|
|
40
|
+
|
|
41
|
+
The script the test suite runs, spliced here as it ran.
|
|
42
|
+
|
|
43
|
+
From [`bindings/python/guides/session.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/session.py):
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
|
|
48
|
+
from pamoja.core import PamojaError
|
|
49
|
+
from pamoja.session import AgreementKey, Role, Session
|
|
50
|
+
|
|
51
|
+
# Each device is provisioned with a 32-byte seed and publishes the key it derives. A real
|
|
52
|
+
# seed comes from the factory or a secure element; any 32 bytes stand in here.
|
|
53
|
+
node = AgreementKey(bytes([7]) * 32)
|
|
54
|
+
gateway = AgreementKey(bytes([9]) * 32)
|
|
55
|
+
|
|
56
|
+
# Neither side sends the session key. Both derive it from the shared secret, a salt that
|
|
57
|
+
# travels in the clear, and both public keys, with opposite roles.
|
|
58
|
+
#
|
|
59
|
+
# The salt must be fresh for every session: reusing one derives the same key from the same
|
|
60
|
+
# pair of devices twice. The initiator draws it and sends it in the clear, so the responder
|
|
61
|
+
# uses the salt it received rather than one of its own.
|
|
62
|
+
salt = os.urandom(16)
|
|
63
|
+
uplink = Session(node, gateway.public_key, salt, Role.INITIATOR)
|
|
64
|
+
downlink = Session(gateway, node.public_key, salt, Role.RESPONDER)
|
|
65
|
+
print("both sides derived a key without sending one")
|
|
66
|
+
|
|
67
|
+
# The pump id is authenticated but not encrypted, so a router still reads it while any
|
|
68
|
+
# change to it fails the tag.
|
|
69
|
+
sealed = uplink.seal(b"flow=41.2", b"pump-3")
|
|
70
|
+
print(f"sealed the reading is no longer readable: {sealed.ciphertext != b'flow=41.2'}")
|
|
71
|
+
print(f"opened {downlink.open(sealed, b'pump-3').decode()}")
|
|
72
|
+
|
|
73
|
+
# The anti-replay window refuses a counter it has already accepted, so a frame captured
|
|
74
|
+
# off the air and sent again is not delivered a second time.
|
|
75
|
+
try:
|
|
76
|
+
downlink.open(sealed, b"pump-3")
|
|
77
|
+
print("a replayed frame was accepted, which should never happen")
|
|
78
|
+
except PamojaError as error:
|
|
79
|
+
print(f"replay refused: {error}")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## The same capability in every language
|
|
83
|
+
|
|
84
|
+
| Language | Package | Reference |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| Rust | [`pamoja-session`](https://crates.io/crates/pamoja-session) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_session/index.html), [docs.rs](https://docs.rs/pamoja-session), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-session) |
|
|
87
|
+
| TypeScript | [`@pamoja/session`](https://www.npmjs.com/package/@pamoja/session) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_session.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-session) |
|
|
88
|
+
| Python | [`pamoja-session`](https://pypi.org/project/pamoja-session/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/session.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-session) |
|
|
89
|
+
| C# | [`Pamoja.Session`](https://www.nuget.org/packages/Pamoja.Session) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Session.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-session) |
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
- [`pamoja.session` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/session.html), every class and function in this module.
|
|
94
|
+
- [The Secured session guide](https://pamoja.molex.cloud/docs/guides/session.html), with the same example in Rust, TypeScript, and C#.
|
|
95
|
+
- [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# pamoja-session
|
|
2
|
+
|
|
3
|
+
X25519 key agreement, HKDF, and ChaCha20-Poly1305 with an anti-replay window, with no TLS stack. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
|
|
4
|
+
|
|
5
|
+
[](https://pamoja.molex.cloud/docs/guides/session.html)
|
|
6
|
+
[](https://pamoja.molex.cloud/docs/)
|
|
7
|
+
[](https://pamoja.molex.cloud/docs/reference/python/pamoja/session.html)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install pamoja-session
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pamoja import session
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This pulls in `pamoja-native`, the compiled engine. `pip install pamoja` is the whole framework in one package.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
The script the test suite runs, spliced here as it ran.
|
|
24
|
+
|
|
25
|
+
From [`bindings/python/guides/session.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/session.py):
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
from pamoja.core import PamojaError
|
|
31
|
+
from pamoja.session import AgreementKey, Role, Session
|
|
32
|
+
|
|
33
|
+
# Each device is provisioned with a 32-byte seed and publishes the key it derives. A real
|
|
34
|
+
# seed comes from the factory or a secure element; any 32 bytes stand in here.
|
|
35
|
+
node = AgreementKey(bytes([7]) * 32)
|
|
36
|
+
gateway = AgreementKey(bytes([9]) * 32)
|
|
37
|
+
|
|
38
|
+
# Neither side sends the session key. Both derive it from the shared secret, a salt that
|
|
39
|
+
# travels in the clear, and both public keys, with opposite roles.
|
|
40
|
+
#
|
|
41
|
+
# The salt must be fresh for every session: reusing one derives the same key from the same
|
|
42
|
+
# pair of devices twice. The initiator draws it and sends it in the clear, so the responder
|
|
43
|
+
# uses the salt it received rather than one of its own.
|
|
44
|
+
salt = os.urandom(16)
|
|
45
|
+
uplink = Session(node, gateway.public_key, salt, Role.INITIATOR)
|
|
46
|
+
downlink = Session(gateway, node.public_key, salt, Role.RESPONDER)
|
|
47
|
+
print("both sides derived a key without sending one")
|
|
48
|
+
|
|
49
|
+
# The pump id is authenticated but not encrypted, so a router still reads it while any
|
|
50
|
+
# change to it fails the tag.
|
|
51
|
+
sealed = uplink.seal(b"flow=41.2", b"pump-3")
|
|
52
|
+
print(f"sealed the reading is no longer readable: {sealed.ciphertext != b'flow=41.2'}")
|
|
53
|
+
print(f"opened {downlink.open(sealed, b'pump-3').decode()}")
|
|
54
|
+
|
|
55
|
+
# The anti-replay window refuses a counter it has already accepted, so a frame captured
|
|
56
|
+
# off the air and sent again is not delivered a second time.
|
|
57
|
+
try:
|
|
58
|
+
downlink.open(sealed, b"pump-3")
|
|
59
|
+
print("a replayed frame was accepted, which should never happen")
|
|
60
|
+
except PamojaError as error:
|
|
61
|
+
print(f"replay refused: {error}")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## The same capability in every language
|
|
65
|
+
|
|
66
|
+
| Language | Package | Reference |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| Rust | [`pamoja-session`](https://crates.io/crates/pamoja-session) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_session/index.html), [docs.rs](https://docs.rs/pamoja-session), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-session) |
|
|
69
|
+
| TypeScript | [`@pamoja/session`](https://www.npmjs.com/package/@pamoja/session) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_session.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-session) |
|
|
70
|
+
| Python | [`pamoja-session`](https://pypi.org/project/pamoja-session/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/session.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-session) |
|
|
71
|
+
| C# | [`Pamoja.Session`](https://www.nuget.org/packages/Pamoja.Session) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Session.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-session) |
|
|
72
|
+
|
|
73
|
+
## Documentation
|
|
74
|
+
|
|
75
|
+
- [`pamoja.session` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/session.html), every class and function in this module.
|
|
76
|
+
- [The Secured session guide](https://pamoja.molex.cloud/docs/guides/session.html), with the same example in Rust, TypeScript, and C#.
|
|
77
|
+
- [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Idiomatic secured-session facade.
|
|
2
|
+
|
|
3
|
+
Two devices that already know each other's public keys can agree on a session key
|
|
4
|
+
without ever sending it, and then exchange messages that are confidential, cannot
|
|
5
|
+
be altered undetected, and cannot be replayed. That is the whole of what a small
|
|
6
|
+
device usually needs from transport security, at a fraction of what a TLS stack
|
|
7
|
+
costs it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import enum
|
|
13
|
+
|
|
14
|
+
from pamoja._native import AgreementKey, SealedMessage
|
|
15
|
+
from pamoja._native import Session as _Session
|
|
16
|
+
from pamoja._native import hkdf_sha256_expand as _hkdf_sha256
|
|
17
|
+
from pamoja._native import hmac_sha256_digest as _hmac_sha256
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AgreementKey",
|
|
21
|
+
"Role",
|
|
22
|
+
"SealedMessage",
|
|
23
|
+
"Session",
|
|
24
|
+
"hkdf_sha256",
|
|
25
|
+
"hmac_sha256",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Role(str, enum.Enum):
|
|
30
|
+
"""Which side of a session a device is on.
|
|
31
|
+
|
|
32
|
+
The two devices must choose opposite roles: the role decides the order the
|
|
33
|
+
public keys are mixed in and which direction each side tags its messages
|
|
34
|
+
with, so a session where both sides claim the same role opens nothing.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
#: The device that opens the session.
|
|
38
|
+
INITIATOR = "Initiator"
|
|
39
|
+
#: The device that answers.
|
|
40
|
+
RESPONDER = "Responder"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Session:
|
|
44
|
+
"""A confidential, tamper-evident, replay-protected channel with one peer."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
local: AgreementKey,
|
|
49
|
+
peer_public_key: bytes,
|
|
50
|
+
salt: bytes,
|
|
51
|
+
role: Role,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Establish a session with a peer.
|
|
54
|
+
|
|
55
|
+
:param local: This device's key-agreement secret.
|
|
56
|
+
:param peer_public_key: The peer's 32-byte public key, already
|
|
57
|
+
authenticated by pinning or by a signature.
|
|
58
|
+
:param salt: A fresh per-session salt both sides share, exchanged in the
|
|
59
|
+
clear. Reusing one with the same pair of keys reuses the session key,
|
|
60
|
+
so it must change each session.
|
|
61
|
+
:param role: Whether this device opens the session or answers.
|
|
62
|
+
"""
|
|
63
|
+
self._inner = _Session(local, peer_public_key, salt, role.value)
|
|
64
|
+
|
|
65
|
+
def seal(self, plaintext: bytes, aad: bytes = b"") -> SealedMessage:
|
|
66
|
+
"""Seal a message for the peer.
|
|
67
|
+
|
|
68
|
+
:param plaintext: The message to protect.
|
|
69
|
+
:param aad: Data authenticated but not encrypted, so it stays readable on
|
|
70
|
+
the wire yet cannot be altered: a device identifier or a routing
|
|
71
|
+
header belongs here.
|
|
72
|
+
:returns: The ciphertext, with the counter and tag to send beside it.
|
|
73
|
+
"""
|
|
74
|
+
return self._inner.seal(plaintext, aad)
|
|
75
|
+
|
|
76
|
+
def open(self, sealed: SealedMessage, aad: bytes = b"") -> bytes:
|
|
77
|
+
"""Open a message from the peer.
|
|
78
|
+
|
|
79
|
+
:param sealed: The ciphertext with the counter and tag that arrived with
|
|
80
|
+
it.
|
|
81
|
+
:param aad: The same associated data the sender authenticated.
|
|
82
|
+
:returns: The plaintext.
|
|
83
|
+
:raises PamojaError: When the counter repeats or is older than the replay
|
|
84
|
+
window still tracks, and when the tag does not authenticate. Nothing
|
|
85
|
+
readable is ever returned from a message that failed either check.
|
|
86
|
+
"""
|
|
87
|
+
return self._inner.open(sealed, aad)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def hmac_sha256(key: bytes, message: bytes) -> bytes:
|
|
91
|
+
"""Compute a keyed hash over a message.
|
|
92
|
+
|
|
93
|
+
This is the primitive a host uses to authenticate a pairing exchange or a
|
|
94
|
+
single command, where a whole session would be more than the job needs.
|
|
95
|
+
|
|
96
|
+
:param key: The secret key.
|
|
97
|
+
:param message: The message to authenticate.
|
|
98
|
+
:returns: The 32-byte digest.
|
|
99
|
+
"""
|
|
100
|
+
return _hmac_sha256(key, message)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def hkdf_sha256(salt: bytes, ikm: bytes, info: bytes, length: int) -> bytes:
|
|
104
|
+
"""Expand input keying material into ``length`` bytes bound to ``info``.
|
|
105
|
+
|
|
106
|
+
:param salt: The salt, which may be empty.
|
|
107
|
+
:param ikm: The input keying material.
|
|
108
|
+
:param info: Context binding the output to its purpose.
|
|
109
|
+
:param length: How many bytes to derive.
|
|
110
|
+
:returns: The derived bytes.
|
|
111
|
+
"""
|
|
112
|
+
return _hkdf_sha256(salt, ikm, info, length)
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pamoja-session"
|
|
7
|
+
version = "0.1.18"
|
|
8
|
+
description = "X25519 key agreement, HKDF, and ChaCha20-Poly1305 with an anti-replay window, with no TLS stack."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
license-files = ["LICENSE-MIT"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "molexxxx" }]
|
|
14
|
+
keywords = ["pamoja", "iot", "robotics", "session"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pamoja-native==0.1.18",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Repository = "https://github.com/molexxxx/pamoja"
|
|
27
|
+
Documentation = "https://pamoja.molex.cloud/docs/guides/session.html"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["pamoja"]
|