samp-core 1.1.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.
- samp_core-1.1.0/PKG-INFO +12 -0
- samp_core-1.1.0/README.md +87 -0
- samp_core-1.1.0/pyproject.toml +38 -0
- samp_core-1.1.0/samp/__init__.py +243 -0
- samp_core-1.1.0/samp/encryption.py +149 -0
- samp_core-1.1.0/samp/error.py +2 -0
- samp_core-1.1.0/samp/extrinsic.py +154 -0
- samp_core-1.1.0/samp/metadata.py +555 -0
- samp_core-1.1.0/samp/py.typed +0 -0
- samp_core-1.1.0/samp/scale.py +53 -0
- samp_core-1.1.0/samp/secret.py +70 -0
- samp_core-1.1.0/samp/ss58.py +85 -0
- samp_core-1.1.0/samp/types.py +251 -0
- samp_core-1.1.0/samp/wire.py +372 -0
- samp_core-1.1.0/samp_core.egg-info/PKG-INFO +12 -0
- samp_core-1.1.0/samp_core.egg-info/SOURCES.txt +28 -0
- samp_core-1.1.0/samp_core.egg-info/dependency_links.txt +1 -0
- samp_core-1.1.0/samp_core.egg-info/requires.txt +5 -0
- samp_core-1.1.0/samp_core.egg-info/top_level.txt +1 -0
- samp_core-1.1.0/setup.cfg +4 -0
- samp_core-1.1.0/tests/test_conformance.py +304 -0
- samp_core-1.1.0/tests/test_crypto.py +90 -0
- samp_core-1.1.0/tests/test_encryption.py +61 -0
- samp_core-1.1.0/tests/test_extrinsic.py +319 -0
- samp_core-1.1.0/tests/test_metadata.py +115 -0
- samp_core-1.1.0/tests/test_round_trip.py +303 -0
- samp_core-1.1.0/tests/test_scale.py +114 -0
- samp_core-1.1.0/tests/test_ss58.py +71 -0
- samp_core-1.1.0/tests/test_types.py +306 -0
- samp_core-1.1.0/tests/test_wire.py +320 -0
samp_core-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: samp-core
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Substrate Account Messaging Protocol -- Python SDK
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/samp-org/samp
|
|
7
|
+
Project-URL: Specification, https://github.com/samp-org/samp/blob/main/specs/samp.md
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: samp-crypto
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest; extra == "dev"
|
|
12
|
+
Requires-Dist: mypy; extra == "dev"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# samp
|
|
2
|
+
|
|
3
|
+
Python implementation of [SAMP](https://github.com/samp-org/samp) (Substrate Account Messaging Protocol). Crypto operations use a native Rust extension via PyO3.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Requires a Rust toolchain for the native crypto extension.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install maturin
|
|
11
|
+
cd python/samp-crypto && maturin develop && cd ../..
|
|
12
|
+
cd python && pip install -e .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
from samp import (
|
|
21
|
+
EncryptedRemark, Seed, decode_remark,
|
|
22
|
+
nonce_from_bytes, plaintext_from_bytes, pubkey_from_bytes, sr25519_signing_scalar,
|
|
23
|
+
)
|
|
24
|
+
from samp.encryption import compute_view_tag, decrypt, encrypt
|
|
25
|
+
from samp.wire import ContentType, encode_encrypted, encode_public
|
|
26
|
+
|
|
27
|
+
sender_seed = Seed.from_bytes(bytes.fromhex("e5be9a5092b81bca" + "00" * 24))
|
|
28
|
+
recipient_pub = pubkey_from_bytes(bytes.fromhex("8eaf04151687736" + "0" + "00" * 24))
|
|
29
|
+
|
|
30
|
+
# Public message
|
|
31
|
+
remark = encode_public(recipient_pub, "Hello from Python")
|
|
32
|
+
|
|
33
|
+
# Encrypted message
|
|
34
|
+
nonce = nonce_from_bytes(os.urandom(12))
|
|
35
|
+
plaintext = plaintext_from_bytes(b"Private message")
|
|
36
|
+
ciphertext = encrypt(plaintext, recipient_pub, nonce, sender_seed)
|
|
37
|
+
tag = compute_view_tag(sender_seed, recipient_pub, nonce)
|
|
38
|
+
enc_remark = encode_encrypted(ContentType.ENCRYPTED, tag, nonce, ciphertext)
|
|
39
|
+
|
|
40
|
+
# Decrypt
|
|
41
|
+
recipient_seed = Seed.from_bytes(bytes(32))
|
|
42
|
+
scalar = sr25519_signing_scalar(recipient_seed)
|
|
43
|
+
parsed = decode_remark(enc_remark)
|
|
44
|
+
assert isinstance(parsed, EncryptedRemark)
|
|
45
|
+
clear = decrypt(parsed.ciphertext, parsed.nonce, scalar)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
### Wire
|
|
51
|
+
|
|
52
|
+
| Function | Description |
|
|
53
|
+
|----------|-------------|
|
|
54
|
+
| `encode_public` | Public message (`0x10`) |
|
|
55
|
+
| `encode_encrypted` | Encrypted or thread message (`0x11`/`0x12`) |
|
|
56
|
+
| `encode_channel_create` | Channel creation (`0x13`) |
|
|
57
|
+
| `encode_channel_msg` | Channel message (`0x14`) |
|
|
58
|
+
| `encode_group` | Group message (`0x15`) |
|
|
59
|
+
| `decode_remark` | Parse any SAMP remark → `Remark` dataclass |
|
|
60
|
+
| `encode_thread_content` / `decode_thread_content` | Thread plaintext (refs + body) |
|
|
61
|
+
| `encode_channel_content` / `decode_channel_content` | Channel plaintext (refs + body) |
|
|
62
|
+
| `decode_group_content` | Group plaintext (refs + body) |
|
|
63
|
+
| `encode_group_members` / `decode_group_members` | Group member list |
|
|
64
|
+
| `channel_ref_from_recipient` | Extract channel ref from recipient field |
|
|
65
|
+
|
|
66
|
+
### Crypto
|
|
67
|
+
|
|
68
|
+
| Function | Description |
|
|
69
|
+
|----------|-------------|
|
|
70
|
+
| `encrypt` / `decrypt` | 1:1 ECDH + ChaCha20-Poly1305 |
|
|
71
|
+
| `decrypt_as_sender` | Sender self-decryption via sealed_to |
|
|
72
|
+
| `encrypt_for_group` / `decrypt_from_group` | Multi-recipient encryption |
|
|
73
|
+
| `sr25519_signing_scalar` | Derive signing scalar from sr25519 seed |
|
|
74
|
+
| `public_from_seed` | Derive public key from seed |
|
|
75
|
+
| `compute_view_tag` / `check_view_tag` | 1-byte recipient filter |
|
|
76
|
+
| `unseal_recipient` | Recover recipient from sealed_to field |
|
|
77
|
+
| `build_capsules` / `scan_capsules` | Group capsule construction and scanning |
|
|
78
|
+
|
|
79
|
+
### Types
|
|
80
|
+
|
|
81
|
+
`Remark` (dataclass), `SampError` (exception)
|
|
82
|
+
|
|
83
|
+
## Test
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
pytest tests/ -v
|
|
87
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "samp-core"
|
|
3
|
+
version = "1.1.0"
|
|
4
|
+
description = "Substrate Account Messaging Protocol -- Python SDK"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
dependencies = ["samp-crypto"]
|
|
8
|
+
|
|
9
|
+
[project.optional-dependencies]
|
|
10
|
+
dev = ["pytest", "mypy"]
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Repository = "https://github.com/samp-org/samp"
|
|
14
|
+
Specification = "https://github.com/samp-org/samp/blob/main/specs/samp.md"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["setuptools"]
|
|
18
|
+
build-backend = "setuptools.build_meta"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.package-data]
|
|
21
|
+
samp = ["py.typed"]
|
|
22
|
+
|
|
23
|
+
[tool.mypy]
|
|
24
|
+
strict = true
|
|
25
|
+
files = ["samp"]
|
|
26
|
+
warn_unused_ignores = true
|
|
27
|
+
no_implicit_optional = true
|
|
28
|
+
disallow_untyped_defs = true
|
|
29
|
+
|
|
30
|
+
[[tool.mypy.overrides]]
|
|
31
|
+
module = "samp_crypto"
|
|
32
|
+
ignore_missing_imports = true
|
|
33
|
+
|
|
34
|
+
[tool.pyright]
|
|
35
|
+
include = ["samp", "tests"]
|
|
36
|
+
venvPath = ".."
|
|
37
|
+
venv = ".venv"
|
|
38
|
+
reportMissingTypeStubs = "none"
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from samp.encryption import (
|
|
2
|
+
ENCRYPTED_OVERHEAD,
|
|
3
|
+
build_capsules,
|
|
4
|
+
check_view_tag,
|
|
5
|
+
compute_view_tag,
|
|
6
|
+
decrypt,
|
|
7
|
+
decrypt_as_sender,
|
|
8
|
+
decrypt_from_group,
|
|
9
|
+
derive_group_ephemeral,
|
|
10
|
+
encrypt,
|
|
11
|
+
encrypt_for_group,
|
|
12
|
+
public_from_seed,
|
|
13
|
+
sr25519_sign,
|
|
14
|
+
sr25519_signing_scalar,
|
|
15
|
+
unseal_recipient,
|
|
16
|
+
)
|
|
17
|
+
from samp.error import SampError
|
|
18
|
+
from samp.extrinsic import (
|
|
19
|
+
ChainParams,
|
|
20
|
+
ExtractedCall,
|
|
21
|
+
ExtrinsicError,
|
|
22
|
+
build_signed_extrinsic,
|
|
23
|
+
extract_call,
|
|
24
|
+
extract_signer,
|
|
25
|
+
)
|
|
26
|
+
from samp.metadata import (
|
|
27
|
+
ErrorEntry,
|
|
28
|
+
ErrorTable,
|
|
29
|
+
FieldNotFoundError,
|
|
30
|
+
Metadata,
|
|
31
|
+
MetadataError,
|
|
32
|
+
ScaleError,
|
|
33
|
+
StorageLayout,
|
|
34
|
+
StorageNotFoundError,
|
|
35
|
+
)
|
|
36
|
+
from samp.scale import decode_bytes, decode_compact, encode_compact
|
|
37
|
+
from samp.secret import ContentKey, Seed, ViewScalar
|
|
38
|
+
from samp.ss58 import decode as ss58_decode
|
|
39
|
+
from samp.ss58 import encode as ss58_encode
|
|
40
|
+
from samp.types import (
|
|
41
|
+
CAPSULE_SIZE,
|
|
42
|
+
CHANNEL_DESC_MAX,
|
|
43
|
+
CHANNEL_NAME_MAX,
|
|
44
|
+
SS58_PREFIX_KUSAMA,
|
|
45
|
+
SS58_PREFIX_POLKADOT,
|
|
46
|
+
SS58_PREFIX_SUBSTRATE_GENERIC,
|
|
47
|
+
BlockNumber,
|
|
48
|
+
BlockRef,
|
|
49
|
+
CallArgs,
|
|
50
|
+
CallIdx,
|
|
51
|
+
Capsules,
|
|
52
|
+
ChannelDescription,
|
|
53
|
+
ChannelName,
|
|
54
|
+
Ciphertext,
|
|
55
|
+
EphPubkey,
|
|
56
|
+
ExtIndex,
|
|
57
|
+
ExtrinsicBytes,
|
|
58
|
+
ExtrinsicNonce,
|
|
59
|
+
GenesisHash,
|
|
60
|
+
Nonce,
|
|
61
|
+
PalletIdx,
|
|
62
|
+
Plaintext,
|
|
63
|
+
Pubkey,
|
|
64
|
+
RemarkBytes,
|
|
65
|
+
Signature,
|
|
66
|
+
SpecVersion,
|
|
67
|
+
Ss58Address,
|
|
68
|
+
Ss58Prefix,
|
|
69
|
+
TxVersion,
|
|
70
|
+
ViewTag,
|
|
71
|
+
block_number_from_int,
|
|
72
|
+
call_args_from_bytes,
|
|
73
|
+
call_idx_from_int,
|
|
74
|
+
capsules_count,
|
|
75
|
+
capsules_from_bytes,
|
|
76
|
+
ciphertext_from_bytes,
|
|
77
|
+
eph_pubkey_from_bytes,
|
|
78
|
+
ext_index_from_int,
|
|
79
|
+
extrinsic_bytes_from_bytes,
|
|
80
|
+
extrinsic_nonce_from_int,
|
|
81
|
+
genesis_hash_from_bytes,
|
|
82
|
+
nonce_from_bytes,
|
|
83
|
+
pallet_idx_from_int,
|
|
84
|
+
plaintext_from_bytes,
|
|
85
|
+
pubkey_from_bytes,
|
|
86
|
+
pubkey_zero,
|
|
87
|
+
remark_bytes_from_bytes,
|
|
88
|
+
signature_from_bytes,
|
|
89
|
+
spec_version_from_int,
|
|
90
|
+
ss58_prefix_from_int,
|
|
91
|
+
tx_version_from_int,
|
|
92
|
+
view_tag_from_int,
|
|
93
|
+
)
|
|
94
|
+
from samp.wire import (
|
|
95
|
+
CHANNEL_HEADER_SIZE,
|
|
96
|
+
SAMP_VERSION,
|
|
97
|
+
THREAD_HEADER_SIZE,
|
|
98
|
+
ApplicationRemark,
|
|
99
|
+
ChannelCreateRemark,
|
|
100
|
+
ChannelRemark,
|
|
101
|
+
ContentType,
|
|
102
|
+
EncryptedRemark,
|
|
103
|
+
GroupRemark,
|
|
104
|
+
PublicRemark,
|
|
105
|
+
Remark,
|
|
106
|
+
ThreadRemark,
|
|
107
|
+
content_type_from_byte,
|
|
108
|
+
decode_channel_content,
|
|
109
|
+
decode_channel_create,
|
|
110
|
+
decode_group_content,
|
|
111
|
+
decode_group_members,
|
|
112
|
+
decode_remark,
|
|
113
|
+
decode_thread_content,
|
|
114
|
+
encode_channel_content,
|
|
115
|
+
encode_channel_create,
|
|
116
|
+
encode_channel_msg,
|
|
117
|
+
encode_encrypted,
|
|
118
|
+
encode_group,
|
|
119
|
+
encode_group_members,
|
|
120
|
+
encode_public,
|
|
121
|
+
encode_thread_content,
|
|
122
|
+
is_samp_remark,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
__all__ = [
|
|
126
|
+
"SAMP_VERSION",
|
|
127
|
+
"ContentType",
|
|
128
|
+
"content_type_from_byte",
|
|
129
|
+
"is_samp_remark",
|
|
130
|
+
"CAPSULE_SIZE",
|
|
131
|
+
"CHANNEL_HEADER_SIZE",
|
|
132
|
+
"THREAD_HEADER_SIZE",
|
|
133
|
+
"CHANNEL_NAME_MAX",
|
|
134
|
+
"CHANNEL_DESC_MAX",
|
|
135
|
+
"ENCRYPTED_OVERHEAD",
|
|
136
|
+
"Remark",
|
|
137
|
+
"PublicRemark",
|
|
138
|
+
"EncryptedRemark",
|
|
139
|
+
"ThreadRemark",
|
|
140
|
+
"ChannelCreateRemark",
|
|
141
|
+
"ChannelRemark",
|
|
142
|
+
"GroupRemark",
|
|
143
|
+
"ApplicationRemark",
|
|
144
|
+
"SampError",
|
|
145
|
+
"encode_public",
|
|
146
|
+
"encode_encrypted",
|
|
147
|
+
"encode_channel_msg",
|
|
148
|
+
"encode_channel_create",
|
|
149
|
+
"encode_group",
|
|
150
|
+
"encode_group_members",
|
|
151
|
+
"decode_remark",
|
|
152
|
+
"decode_thread_content",
|
|
153
|
+
"decode_channel_content",
|
|
154
|
+
"decode_channel_create",
|
|
155
|
+
"decode_group_content",
|
|
156
|
+
"decode_group_members",
|
|
157
|
+
"encode_thread_content",
|
|
158
|
+
"encode_channel_content",
|
|
159
|
+
"sr25519_sign",
|
|
160
|
+
"sr25519_signing_scalar",
|
|
161
|
+
"public_from_seed",
|
|
162
|
+
"encrypt",
|
|
163
|
+
"decrypt",
|
|
164
|
+
"decrypt_as_sender",
|
|
165
|
+
"compute_view_tag",
|
|
166
|
+
"check_view_tag",
|
|
167
|
+
"unseal_recipient",
|
|
168
|
+
"derive_group_ephemeral",
|
|
169
|
+
"build_capsules",
|
|
170
|
+
"encrypt_for_group",
|
|
171
|
+
"decrypt_from_group",
|
|
172
|
+
"decode_compact",
|
|
173
|
+
"encode_compact",
|
|
174
|
+
"decode_bytes",
|
|
175
|
+
"Metadata",
|
|
176
|
+
"StorageLayout",
|
|
177
|
+
"ErrorEntry",
|
|
178
|
+
"ErrorTable",
|
|
179
|
+
"MetadataError",
|
|
180
|
+
"ScaleError",
|
|
181
|
+
"StorageNotFoundError",
|
|
182
|
+
"FieldNotFoundError",
|
|
183
|
+
"ChainParams",
|
|
184
|
+
"ExtractedCall",
|
|
185
|
+
"ExtrinsicError",
|
|
186
|
+
"build_signed_extrinsic",
|
|
187
|
+
"extract_signer",
|
|
188
|
+
"extract_call",
|
|
189
|
+
"Seed",
|
|
190
|
+
"ViewScalar",
|
|
191
|
+
"ContentKey",
|
|
192
|
+
"ss58_encode",
|
|
193
|
+
"ss58_decode",
|
|
194
|
+
"BlockNumber",
|
|
195
|
+
"BlockRef",
|
|
196
|
+
"CallArgs",
|
|
197
|
+
"CallIdx",
|
|
198
|
+
"Capsules",
|
|
199
|
+
"ChannelDescription",
|
|
200
|
+
"ChannelName",
|
|
201
|
+
"Ciphertext",
|
|
202
|
+
"EphPubkey",
|
|
203
|
+
"ExtIndex",
|
|
204
|
+
"ExtrinsicBytes",
|
|
205
|
+
"ExtrinsicNonce",
|
|
206
|
+
"GenesisHash",
|
|
207
|
+
"Nonce",
|
|
208
|
+
"PalletIdx",
|
|
209
|
+
"Plaintext",
|
|
210
|
+
"Pubkey",
|
|
211
|
+
"RemarkBytes",
|
|
212
|
+
"Signature",
|
|
213
|
+
"SpecVersion",
|
|
214
|
+
"Ss58Address",
|
|
215
|
+
"Ss58Prefix",
|
|
216
|
+
"TxVersion",
|
|
217
|
+
"ViewTag",
|
|
218
|
+
"SS58_PREFIX_KUSAMA",
|
|
219
|
+
"SS58_PREFIX_POLKADOT",
|
|
220
|
+
"SS58_PREFIX_SUBSTRATE_GENERIC",
|
|
221
|
+
"block_number_from_int",
|
|
222
|
+
"call_args_from_bytes",
|
|
223
|
+
"call_idx_from_int",
|
|
224
|
+
"capsules_count",
|
|
225
|
+
"capsules_from_bytes",
|
|
226
|
+
"ciphertext_from_bytes",
|
|
227
|
+
"eph_pubkey_from_bytes",
|
|
228
|
+
"ext_index_from_int",
|
|
229
|
+
"extrinsic_bytes_from_bytes",
|
|
230
|
+
"extrinsic_nonce_from_int",
|
|
231
|
+
"genesis_hash_from_bytes",
|
|
232
|
+
"nonce_from_bytes",
|
|
233
|
+
"pallet_idx_from_int",
|
|
234
|
+
"plaintext_from_bytes",
|
|
235
|
+
"pubkey_from_bytes",
|
|
236
|
+
"pubkey_zero",
|
|
237
|
+
"remark_bytes_from_bytes",
|
|
238
|
+
"signature_from_bytes",
|
|
239
|
+
"spec_version_from_int",
|
|
240
|
+
"ss58_prefix_from_int",
|
|
241
|
+
"tx_version_from_int",
|
|
242
|
+
"view_tag_from_int",
|
|
243
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import samp_crypto
|
|
6
|
+
|
|
7
|
+
from samp.error import SampError
|
|
8
|
+
from samp.secret import ContentKey, Seed, ViewScalar
|
|
9
|
+
from samp.types import (
|
|
10
|
+
Capsules,
|
|
11
|
+
Ciphertext,
|
|
12
|
+
EphPubkey,
|
|
13
|
+
Nonce,
|
|
14
|
+
Plaintext,
|
|
15
|
+
Pubkey,
|
|
16
|
+
Signature,
|
|
17
|
+
ViewTag,
|
|
18
|
+
capsules_from_bytes,
|
|
19
|
+
ciphertext_from_bytes,
|
|
20
|
+
eph_pubkey_from_bytes,
|
|
21
|
+
plaintext_from_bytes,
|
|
22
|
+
pubkey_from_bytes,
|
|
23
|
+
signature_from_bytes,
|
|
24
|
+
view_tag_from_int,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
ENCRYPTED_OVERHEAD = 80
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sr25519_sign(seed: Seed, message: bytes) -> Signature:
|
|
31
|
+
return signature_from_bytes(samp_crypto.sr25519_sign(seed.expose_secret(), message))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def sr25519_signing_scalar(seed: Seed) -> ViewScalar:
|
|
35
|
+
return ViewScalar.from_bytes(samp_crypto.sr25519_signing_scalar(seed.expose_secret()))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def public_from_seed(seed: Seed) -> Pubkey:
|
|
39
|
+
return pubkey_from_bytes(samp_crypto.public_from_seed(seed.expose_secret()))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def encrypt(
|
|
43
|
+
plaintext: Plaintext,
|
|
44
|
+
recipient: Pubkey,
|
|
45
|
+
nonce: Nonce,
|
|
46
|
+
sender_seed: Seed,
|
|
47
|
+
) -> Ciphertext:
|
|
48
|
+
return ciphertext_from_bytes(
|
|
49
|
+
samp_crypto.encrypt_content(plaintext, recipient, nonce, sender_seed.expose_secret())
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def decrypt(
|
|
54
|
+
ciphertext: Ciphertext,
|
|
55
|
+
nonce: Nonce,
|
|
56
|
+
signing_scalar: ViewScalar,
|
|
57
|
+
) -> Plaintext:
|
|
58
|
+
return plaintext_from_bytes(
|
|
59
|
+
samp_crypto.decrypt_content(ciphertext, signing_scalar.expose_secret(), nonce)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def decrypt_as_sender(
|
|
64
|
+
ciphertext: Ciphertext,
|
|
65
|
+
nonce: Nonce,
|
|
66
|
+
sender_seed: Seed,
|
|
67
|
+
) -> Plaintext:
|
|
68
|
+
return plaintext_from_bytes(
|
|
69
|
+
samp_crypto.decrypt_as_sender(ciphertext, sender_seed.expose_secret(), nonce)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compute_view_tag(sender_seed: Seed, recipient: Pubkey, nonce: Nonce) -> ViewTag:
|
|
74
|
+
return view_tag_from_int(
|
|
75
|
+
samp_crypto.compute_view_tag(sender_seed.expose_secret(), recipient, nonce)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def check_view_tag(ciphertext: Ciphertext, signing_scalar: ViewScalar) -> ViewTag:
|
|
80
|
+
return view_tag_from_int(samp_crypto.check_view_tag(signing_scalar.expose_secret(), ciphertext))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def unseal_recipient(ciphertext: Ciphertext, nonce: Nonce, sender_seed: Seed) -> Pubkey:
|
|
84
|
+
return pubkey_from_bytes(
|
|
85
|
+
samp_crypto.unseal_recipient(ciphertext, sender_seed.expose_secret(), nonce)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def derive_group_ephemeral(sender_seed: Seed, nonce: Nonce) -> bytes:
|
|
90
|
+
result: bytes = samp_crypto.derive_group_ephemeral(sender_seed.expose_secret(), nonce)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_capsules(
|
|
95
|
+
content_key: ContentKey,
|
|
96
|
+
member_pubkeys: list[Pubkey],
|
|
97
|
+
eph_scalar: bytes,
|
|
98
|
+
nonce: Nonce,
|
|
99
|
+
) -> Capsules:
|
|
100
|
+
return capsules_from_bytes(
|
|
101
|
+
samp_crypto.build_capsules(
|
|
102
|
+
content_key.expose_secret(), list(member_pubkeys), eph_scalar, nonce
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def scan_capsules(
|
|
108
|
+
data: bytes,
|
|
109
|
+
eph_pubkey: EphPubkey,
|
|
110
|
+
my_scalar: ViewScalar,
|
|
111
|
+
nonce: Nonce,
|
|
112
|
+
) -> Optional[tuple[int, ContentKey]]:
|
|
113
|
+
result = samp_crypto.scan_capsules(data, eph_pubkey, my_scalar.expose_secret(), nonce)
|
|
114
|
+
if result is None:
|
|
115
|
+
return None
|
|
116
|
+
idx, ck = result
|
|
117
|
+
return int(idx), ContentKey.from_bytes(bytes(ck))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def encrypt_for_group(
|
|
121
|
+
plaintext: Plaintext,
|
|
122
|
+
member_pubkeys: list[Pubkey],
|
|
123
|
+
nonce: Nonce,
|
|
124
|
+
sender_seed: Seed,
|
|
125
|
+
) -> tuple[EphPubkey, Capsules, Ciphertext]:
|
|
126
|
+
eph, caps, ct = samp_crypto.encrypt_for_group(
|
|
127
|
+
plaintext, list(member_pubkeys), nonce, sender_seed.expose_secret()
|
|
128
|
+
)
|
|
129
|
+
return (
|
|
130
|
+
eph_pubkey_from_bytes(eph),
|
|
131
|
+
capsules_from_bytes(caps),
|
|
132
|
+
ciphertext_from_bytes(ct),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def decrypt_from_group(
|
|
137
|
+
content: bytes,
|
|
138
|
+
my_scalar: ViewScalar,
|
|
139
|
+
nonce: Nonce,
|
|
140
|
+
known_n: Optional[int] = None,
|
|
141
|
+
) -> Plaintext:
|
|
142
|
+
try:
|
|
143
|
+
return plaintext_from_bytes(
|
|
144
|
+
samp_crypto.decrypt_from_group(content, my_scalar.expose_secret(), nonce, known_n)
|
|
145
|
+
)
|
|
146
|
+
except SampError:
|
|
147
|
+
raise
|
|
148
|
+
except Exception as e:
|
|
149
|
+
raise SampError(f"decryption failed: {e}") from e
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Callable, Optional
|
|
6
|
+
|
|
7
|
+
from samp.scale import decode_compact, encode_compact
|
|
8
|
+
from samp.types import (
|
|
9
|
+
CallArgs,
|
|
10
|
+
CallIdx,
|
|
11
|
+
ExtrinsicBytes,
|
|
12
|
+
ExtrinsicNonce,
|
|
13
|
+
GenesisHash,
|
|
14
|
+
PalletIdx,
|
|
15
|
+
Pubkey,
|
|
16
|
+
SpecVersion,
|
|
17
|
+
TxVersion,
|
|
18
|
+
call_args_from_bytes,
|
|
19
|
+
call_idx_from_int,
|
|
20
|
+
extrinsic_bytes_from_bytes,
|
|
21
|
+
pallet_idx_from_int,
|
|
22
|
+
pubkey_from_bytes,
|
|
23
|
+
signature_from_bytes,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
EXT_VERSION_SIGNED = 0x84
|
|
27
|
+
ADDR_TYPE_ID = 0x00
|
|
28
|
+
SIG_TYPE_SR25519 = 0x01
|
|
29
|
+
ERA_IMMORTAL = 0x00
|
|
30
|
+
METADATA_HASH_DISABLED = 0x00
|
|
31
|
+
SIGNED_HEADER_LEN = 99
|
|
32
|
+
MIN_SIGNED_EXTRINSIC = 103
|
|
33
|
+
MIN_SIGNER_PAYLOAD = 34
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExtrinsicError(Exception):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class ChainParams:
|
|
42
|
+
genesis_hash: GenesisHash
|
|
43
|
+
spec_version: SpecVersion
|
|
44
|
+
tx_version: TxVersion
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ExtractedCall:
|
|
49
|
+
pallet: PalletIdx
|
|
50
|
+
call: CallIdx
|
|
51
|
+
args: CallArgs
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_signed_extrinsic(
|
|
55
|
+
pallet_idx: PalletIdx,
|
|
56
|
+
call_idx: CallIdx,
|
|
57
|
+
call_args: CallArgs,
|
|
58
|
+
public_key: Pubkey,
|
|
59
|
+
sign: Callable[[bytes], bytes],
|
|
60
|
+
nonce: ExtrinsicNonce,
|
|
61
|
+
chain_params: ChainParams,
|
|
62
|
+
) -> ExtrinsicBytes:
|
|
63
|
+
call_data = bytes([int(pallet_idx), int(call_idx)]) + call_args
|
|
64
|
+
tip = bytes([0])
|
|
65
|
+
|
|
66
|
+
signing_payload = (
|
|
67
|
+
call_data
|
|
68
|
+
+ bytes([ERA_IMMORTAL])
|
|
69
|
+
+ encode_compact(int(nonce))
|
|
70
|
+
+ tip
|
|
71
|
+
+ bytes([METADATA_HASH_DISABLED])
|
|
72
|
+
+ int(chain_params.spec_version).to_bytes(4, "little")
|
|
73
|
+
+ int(chain_params.tx_version).to_bytes(4, "little")
|
|
74
|
+
+ chain_params.genesis_hash
|
|
75
|
+
+ chain_params.genesis_hash
|
|
76
|
+
+ bytes([0x00])
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if len(signing_payload) > 256:
|
|
80
|
+
to_sign = hashlib.blake2b(signing_payload, digest_size=32).digest()
|
|
81
|
+
else:
|
|
82
|
+
to_sign = signing_payload
|
|
83
|
+
|
|
84
|
+
signature = signature_from_bytes(sign(to_sign))
|
|
85
|
+
|
|
86
|
+
extrinsic_payload = (
|
|
87
|
+
bytes([EXT_VERSION_SIGNED, ADDR_TYPE_ID])
|
|
88
|
+
+ public_key
|
|
89
|
+
+ bytes([SIG_TYPE_SR25519])
|
|
90
|
+
+ signature
|
|
91
|
+
+ bytes([ERA_IMMORTAL])
|
|
92
|
+
+ encode_compact(int(nonce))
|
|
93
|
+
+ tip
|
|
94
|
+
+ bytes([METADATA_HASH_DISABLED])
|
|
95
|
+
+ call_data
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return extrinsic_bytes_from_bytes(encode_compact(len(extrinsic_payload)) + extrinsic_payload)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def extract_signer(extrinsic_bytes: ExtrinsicBytes) -> Optional[Pubkey]:
|
|
102
|
+
decoded = decode_compact(extrinsic_bytes)
|
|
103
|
+
if decoded is None:
|
|
104
|
+
return None
|
|
105
|
+
_, prefix_len = decoded
|
|
106
|
+
payload = extrinsic_bytes[prefix_len:]
|
|
107
|
+
if (
|
|
108
|
+
len(payload) < MIN_SIGNER_PAYLOAD
|
|
109
|
+
or payload[0] & 0x80 == 0
|
|
110
|
+
or payload[1] != ADDR_TYPE_ID
|
|
111
|
+
):
|
|
112
|
+
return None
|
|
113
|
+
return pubkey_from_bytes(payload[2:34])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def extract_call(extrinsic_bytes: ExtrinsicBytes) -> Optional[ExtractedCall]:
|
|
117
|
+
decoded = decode_compact(extrinsic_bytes)
|
|
118
|
+
if decoded is None:
|
|
119
|
+
return None
|
|
120
|
+
_, prefix_len = decoded
|
|
121
|
+
payload = extrinsic_bytes[prefix_len:]
|
|
122
|
+
|
|
123
|
+
if len(payload) < MIN_SIGNED_EXTRINSIC or payload[0] & 0x80 == 0:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
offset = SIGNED_HEADER_LEN
|
|
127
|
+
if payload[offset] != 0x00:
|
|
128
|
+
offset += 2
|
|
129
|
+
else:
|
|
130
|
+
offset += 1
|
|
131
|
+
|
|
132
|
+
nonce = decode_compact(payload[offset:])
|
|
133
|
+
if nonce is None:
|
|
134
|
+
return None
|
|
135
|
+
offset += nonce[1]
|
|
136
|
+
|
|
137
|
+
tip = decode_compact(payload[offset:])
|
|
138
|
+
if tip is None:
|
|
139
|
+
return None
|
|
140
|
+
offset += tip[1]
|
|
141
|
+
|
|
142
|
+
offset += 1
|
|
143
|
+
|
|
144
|
+
if offset + 2 > len(payload):
|
|
145
|
+
return None
|
|
146
|
+
pallet = payload[offset]
|
|
147
|
+
call = payload[offset + 1]
|
|
148
|
+
offset += 2
|
|
149
|
+
|
|
150
|
+
return ExtractedCall(
|
|
151
|
+
pallet=pallet_idx_from_int(pallet),
|
|
152
|
+
call=call_idx_from_int(call),
|
|
153
|
+
args=call_args_from_bytes(payload[offset:]),
|
|
154
|
+
)
|