async-mega-py 2.5.1__tar.gz → 2.5.2__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.
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/PKG-INFO +1 -1
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/pyproject.toml +1 -1
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/pyproject.toml.orig +1 -1
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/core.py +14 -5
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/data_structures.py +9 -7
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/errors.py +7 -1
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/filesystem.py +15 -7
- async_mega_py-2.5.2/src/mega/vault.py +103 -0
- async_mega_py-2.5.1/src/mega/vault.py +0 -85
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/LICENSE +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/README.md +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/__init__.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/__main__.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/api.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/auth.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/chunker.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/client.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/crypto.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/download.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/env.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/progress/__init__.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/progress/rich_progress.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/py.typed +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/transfer_it.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/upload.py +0 -0
- {async_mega_py-2.5.1 → async_mega_py-2.5.2}/src/mega/utils.py +0 -0
|
@@ -23,7 +23,6 @@ from mega.crypto import (
|
|
|
23
23
|
encrypt_key,
|
|
24
24
|
)
|
|
25
25
|
from mega.data_structures import (
|
|
26
|
-
_EMPTY_ATTRS,
|
|
27
26
|
AccountStatsSerialized,
|
|
28
27
|
Attributes,
|
|
29
28
|
Crypto,
|
|
@@ -235,9 +234,19 @@ class MegaCore:
|
|
|
235
234
|
def decrypt(self, node: Node) -> Node:
|
|
236
235
|
crypto = attributes = None
|
|
237
236
|
if node.type in (NodeType.FILE, NodeType.FOLDER):
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
possible_keys = self.vault.get_keys(node)
|
|
238
|
+
if len(possible_keys) > 1:
|
|
239
|
+
logger.warning(
|
|
240
|
+
"Found multiple keys for node %s (%s keys), trying all of them", node.id, len(possible_keys)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
for key in possible_keys:
|
|
244
|
+
crypto = Crypto.decompose(key.full_key, node.type, key.share_key)
|
|
245
|
+
attributes = self._decrypt_attrs(node._a, crypto.key)
|
|
246
|
+
if not attributes.is_error():
|
|
247
|
+
break
|
|
248
|
+
else:
|
|
249
|
+
logger.warning("Unable to decrypt attributes of node %s", node.id)
|
|
241
250
|
|
|
242
251
|
else:
|
|
243
252
|
name = {
|
|
@@ -419,7 +428,7 @@ class MegaCore:
|
|
|
419
428
|
@classmethod
|
|
420
429
|
def decrypt_attrs(cls, attrs: str, key: tuple[int, ...], node_id: NodeID) -> Attributes:
|
|
421
430
|
attributes = cls._decrypt_attrs(attrs, key)
|
|
422
|
-
if attributes
|
|
431
|
+
if attributes.is_error():
|
|
423
432
|
logger.warning("Unable to decrypt attributes of node %s", node_id)
|
|
424
433
|
return attributes
|
|
425
434
|
|
|
@@ -13,7 +13,7 @@ import dataclasses
|
|
|
13
13
|
import time
|
|
14
14
|
from enum import IntEnum
|
|
15
15
|
from types import MappingProxyType
|
|
16
|
-
from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, Self, TypeAlias, TypedDict
|
|
16
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, Self, TypeAlias, TypedDict, final
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from collections.abc import Mapping
|
|
@@ -236,9 +236,6 @@ class Crypto(_DictDumper):
|
|
|
236
236
|
)
|
|
237
237
|
assert iv
|
|
238
238
|
assert meta_mac
|
|
239
|
-
else:
|
|
240
|
-
if len(composed_key) != 4:
|
|
241
|
-
raise RuntimeError(f"Invalid key, expected key len for folders is 4, got {len(composed_key) = }")
|
|
242
239
|
|
|
243
240
|
return Crypto(key, iv, meta_mac, composed_key, share_key) # pyright: ignore[reportArgumentType]
|
|
244
241
|
|
|
@@ -329,17 +326,22 @@ class Node(_DictDumper):
|
|
|
329
326
|
_LABELS: Final = "", "red", "orange", "yellow", "green", "blue", "purple", "grey"
|
|
330
327
|
|
|
331
328
|
|
|
329
|
+
@final
|
|
332
330
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
333
331
|
class Attributes(_DictDumper):
|
|
334
332
|
name: str
|
|
335
333
|
label: str = ""
|
|
336
334
|
favorited: bool = False
|
|
337
335
|
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
def is_error(self) -> bool:
|
|
337
|
+
return self is _EMPTY_ATTRS
|
|
338
|
+
|
|
339
|
+
@staticmethod
|
|
340
|
+
def parse(attrs: AttributesSerialized) -> Attributes:
|
|
340
341
|
if not attrs:
|
|
341
342
|
return _EMPTY_ATTRS
|
|
342
|
-
|
|
343
|
+
|
|
344
|
+
return Attributes(
|
|
343
345
|
name=attrs.get("n", ""),
|
|
344
346
|
label=_LABELS[attrs.get("lbl", 0)],
|
|
345
347
|
favorited=bool(attrs.get("fav")),
|
|
@@ -3,12 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Final
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
|
-
from mega.data_structures import NodeID
|
|
6
|
+
from mega.data_structures import Node, NodeID
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class MegaNzError(Exception): ...
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
class FileSystemError(MegaNzError, RuntimeError):
|
|
13
|
+
def __init__(self, nodes: tuple[Node, ...]) -> None:
|
|
14
|
+
super().__init__("Unable to decript attribute of some nodes in the file system")
|
|
15
|
+
self.nodes = nodes
|
|
16
|
+
|
|
17
|
+
|
|
12
18
|
class ValidationError(MegaNzError, ValueError):
|
|
13
19
|
"""Error in validation stage"""
|
|
14
20
|
|
|
@@ -12,7 +12,7 @@ from types import MappingProxyType
|
|
|
12
12
|
from typing import TYPE_CHECKING, Any, Final, NamedTuple, Self
|
|
13
13
|
|
|
14
14
|
from mega.data_structures import Node, NodeID, NodeType, _DictDumper
|
|
15
|
-
from mega.errors import MultipleNodesFoundError, ValidationError
|
|
15
|
+
from mega.errors import FileSystemError, MultipleNodesFoundError, ValidationError
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
18
|
from collections.abc import Generator, Iterable, Iterator
|
|
@@ -115,7 +115,7 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
|
|
|
115
115
|
yield from self.iterdir(self.trash_bin.id)
|
|
116
116
|
|
|
117
117
|
@classmethod
|
|
118
|
-
def build(cls, nodes: Iterable[Node]) -> Self:
|
|
118
|
+
def build(cls, nodes: Iterable[Node], *, strict: bool = False) -> Self:
|
|
119
119
|
root = inbox = trash_bin = None
|
|
120
120
|
file_count = folder_count = 0
|
|
121
121
|
|
|
@@ -127,7 +127,12 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
|
|
|
127
127
|
ids = current.id, found.id
|
|
128
128
|
raise ValidationError(f"Multiple nodes of type {found.type.name} found: {ids}")
|
|
129
129
|
|
|
130
|
+
decrypt_errors: list[Node] = []
|
|
130
131
|
for node in nodes:
|
|
132
|
+
if node.attributes.is_error() and strict:
|
|
133
|
+
decrypt_errors.append(node)
|
|
134
|
+
continue
|
|
135
|
+
|
|
131
136
|
nodes_map[node.id] = node
|
|
132
137
|
if node.parent_id:
|
|
133
138
|
children.setdefault(node.parent_id, []).append(node.id)
|
|
@@ -148,6 +153,9 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
|
|
|
148
153
|
case _:
|
|
149
154
|
raise RuntimeError # pyright: ignore[reportUnreachable]
|
|
150
155
|
|
|
156
|
+
if decrypt_errors:
|
|
157
|
+
raise FileSystemError(nodes=tuple(decrypt_errors))
|
|
158
|
+
|
|
151
159
|
return cls(
|
|
152
160
|
root=root,
|
|
153
161
|
inbox=inbox,
|
|
@@ -170,8 +178,8 @@ class SimpleFileSystem(_NodeWalker, _DictDumper):
|
|
|
170
178
|
)
|
|
171
179
|
|
|
172
180
|
@classmethod
|
|
173
|
-
def from_dump(cls, dump: dict[str, Any],
|
|
174
|
-
return cls.build(Node.from_dump(node) for node in dump["nodes"].values())
|
|
181
|
+
def from_dump(cls, dump: dict[str, Any], /, *, strict: bool = False) -> Self:
|
|
182
|
+
return cls.build((Node.from_dump(node) for node in dump["nodes"].values()), strict=strict)
|
|
175
183
|
|
|
176
184
|
|
|
177
185
|
@dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True, repr=False)
|
|
@@ -193,7 +201,7 @@ class FileSystem(SimpleFileSystem):
|
|
|
193
201
|
_deleted: frozenset[NodeID]
|
|
194
202
|
|
|
195
203
|
@classmethod
|
|
196
|
-
def build(cls, nodes: Iterable[Node]) -> Self:
|
|
204
|
+
def build(cls, nodes: Iterable[Node], *, strict: bool = False) -> Self:
|
|
197
205
|
# This is really expensive
|
|
198
206
|
# We do 5 loops over all nodes:
|
|
199
207
|
# SimpleFileSystem:
|
|
@@ -205,14 +213,14 @@ class FileSystem(SimpleFileSystem):
|
|
|
205
213
|
# - 4. Sort their paths
|
|
206
214
|
# - 5. Freeze inv paths (list -> tuple)
|
|
207
215
|
|
|
208
|
-
self = SimpleFileSystem.build(nodes)
|
|
216
|
+
self = SimpleFileSystem.build(nodes, strict=strict)
|
|
209
217
|
|
|
210
218
|
roots = list(filter(None, (self.root, self.inbox, self.trash_bin))) or [next(iter(self.nodes.values()))]
|
|
211
219
|
paths: dict[NodeID, PurePosixPath] = {}
|
|
212
220
|
inv_paths: dict[PurePosixPath, list[NodeID]] = {}
|
|
213
221
|
deleted_ids: set[NodeID] = set()
|
|
214
222
|
|
|
215
|
-
for node_id, path, was_deleted in sorted(_resolve_paths(self, *roots), key=lambda x: str(x
|
|
223
|
+
for node_id, path, was_deleted in sorted(_resolve_paths(self, *roots), key=lambda x: str(x.path).casefold()):
|
|
216
224
|
paths[node_id] = path
|
|
217
225
|
inv_paths.setdefault(path, []).append(node_id)
|
|
218
226
|
if was_deleted:
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import logging
|
|
5
|
+
from typing import TYPE_CHECKING, Final, NamedTuple
|
|
6
|
+
|
|
7
|
+
from mega.crypto import b64_to_a32, decrypt_key
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Generator
|
|
11
|
+
|
|
12
|
+
from mega.data_structures import ComposedFileKey, GetNodesResponse, Node, NodeID, NodeKey, SharedKeys, UserID
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
_EXPORTED: Final = "EXP"
|
|
16
|
+
# An special owner used for exported (AKA public) file/folders
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KeyLookup(NamedTuple):
|
|
20
|
+
full_key: ComposedFileKey | NodeKey
|
|
21
|
+
share_key: NodeKey | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclasses.dataclass(slots=True, frozen=True)
|
|
25
|
+
class MegaVault:
|
|
26
|
+
master_key: tuple[int, ...] = ()
|
|
27
|
+
|
|
28
|
+
_shared_keys: dict[UserID, SharedKeys] = dataclasses.field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
_ = self._shared_keys.setdefault(_EXPORTED, {})
|
|
32
|
+
|
|
33
|
+
def update(self, api_resp: GetNodesResponse) -> None:
|
|
34
|
+
"""Parse and update keys not associated with an user"""
|
|
35
|
+
|
|
36
|
+
new_keys: SharedKeys = {}
|
|
37
|
+
for share_key in api_resp["ok"]:
|
|
38
|
+
node_id, key = share_key["h"], share_key["k"]
|
|
39
|
+
new_keys[node_id] = decrypt_key(b64_to_a32(key), self.master_key)
|
|
40
|
+
|
|
41
|
+
for share_target in api_resp["s"]:
|
|
42
|
+
node_id, owner = share_target["h"], share_target["u"]
|
|
43
|
+
if key := new_keys.get(node_id):
|
|
44
|
+
self._shared_keys.setdefault(owner, {})[node_id] = key
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, node: Node) -> KeyLookup:
|
|
47
|
+
keys = self.get_keys(node)
|
|
48
|
+
if len(keys) > 1:
|
|
49
|
+
logger.warning("Found multiple keys for node %s, using latest one", node.id)
|
|
50
|
+
return keys[-1]
|
|
51
|
+
|
|
52
|
+
def _get_keys(self, node: Node) -> Generator[KeyLookup]:
|
|
53
|
+
def cast_key(full_key: tuple[int, ...], share_key: tuple[int, ...] | None):
|
|
54
|
+
if len(full_key) not in (4, 8):
|
|
55
|
+
raise RuntimeError(f"Invalid key len found for node {node.id} ({len(full_key)})")
|
|
56
|
+
|
|
57
|
+
return KeyLookup(full_key, share_key) # pyright: ignore[reportArgumentType]
|
|
58
|
+
|
|
59
|
+
# my files/folders
|
|
60
|
+
if node.owner in node.keys:
|
|
61
|
+
full_key = decrypt_key(b64_to_a32(node.keys[node.owner]), self.master_key)
|
|
62
|
+
yield cast_key(full_key, None)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# folders shared with me
|
|
66
|
+
if node.share_owner and node.share_key and node.id in node.keys:
|
|
67
|
+
share_key = decrypt_key(b64_to_a32(node.share_key), self.master_key)
|
|
68
|
+
full_key = decrypt_key(b64_to_a32(node.keys[node.id]), share_key)
|
|
69
|
+
self._shared_keys.setdefault(node.share_owner, {})[node.id] = share_key
|
|
70
|
+
yield cast_key(full_key, share_key)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# files shared with me
|
|
74
|
+
if node.owner in self._shared_keys:
|
|
75
|
+
shared_keys = self._shared_keys[node.owner]
|
|
76
|
+
for node_id, encrypted_key in node.keys.items():
|
|
77
|
+
try:
|
|
78
|
+
share_key = shared_keys[node_id]
|
|
79
|
+
except KeyError:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
full_key = decrypt_key(b64_to_a32(encrypted_key), share_key)
|
|
83
|
+
yield cast_key(full_key, share_key)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# public files/folders
|
|
87
|
+
if share_key := self._shared_keys[_EXPORTED].get(node.id):
|
|
88
|
+
exported_nodes = self._shared_keys[_EXPORTED].keys()
|
|
89
|
+
for node_id, encrypted_key in node.keys.items():
|
|
90
|
+
if node_id not in exported_nodes:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
full_key = decrypt_key(b64_to_a32(encrypted_key), share_key)
|
|
94
|
+
yield cast_key(full_key, share_key)
|
|
95
|
+
|
|
96
|
+
def get_keys(self, node: Node) -> list[KeyLookup]:
|
|
97
|
+
keys = list(self._get_keys(node))
|
|
98
|
+
if len(keys) == 0:
|
|
99
|
+
raise RuntimeError(f"We do not have keys for {node = }")
|
|
100
|
+
return keys
|
|
101
|
+
|
|
102
|
+
def save_public_key(self, node_id: NodeID, share_key: tuple[int, ...]) -> None:
|
|
103
|
+
self._shared_keys[_EXPORTED][node_id] = share_key
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import dataclasses
|
|
4
|
-
from typing import TYPE_CHECKING, Final, NamedTuple, cast
|
|
5
|
-
|
|
6
|
-
from mega.crypto import b64_to_a32, decrypt_key
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from mega.data_structures import (
|
|
10
|
-
ComposedFileKey,
|
|
11
|
-
GetNodesResponse,
|
|
12
|
-
Node,
|
|
13
|
-
NodeID,
|
|
14
|
-
NodeKey,
|
|
15
|
-
SharedKeys,
|
|
16
|
-
UserID,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
_EXPORTED: Final = "EXP"
|
|
21
|
-
# An special owner used for exported (AKA public) file/folders
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class KeyLookup(NamedTuple):
|
|
25
|
-
full_key: ComposedFileKey | NodeKey
|
|
26
|
-
share_key: NodeKey | None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@dataclasses.dataclass(slots=True, frozen=True)
|
|
30
|
-
class MegaVault:
|
|
31
|
-
master_key: tuple[int, ...] = ()
|
|
32
|
-
|
|
33
|
-
_shared_keys: dict[UserID, SharedKeys] = dataclasses.field(default_factory=dict)
|
|
34
|
-
|
|
35
|
-
def __post_init__(self) -> None:
|
|
36
|
-
_ = self._shared_keys.setdefault(_EXPORTED, {})
|
|
37
|
-
|
|
38
|
-
def update(self, api_resp: GetNodesResponse) -> None:
|
|
39
|
-
"""Parse and update keys not associated with an user"""
|
|
40
|
-
|
|
41
|
-
new_keys: SharedKeys = {}
|
|
42
|
-
for share_key in api_resp["ok"]:
|
|
43
|
-
node_id, key = share_key["h"], share_key["k"]
|
|
44
|
-
new_keys[node_id] = decrypt_key(b64_to_a32(key), self.master_key)
|
|
45
|
-
|
|
46
|
-
for share_target in api_resp["s"]:
|
|
47
|
-
node_id, owner = share_target["h"], share_target["u"]
|
|
48
|
-
if key := new_keys.get(node_id):
|
|
49
|
-
self._shared_keys.setdefault(owner, {})[node_id] = key
|
|
50
|
-
|
|
51
|
-
def __getitem__(self, node: Node) -> KeyLookup:
|
|
52
|
-
share_key: tuple[int, ...] | None = None
|
|
53
|
-
|
|
54
|
-
# my files/folders
|
|
55
|
-
if node.owner in node.keys:
|
|
56
|
-
full_key = decrypt_key(b64_to_a32(node.keys[node.owner]), self.master_key)
|
|
57
|
-
|
|
58
|
-
# folders shared with me
|
|
59
|
-
elif node.share_owner and node.share_key and node.id in node.keys:
|
|
60
|
-
share_key = decrypt_key(b64_to_a32(node.share_key), self.master_key)
|
|
61
|
-
full_key = decrypt_key(b64_to_a32(node.keys[node.id]), share_key)
|
|
62
|
-
self._shared_keys.setdefault(node.share_owner, {})[node.id] = share_key
|
|
63
|
-
|
|
64
|
-
# files shared with me
|
|
65
|
-
elif node.owner in self._shared_keys:
|
|
66
|
-
real_node_id = next(node_id for node_id in node.keys if node_id in self._shared_keys[node.owner])
|
|
67
|
-
share_key = self._shared_keys[node.owner][real_node_id]
|
|
68
|
-
full_key = decrypt_key(b64_to_a32(node.keys[real_node_id]), share_key)
|
|
69
|
-
|
|
70
|
-
# public files/folders
|
|
71
|
-
elif share_key := self._shared_keys[_EXPORTED].get(node.id):
|
|
72
|
-
real_node_id = next(node_id for node_id in node.keys if node_id in self._shared_keys[_EXPORTED])
|
|
73
|
-
encrypted_key = b64_to_a32(node.keys[real_node_id])
|
|
74
|
-
full_key = decrypt_key(encrypted_key, share_key)
|
|
75
|
-
|
|
76
|
-
else:
|
|
77
|
-
raise RuntimeError(f"We do not have keys for {node = }")
|
|
78
|
-
|
|
79
|
-
if len(full_key) not in (4, 8):
|
|
80
|
-
raise RuntimeError(f"Invalid key len found for node {node.id} ({len(full_key)})")
|
|
81
|
-
|
|
82
|
-
return KeyLookup(cast("ComposedFileKey | NodeKey", full_key), share_key)
|
|
83
|
-
|
|
84
|
-
def save_public_key(self, node_id: NodeID, share_key: tuple[int, ...]) -> None:
|
|
85
|
-
self._shared_keys[_EXPORTED][node_id] = share_key
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|