revault-api 0.1.0__py3-none-win_amd64.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.
@@ -0,0 +1,409 @@
1
+ """Typed ctypes wrapper for the reVault native binding library."""
2
+ from __future__ import annotations
3
+
4
+ import ctypes
5
+ import os
6
+ import platform
7
+ import sys
8
+ from pathlib import Path
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from typing import Optional
12
+
13
+ from . import revault_bindings_pb2 as messages
14
+
15
+
16
+ class _Buffer(ctypes.Structure):
17
+ _fields_ = [("ptr", ctypes.POINTER(ctypes.c_uint8)), ("len", ctypes.c_size_t)]
18
+
19
+
20
+ class LockboxEntryKind(str, Enum):
21
+ FILE = "file"
22
+ SYMLINK = "symlink"
23
+ DIRECTORY = "directory"
24
+
25
+
26
+ class ProfileGenerationStatus(str, Enum):
27
+ ACTIVE = "active"
28
+ RETIRED = "retired"
29
+ COMPROMISED = "compromised"
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ProfileGeneration:
34
+ index: int
35
+ status: ProfileGenerationStatus
36
+ contact_fingerprint: bytes
37
+ created_at_unix_ms: int
38
+ retired_at_unix_ms: int | None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ProfileHistory:
43
+ name: str
44
+ active_generation: int
45
+ generations: tuple[ProfileGeneration, ...]
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class LockboxEntry:
50
+ path: str
51
+ kind: LockboxEntryKind
52
+ length: int
53
+ permissions: int
54
+
55
+ @classmethod
56
+ def from_protobuf(cls, value: bytes) -> "LockboxEntry":
57
+ message = messages.LockboxEntry.FromString(value)
58
+ return cls(
59
+ path=message.path,
60
+ kind=LockboxEntryKind({1: "file", 2: "symlink", 3: "directory"}[message.kind]),
61
+ length=message.length,
62
+ permissions=message.permissions,
63
+ )
64
+
65
+
66
+ class Lockbox:
67
+ def __init__(self, library: ctypes.CDLL, handle: int):
68
+ self._lib, self._handle = library, ctypes.c_void_p(handle)
69
+
70
+ def __del__(self) -> None:
71
+ if getattr(self, "_handle", None):
72
+ self._lib.lockbox_free(self._handle)
73
+ self._handle = None
74
+
75
+ def add_file(self, path: str, data: bytes, replace: bool = False) -> None:
76
+ encoded = path.encode()
77
+ if not self._lib.lockbox_add_file(self._handle, encoded, len(encoded), data, len(data), replace):
78
+ raise RuntimeError(_error(self._lib))
79
+
80
+ def commit(self) -> None:
81
+ if not self._lib.lockbox_commit(self._handle):
82
+ raise RuntimeError(_error(self._lib))
83
+
84
+ def _bytes(self, result: _Buffer) -> bytes:
85
+ if not result.ptr:
86
+ raise RuntimeError(_error(self._lib))
87
+ try:
88
+ return ctypes.string_at(result.ptr, result.len)
89
+ finally:
90
+ self._lib.buffer_free(result)
91
+
92
+ def add_file_with_permissions(self, path: str, data: bytes, permissions: int, replace: bool = False) -> None:
93
+ encoded = path.encode()
94
+ if not self._lib.lockbox_add_file_with_permissions(self._handle, encoded, len(encoded), data, len(data), permissions, replace):
95
+ raise RuntimeError(_error(self._lib))
96
+
97
+ def create_dir(self, path: str, create_parents: bool = True) -> None:
98
+ encoded = path.encode()
99
+ if not self._lib.lockbox_create_dir(self._handle, encoded, len(encoded), create_parents):
100
+ raise RuntimeError(_error(self._lib))
101
+
102
+ def remove_dir(self, path: str, recursive: bool = False) -> None:
103
+ encoded = path.encode()
104
+ if not self._lib.lockbox_remove_dir(self._handle, encoded, len(encoded), recursive):
105
+ raise RuntimeError(_error(self._lib))
106
+
107
+ def delete(self, path: str) -> None:
108
+ encoded = path.encode()
109
+ if not self._lib.lockbox_delete(self._handle, encoded, len(encoded)):
110
+ raise RuntimeError(_error(self._lib))
111
+
112
+ def rename(self, source: str, destination: str) -> None:
113
+ source_bytes, destination_bytes = source.encode(), destination.encode()
114
+ if not self._lib.lockbox_rename(self._handle, source_bytes, len(source_bytes), destination_bytes, len(destination_bytes)):
115
+ raise RuntimeError(_error(self._lib))
116
+
117
+ def read_range(self, path: str, offset: int, length: int) -> bytes:
118
+ encoded = path.encode()
119
+ return self._bytes(self._lib.lockbox_read_range(self._handle, encoded, len(encoded), offset, length))
120
+
121
+ def list(self, path: str = "/", recursive: bool = False) -> list[LockboxEntry]:
122
+ encoded = path.encode()
123
+ payload = _wire_payload(self._bytes(self._lib.lockbox_list(self._handle, encoded, len(encoded), recursive)))
124
+ result = messages.LockboxEntryList.FromString(payload)
125
+ return [LockboxEntry.from_protobuf(value.SerializeToString()) for value in result.entries]
126
+
127
+ def stat(self, path: str) -> LockboxEntry | None:
128
+ encoded = path.encode()
129
+ result = self._lib.lockbox_stat(self._handle, encoded, len(encoded))
130
+ if not result.ptr:
131
+ return None
132
+ payload = _wire_payload(self._bytes(result))
133
+ entry = messages.OptionalLockboxEntry.FromString(payload)
134
+ return LockboxEntry.from_protobuf(entry.value.SerializeToString()) if entry.HasField("value") else None
135
+
136
+ def set_variable(self, name: str, value: str, secret: bool = False) -> None:
137
+ name_bytes, value_bytes = name.encode(), value.encode()
138
+ if not self._lib.lockbox_set_variable(self._handle, name_bytes, len(name_bytes), value_bytes, len(value_bytes), secret):
139
+ raise RuntimeError(_error(self._lib))
140
+
141
+ def get_variable(self, name: str) -> str:
142
+ encoded = name.encode()
143
+ return self._bytes(self._lib.lockbox_get_variable(self._handle, encoded, len(encoded))).decode()
144
+
145
+ def get_file(self, path: str) -> bytes:
146
+ encoded = path.encode()
147
+ return self._bytes(self._lib.lockbox_get_file(self._handle, encoded, len(encoded)))
148
+
149
+ def move_variables(self, moves: messages.PathMoveList) -> None:
150
+ value = moves.SerializeToString()
151
+ if not self._lib.lockbox_move_variables(self._handle, value, len(value)):
152
+ raise RuntimeError(_error(self._lib))
153
+
154
+ def move_form_records(self, moves: messages.PathMoveList) -> None:
155
+ value = moves.SerializeToString()
156
+ if not self._lib.lockbox_move_form_records(self._handle, value, len(value)):
157
+ raise RuntimeError(_error(self._lib))
158
+
159
+
160
+ class ContactKey:
161
+ def __init__(self, library: ctypes.CDLL, handle: int):
162
+ self._lib, self._handle = library, ctypes.c_void_p(handle)
163
+
164
+ def __del__(self) -> None:
165
+ if getattr(self, "_handle", None):
166
+ self._lib.key_contact_free(self._handle)
167
+ self._handle = None
168
+
169
+ def public_bytes(self) -> bytes:
170
+ return Lockbox._bytes(self, self._lib.key_contact_public(self._handle))
171
+
172
+ def private_record(self) -> bytes:
173
+ return Lockbox._bytes(self, self._lib.key_contact_private(self._handle))
174
+
175
+
176
+ class VaultDirectory:
177
+ def __init__(self, library: ctypes.CDLL, handle: int):
178
+ self._lib, self._handle = library, ctypes.c_void_p(handle)
179
+
180
+ def __del__(self) -> None:
181
+ if getattr(self, "_handle", None):
182
+ self._lib.vault_directory_free(self._handle)
183
+ self._handle = None
184
+
185
+ def list_profile_generations(self, name: str) -> ProfileHistory:
186
+ encoded = name.encode()
187
+ frame = self._lib.vault_directory_list_profile_generations(self._handle, encoded, len(encoded))
188
+ payload = _wire_payload(Lockbox._bytes(self, frame))
189
+ value = messages.ProfileHistory.FromString(payload)
190
+ generations = tuple(
191
+ ProfileGeneration(
192
+ index=generation.index,
193
+ status=ProfileGenerationStatus(generation.status),
194
+ contact_fingerprint=generation.contact_fingerprint,
195
+ created_at_unix_ms=generation.created_at_unix_ms,
196
+ retired_at_unix_ms=generation.retired_at_unix_ms if generation.has_retired_at else None,
197
+ )
198
+ for generation in value.generations
199
+ )
200
+ return ProfileHistory(
201
+ name=value.name,
202
+ active_generation=value.active_generation,
203
+ generations=generations,
204
+ )
205
+
206
+ def list_form_revisions(self, type_id: str) -> messages.FormDefinitionList:
207
+ encoded = type_id.encode()
208
+ return _message(self, self._lib.vault_directory_list_form_revisions(
209
+ self._handle, encoded, len(encoded)), messages.FormDefinitionList)
210
+
211
+
212
+ class ReadOnlyVaultDirectory:
213
+ def __init__(self, library: ctypes.CDLL, handle: int):
214
+ self._lib, self._handle = library, ctypes.c_void_p(handle)
215
+
216
+ def close(self) -> None:
217
+ if self._handle:
218
+ self._lib.vault_read_only_free(self._handle)
219
+ self._handle = None
220
+
221
+ def __del__(self) -> None:
222
+ self.close()
223
+
224
+ def list_profile_names(self) -> messages.StringList:
225
+ return _message(self, self._lib.vault_read_only_list_profile_names(self._handle), messages.StringList)
226
+
227
+ def list_contact_names(self) -> messages.StringList:
228
+ return _message(self, self._lib.vault_read_only_list_contact_names(self._handle), messages.StringList)
229
+
230
+ def list_form_aliases(self) -> messages.StringList:
231
+ return _message(self, self._lib.vault_read_only_list_form_aliases(self._handle), messages.StringList)
232
+
233
+ def list_known_lockboxes(self) -> messages.KnownLockboxList:
234
+ return _message(self, self._lib.vault_read_only_list_known_lockboxes(self._handle), messages.KnownLockboxList)
235
+
236
+
237
+ class Revault:
238
+ """Owned entry point for the native lockbox and vault APIs."""
239
+ def __init__(self, path: Optional[str | Path] = None):
240
+ self.library = load(path)
241
+ self._lib = self.library
242
+
243
+ @property
244
+ def lockbox_format_version(self) -> int:
245
+ return self.library.lockbox_format_version()
246
+
247
+ def probe_lockbox_format_version(self, value: bytes) -> int:
248
+ return self.library.lockbox_probe_format_version(value, len(value))
249
+
250
+ @property
251
+ def current_vault_structure_version(self) -> int:
252
+ return self.library.vault_structure_version_current()
253
+
254
+ def probe_vault_structure_version(self, root: str, password: bytes) -> int:
255
+ encoded = root.encode()
256
+ return self.library.vault_directory_probe_structure_version(encoded, len(encoded), password, len(password))
257
+
258
+ def last_error_details(self) -> messages.ErrorDetails:
259
+ return _message(self, self.library.buffer_last_error_details(), messages.ErrorDetails)
260
+
261
+ def open_read_only_vault(self, root: str, password: bytes) -> ReadOnlyVaultDirectory:
262
+ encoded = root.encode()
263
+ handle = self.library.vault_read_only_open(encoded, len(encoded), password, len(password))
264
+ if not handle: raise RuntimeError(_error(self.library))
265
+ return ReadOnlyVaultDirectory(self.library, handle)
266
+
267
+ def open_default_read_only_vault(self, password: bytes) -> ReadOnlyVaultDirectory:
268
+ handle = self.library.vault_read_only_open_default(password, len(password))
269
+ if not handle: raise RuntimeError(_error(self.library))
270
+ return ReadOnlyVaultDirectory(self.library, handle)
271
+
272
+ def start_agent(self) -> None:
273
+ if not self.library.vault_agent_start(): raise RuntimeError(_error(self.library))
274
+
275
+ def put_vault_unlock_key(self, vault_id: str, key: bytes, ttl_seconds: int) -> None:
276
+ value = vault_id.encode()
277
+ if not self.library.vault_agent_put_vault_unlock_key(value, len(value), key, len(key), ttl_seconds):
278
+ raise RuntimeError(_error(self.library))
279
+
280
+ def get_vault_unlock_key(self, vault_id: str) -> bytes:
281
+ value = vault_id.encode()
282
+ return Lockbox._bytes(self, self.library.vault_agent_get_vault_unlock_key(value, len(value)))
283
+
284
+
285
+ def _error(lib: ctypes.CDLL) -> str:
286
+ return lib.buffer_last_error().decode()
287
+
288
+
289
+ def _wire_payload(frame: bytes) -> bytes:
290
+ if frame[:4] != b"LBWF" or len(frame) < 12:
291
+ raise RuntimeError("invalid binding frame")
292
+ length = int.from_bytes(frame[8:12], "big")
293
+ if len(frame) != 12 + length:
294
+ raise RuntimeError("invalid binding frame length")
295
+ return frame[12:]
296
+
297
+
298
+ def _message(owner: object, frame: _Buffer, message_type: type):
299
+ return message_type.FromString(_wire_payload(Lockbox._bytes(owner, frame)))
300
+
301
+
302
+ def _native_library_path() -> str:
303
+ override = os.environ.get("REVAULT_LIBRARY")
304
+ if override:
305
+ return override
306
+ machine = platform.machine().lower()
307
+ arch = {"amd64": "x86_64", "x86_64": "x86_64", "arm64": "aarch64", "aarch64": "aarch64"}.get(machine)
308
+ os_name = "macos" if sys.platform == "darwin" else "windows" if sys.platform == "win32" else "linux" if sys.platform.startswith("linux") else None
309
+ if arch is None or os_name is None:
310
+ raise RuntimeError(f"reVault does not publish a native library for {sys.platform}/{machine}")
311
+ target = f"{os_name}-{arch}" + ("-gnu" if os_name == "linux" else "-msvc" if os_name == "windows" else "")
312
+ filename = "revault_api.dll" if os_name == "windows" else "librevault_api.dylib" if os_name == "macos" else "librevault_api.so"
313
+ bundled = Path(__file__).resolve().parent / "_native" / target / filename
314
+ if not bundled.is_file():
315
+ raise RuntimeError(
316
+ f"revault-api native carrier is missing for {target}; "
317
+ "set REVAULT_LIBRARY for development"
318
+ )
319
+ return str(bundled)
320
+
321
+
322
+ def load(path: Optional[str | Path] = None) -> ctypes.CDLL:
323
+ """Load the native library and configure its ABI signatures."""
324
+ lib = ctypes.CDLL(str(path or _native_library_path()))
325
+ lib.api_abi_version.argtypes = []
326
+ lib.api_abi_version.restype = ctypes.c_uint32
327
+ if lib.api_abi_version() != 1:
328
+ raise RuntimeError("revault-api native ABI mismatch; expected 1")
329
+ try:
330
+ from .revault_native import configure_native
331
+ except ImportError:
332
+ from revault_native import configure_native
333
+ configure_native(lib, _Buffer)
334
+ lib.lockbox_create.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
335
+ lib.lockbox_create.restype = ctypes.c_void_p
336
+ _configure(lib, "lockbox_add_file", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_bool)
337
+ _configure(lib, "lockbox_add_file_with_permissions", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32, ctypes.c_bool], ctypes.c_bool)
338
+ _configure(lib, "lockbox_commit", [ctypes.c_void_p], ctypes.c_bool)
339
+ _configure(lib, "lockbox_create_dir", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_bool)
340
+ _configure(lib, "lockbox_remove_dir", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_bool)
341
+ _configure(lib, "lockbox_delete", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t], ctypes.c_bool)
342
+ _configure(lib, "lockbox_rename", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t], ctypes.c_bool)
343
+ _configure(lib, "lockbox_get_file", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t], _Buffer)
344
+ _configure(lib, "lockbox_read_range", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_uint64, ctypes.c_uint64], _Buffer)
345
+ _configure(lib, "lockbox_list", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], _Buffer)
346
+ _configure(lib, "lockbox_stat", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t], _Buffer)
347
+ _configure(lib, "lockbox_set_variable", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_bool], ctypes.c_bool)
348
+ _configure(lib, "lockbox_get_variable", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t], _Buffer)
349
+ _configure(lib, "key_contact_generate", [], ctypes.c_void_p)
350
+ _configure(lib, "key_contact_public", [ctypes.c_void_p], _Buffer)
351
+ _configure(lib, "key_contact_private", [ctypes.c_void_p], _Buffer)
352
+ _configure(lib, "key_contact_free", [ctypes.c_void_p], None)
353
+ lib.lockbox_free.argtypes = [ctypes.c_void_p]
354
+ _configure(lib, "vault_directory_list_profile_generations", [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t], _Buffer)
355
+ _configure(lib, "vault_directory_open_or_create", [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t], ctypes.c_void_p)
356
+ _configure(lib, "vault_directory_free", [ctypes.c_void_p], None)
357
+ lib.buffer_free.argtypes = [_Buffer]
358
+ lib.buffer_last_error.restype = ctypes.c_char_p
359
+ return lib
360
+
361
+
362
+ def _configure(lib: ctypes.CDLL, name: str, args: list[object], result: object) -> None:
363
+ function = getattr(lib, name)
364
+ function.argtypes, function.restype = args, result
365
+
366
+
367
+ # The generated facade is imported after the low-level loader and frame helpers
368
+ # are defined so every public native operation has an owned, class-oriented route.
369
+ from .facade import ( # noqa: E402,F401
370
+ Agent,
371
+ AgentActivity,
372
+ ContactKeyPair,
373
+ ContactPublicKey,
374
+ LocalVault,
375
+ Lockbox,
376
+ Platform,
377
+ ReadOnlyVaultDirectory,
378
+ Revault,
379
+ SigningKeyPair,
380
+ SigningPublicKey,
381
+ Vault,
382
+ VaultDirectory,
383
+ WrappedContactKey,
384
+ )
385
+
386
+
387
+ def generate_contact_key(library: Optional[ctypes.CDLL] = None) -> ContactKey:
388
+ lib = library or load()
389
+ handle = lib.key_contact_generate()
390
+ if not handle:
391
+ raise RuntimeError(_error(lib))
392
+ return ContactKey(lib, handle)
393
+
394
+
395
+ def create(key: bytes, library: Optional[ctypes.CDLL] = None) -> Lockbox:
396
+ lib = library or load()
397
+ handle = lib.lockbox_create(key, len(key))
398
+ if not handle:
399
+ raise RuntimeError(_error(lib))
400
+ return Lockbox(lib, handle)
401
+
402
+
403
+ def open_vault_directory(root: str, password: bytes, library: Optional[ctypes.CDLL] = None) -> VaultDirectory:
404
+ lib = library or load()
405
+ encoded = root.encode()
406
+ handle = lib.vault_directory_open_or_create(encoded, len(encoded), password, len(password))
407
+ if not handle:
408
+ raise RuntimeError(_error(lib))
409
+ return VaultDirectory(lib, handle)