python-veracrypt 0.1.0__py3-none-any.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.
veracrypt/__about__.py ADDED
@@ -0,0 +1,7 @@
1
+ __title__ = "python-veracrypt"
2
+ __description__ = "A cross-platform Python wrapper for the VeraCrypt CLI."
3
+ __url__ = "https://github.com/srichs/python-veracrypt"
4
+ __version__ = "0.1.0"
5
+ __author__ = "srichs"
6
+ __author_email__ = "srichs@pm.me"
7
+ __license__ = "GNU GPL v3.0"
veracrypt/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .veracrypt import Encryption, FileSystem, Hash, VeraCrypt, VeraCryptError
2
+
3
+ __all__ = ["Encryption", "FileSystem", "Hash", "VeraCrypt", "VeraCryptError"]
veracrypt/veracrypt.py ADDED
@@ -0,0 +1,608 @@
1
+ """Python wrapper around the VeraCrypt CLI."""
2
+
3
+ import logging
4
+ import os
5
+ import platform
6
+ import subprocess
7
+ from enum import Enum
8
+ from typing import List, Optional, Tuple
9
+
10
+
11
+ class Encryption(Enum):
12
+ """Supported VeraCrypt encryption algorithms."""
13
+
14
+ AES = "AES"
15
+ SERPENT = "Serpent"
16
+ TWOFISH = "Twofish"
17
+ CAMELLIA = "Camellia"
18
+ KUZNYECHIK = "Kuznyechik"
19
+ AES_TWOFISH = "AES(Twofish)"
20
+ TWOFISH_SERPENT = "Twofish(Serpent)"
21
+ SERPENT_AES = "Serpent(AES)"
22
+ SERPENT_TWOFISH_AES = "Serpent(Twofish(AES))"
23
+ AES_SERPENT = "AES(Serpent)"
24
+ KUZNYECHIK_CAMELLIA = "Kuznyechik(Camellia)"
25
+ CAMELLIA_KUZNYECHIK = "Camellia(Kuznyechik)"
26
+
27
+
28
+ class Hash(Enum):
29
+ """Supported VeraCrypt hash algorithms."""
30
+
31
+ SHA256 = "sha-256"
32
+ SHA512 = "sha-512"
33
+ WHIRLPOOL = "whirlpool"
34
+ BLAKE2S = "blake2s"
35
+ RIPEMD160 = "ripemd-160"
36
+ STREEBOG = "streebog"
37
+
38
+
39
+ class FileSystem(Enum):
40
+ """Supported filesystem formats for newly created volumes."""
41
+
42
+ NONE = "None"
43
+ FAT = "FAT"
44
+ EXFAT = "exFAT"
45
+ NTFS = "NTFS"
46
+ EXT2 = "ext2"
47
+ EXT3 = "ext3"
48
+ EXT4 = "ext4"
49
+ HFS = "HFS"
50
+ APFS = "APFS"
51
+
52
+
53
+ class VeraCryptError(RuntimeError):
54
+ """Raised when VeraCrypt operations fail."""
55
+
56
+
57
+ class VeraCrypt(object):
58
+ """Cross-platform wrapper for core VeraCrypt CLI operations.
59
+
60
+ The class wraps the VeraCrypt CLI to create, mount, and dismount volumes on Windows,
61
+ macOS, and Linux systems. Windows uses ``/param value`` arguments and separates the
62
+ mount/dismount tool (``VeraCrypt.exe``) from the format tool
63
+ (``VeraCrypt Format.exe``),
64
+ while Linux/macOS use ``--param value`` arguments against a single executable.
65
+
66
+ Extra CLI options can be supplied to the public methods, but invalid options will
67
+ result in a VeraCrypt CLI error.
68
+
69
+ The :meth:`command` method allows arbitrary command execution. On Windows, set
70
+ ``windows_program="VeraCrypt Format.exe"`` to target the volume creation CLI.
71
+
72
+ **WARNING:** On Windows the VeraCrypt CLI does not accept passwords from stdin. The
73
+ password will be present in the subprocess arguments for the duration of the call.
74
+ The returned ``CompletedProcess.args`` has the password masked for logging safety.
75
+
76
+ :param log_level: Logging level to use. Defaults to ``logging.ERROR``.
77
+ :param log_fmt: Logging format string.
78
+ :param log_datefmt: Date format string used in log messages.
79
+ :param veracrypt_path: Path to the VeraCrypt executable. On Windows, this should be
80
+ the directory containing the executables. On macOS/Linux, this should be the
81
+ full path to the VeraCrypt binary. If ``None``, a platform-specific default is
82
+ discovered.
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ log_level: Optional[int] = logging.ERROR,
88
+ log_fmt: Optional[str] = "%(levelname)s:%(module)s:%(funcName)s:%(message)s",
89
+ log_datefmt: Optional[str] = "%Y-%m-%d %H:%M:%S",
90
+ veracrypt_path: Optional[str] = None,
91
+ ):
92
+ if log_fmt is None:
93
+ log_fmt = "%(levelname)s:%(module)s:%(funcName)s:%(message)s"
94
+ if log_datefmt is None:
95
+ log_datefmt = "%Y-%m-%d %H:%M:%S"
96
+ logging.basicConfig(level=log_level, format=log_fmt, datefmt=log_datefmt)
97
+ self.logger = logging.getLogger("veracrypt.py")
98
+ self.os_name = platform.system()
99
+ self.veracrypt_path = veracrypt_path or self._default_path()
100
+ self.logger.info("Object initialized")
101
+
102
+ def _validate_options(self, options: Optional[List[str]], context: str) -> None:
103
+ """Validate CLI options passed into public methods."""
104
+ if options is None:
105
+ return
106
+ if not isinstance(options, list) or not all(
107
+ isinstance(item, str) for item in options
108
+ ):
109
+ raise ValueError(
110
+ f"{context} options must be a list of strings when provided."
111
+ )
112
+
113
+ def _validate_keyfiles(self, keyfiles: Optional[List[str]], context: str) -> None:
114
+ """Validate keyfile paths passed into public methods."""
115
+ if keyfiles is None:
116
+ return
117
+ if not isinstance(keyfiles, list) or not all(
118
+ isinstance(item, str) for item in keyfiles
119
+ ):
120
+ raise ValueError(
121
+ f"{context} keyfiles must be a list of strings when provided."
122
+ )
123
+ for keyfile in keyfiles:
124
+ self._check_path(keyfile)
125
+
126
+ @staticmethod
127
+ def _validate_size(size: int) -> None:
128
+ """Validate volume size is a positive integer."""
129
+ if not isinstance(size, int) or size <= 0:
130
+ raise ValueError("Volume size must be a positive integer.")
131
+
132
+ @staticmethod
133
+ def _validate_volume_parent_dir(volume_path: str) -> None:
134
+ """Ensure the parent directory for a volume path exists."""
135
+ parent_dir = os.path.dirname(volume_path) or "."
136
+ if not os.path.exists(parent_dir):
137
+ raise VeraCryptError(
138
+ f"The parent directory for {volume_path} does not exist."
139
+ )
140
+
141
+ @staticmethod
142
+ def _mask_password_in_args(args: List[str], password: str, index: int) -> None:
143
+ """Safely mask a password in a command args list."""
144
+ if 0 <= index < len(args):
145
+ args[index] = "*" * len(password)
146
+
147
+ def _default_path(self) -> str:
148
+ """Return the default VeraCrypt CLI path for the current platform."""
149
+ self.logger.debug("Getting default path")
150
+
151
+ if self.os_name == "Windows":
152
+ path = os.path.join("C:\\", "Program Files", "VeraCrypt")
153
+ path1 = os.path.join(path, "VeraCrypt.exe")
154
+ path2 = os.path.join(path, "VeraCrypt Format.exe")
155
+
156
+ if not os.path.exists(path1):
157
+ raise VeraCryptError(f"VeraCrypt.exe not found at {path1}")
158
+ if not os.path.exists(path2):
159
+ raise VeraCryptError(f'"VeraCrypt Format.exe" not found at {path2}')
160
+ elif self.os_name == "Darwin": # macOS
161
+ path = os.path.join(
162
+ "/", "Applications", "VeraCrypt.app", "Contents", "MacOS", "VeraCrypt"
163
+ )
164
+ elif self.os_name == "Linux":
165
+ path = os.path.join("/", "usr", "bin", "veracrypt")
166
+ else:
167
+ raise VeraCryptError("Unsupported Operating System")
168
+
169
+ self._check_path(path)
170
+ self.logger.info(f"VeraCrypt program path found at {path}")
171
+ return path
172
+
173
+ def _check_path(self, path: str) -> bool:
174
+ """Validate that a path exists.
175
+
176
+ :param path: Filesystem path to validate.
177
+ :raises VeraCryptError: If the path does not exist.
178
+ :return: ``True`` when the path exists.
179
+ """
180
+ if os.path.exists(path):
181
+ self.logger.debug(f"Path {path} exists")
182
+ return True
183
+ else:
184
+ raise VeraCryptError(f"The path {path} does not exist")
185
+
186
+ def mount_volume(
187
+ self,
188
+ volume_path: str,
189
+ password: str,
190
+ mount_point: Optional[str] = None,
191
+ options: Optional[List[str]] = None,
192
+ ) -> subprocess.CompletedProcess:
193
+ """Mount a VeraCrypt volume.
194
+
195
+ :param volume_path: Path to the volume file to mount.
196
+ :param password: Password for the volume.
197
+ :param mount_point: Target mount point (drive letter on Windows).
198
+ :param options: Additional CLI options. Options differ by platform.
199
+ :raises VeraCryptError: If the CLI call fails.
200
+ :return: ``subprocess.CompletedProcess`` for the CLI invocation.
201
+ """
202
+ self.logger.debug("Mounting volume")
203
+ self._validate_options(options, "mount_volume")
204
+ self._check_path(volume_path)
205
+ if self.os_name != "Windows" and mount_point:
206
+ self._check_path(mount_point)
207
+
208
+ if self.os_name == "Windows":
209
+ cmd = self._mount_win(volume_path, password, mount_point, options)
210
+ else:
211
+ cmd = self._mount_nix(volume_path, mount_point, options)
212
+ self.logger.debug(f"Command created: {cmd}")
213
+
214
+ try:
215
+ if self.os_name == "Windows":
216
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
217
+ self._mask_password_in_args(result.args, password, 4)
218
+ else:
219
+ result = subprocess.run(
220
+ cmd,
221
+ input=password + "\n",
222
+ capture_output=True,
223
+ text=True,
224
+ check=True,
225
+ )
226
+ self.logger.info(f"Command executed: returned {result.returncode}")
227
+ self.logger.debug(f"{result.stdout}")
228
+ return result
229
+ except subprocess.CalledProcessError as e:
230
+ raise VeraCryptError(f"Error mounting volume: {e.stderr}") from e
231
+
232
+ def _mount_win(
233
+ self,
234
+ volume_path: str,
235
+ password: str,
236
+ mount_point: Optional[str] = None,
237
+ options: Optional[List[str]] = None,
238
+ ) -> List[str]:
239
+ """Build the Windows CLI command for mounting a volume."""
240
+ self.logger.debug("Mounting volume on Windows")
241
+ cmd = [
242
+ os.path.join(self.veracrypt_path, "VeraCrypt.exe"),
243
+ "/volume",
244
+ volume_path,
245
+ "/password",
246
+ password,
247
+ ]
248
+
249
+ if mount_point:
250
+ cmd += ["/letter", mount_point]
251
+
252
+ if options:
253
+ cmd += options
254
+ cmd += ["/quit", "/silent", "/force"]
255
+ self.logger.debug("Mount command generated")
256
+ return cmd
257
+
258
+ def _mount_nix(
259
+ self,
260
+ volume_path: str,
261
+ mount_point: Optional[str] = None,
262
+ options: Optional[List[str]] = None,
263
+ ) -> List[str]:
264
+ """Build the Linux/macOS CLI command for mounting a volume."""
265
+ self.logger.debug("Mounting volume on Linux/MacOS")
266
+ cmd = [
267
+ "sudo",
268
+ self.veracrypt_path,
269
+ "--text",
270
+ "--non-interactive",
271
+ "--mount",
272
+ volume_path,
273
+ ]
274
+
275
+ if mount_point:
276
+ cmd += [mount_point]
277
+
278
+ if options:
279
+ cmd += options
280
+ cmd += ["--stdin", "--force"]
281
+ self.logger.debug("Mount command generated")
282
+ return cmd
283
+
284
+ def dismount_volume(
285
+ self, target: str = "all", options: Optional[List[str]] = None
286
+ ) -> subprocess.CompletedProcess:
287
+ """Dismount a VeraCrypt volume or all mounted volumes.
288
+
289
+ :param target: Mount point to dismount. Use ``"all"`` to dismount all volumes.
290
+ :param options: Additional CLI options.
291
+ :raises VeraCryptError: If the CLI call fails.
292
+ :return: ``subprocess.CompletedProcess`` for the CLI invocation.
293
+ """
294
+ self.logger.debug("Dismounting volume")
295
+ self._validate_options(options, "dismount_volume")
296
+
297
+ if self.os_name == "Windows":
298
+ cmd = self._dismount_win(target, options)
299
+ else:
300
+ cmd = self._dismount_nix(target, options)
301
+ self.logger.debug(f"Command created: {cmd}")
302
+
303
+ try:
304
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
305
+ self.logger.info(f"Command executed: returned {result.returncode}")
306
+ self.logger.debug(f"{result.stdout}")
307
+ return result
308
+ except subprocess.CalledProcessError as e:
309
+ raise VeraCryptError(f"Error dismounting volume: {e.stderr}") from e
310
+
311
+ def _dismount_win(
312
+ self, target: str = "all", options: Optional[List[str]] = None
313
+ ) -> List[str]:
314
+ """Build the Windows CLI command for dismounting volumes."""
315
+ self.logger.debug("Dismounting volume on Windows")
316
+ cmd = [os.path.join(self.veracrypt_path, "VeraCrypt.exe"), "/dismount"]
317
+
318
+ if target != "all":
319
+ cmd.append(target)
320
+
321
+ if options:
322
+ cmd += options
323
+ cmd += ["/quit", "/silent", "/force"]
324
+ self.logger.debug("Dismount command generated")
325
+ return cmd
326
+
327
+ def _dismount_nix(
328
+ self, target: str = "all", options: Optional[List[str]] = None
329
+ ) -> List[str]:
330
+ """Build the Linux/macOS CLI command for dismounting volumes."""
331
+ self.logger.debug("Dismounting volume on Linux/MacOS")
332
+ cmd = ["sudo", self.veracrypt_path, "--text", "--non-interactive", "--unmount"]
333
+
334
+ if target != "all":
335
+ self._check_path(target)
336
+ cmd.append(target)
337
+
338
+ if options:
339
+ cmd += options
340
+ self.logger.debug("Dismount command generated")
341
+ return cmd
342
+
343
+ def create_volume(
344
+ self,
345
+ volume_path: str,
346
+ password: str,
347
+ size: int,
348
+ encryption: Encryption = Encryption.AES,
349
+ hash_alg: Hash = Hash.SHA512,
350
+ filesystem: FileSystem = FileSystem.FAT,
351
+ keyfiles: Optional[List[str]] = None,
352
+ hidden: bool = False,
353
+ options: Optional[List[str]] = None,
354
+ ) -> subprocess.CompletedProcess:
355
+ """Create a new VeraCrypt volume.
356
+
357
+ :param volume_path: Destination path for the volume file.
358
+ :param password: Password for the volume.
359
+ :param size: Volume size in bytes.
360
+ :param encryption: Encryption algorithm to use.
361
+ :param hash_alg: Hash algorithm to use.
362
+ :param filesystem: Filesystem format to apply.
363
+ :param keyfiles: Optional keyfiles to include in encryption.
364
+ :param hidden: Whether to create a hidden volume.
365
+ :param options: Additional CLI options.
366
+ :raises VeraCryptError: If the CLI call fails.
367
+ :return: ``subprocess.CompletedProcess`` for the CLI invocation.
368
+ """
369
+ self.logger.debug("Creating volume")
370
+ self._validate_options(options, "create_volume")
371
+ self._validate_keyfiles(keyfiles, "create_volume")
372
+ self._validate_size(size)
373
+ self._validate_volume_parent_dir(volume_path)
374
+
375
+ if self.os_name == "Windows":
376
+ cmd = self._create_win(
377
+ volume_path,
378
+ password,
379
+ size,
380
+ encryption,
381
+ hash_alg,
382
+ filesystem,
383
+ keyfiles,
384
+ options,
385
+ )
386
+ else:
387
+ if self.os_name == "Darwin":
388
+ if not os.path.exists(volume_path):
389
+ with open(volume_path, "w"):
390
+ pass
391
+ cmd = self._create_nix(
392
+ volume_path,
393
+ size,
394
+ encryption,
395
+ hash_alg,
396
+ filesystem,
397
+ keyfiles,
398
+ hidden,
399
+ options,
400
+ )
401
+ self.logger.debug(f"Command created: {cmd}")
402
+
403
+ try:
404
+ result = None
405
+ if self.os_name == "Windows":
406
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
407
+ self._mask_password_in_args(result.args, password, 4)
408
+ else:
409
+ result = subprocess.run(
410
+ cmd,
411
+ input=password + "\n",
412
+ capture_output=True,
413
+ text=True,
414
+ check=True,
415
+ )
416
+ self.logger.info(f"Command executed: returned {result.returncode}")
417
+ self.logger.debug(f"{result.stdout}")
418
+ return result
419
+ except subprocess.CalledProcessError as e:
420
+ raise VeraCryptError(f"Error creating volume: {e.stderr}") from e
421
+
422
+ def _create_win(
423
+ self,
424
+ volume_path: str,
425
+ password: str,
426
+ size: int,
427
+ encryption: Encryption = Encryption.AES,
428
+ hash_alg: Hash = Hash.SHA512,
429
+ filesystem: FileSystem = FileSystem.FAT,
430
+ keyfiles: Optional[List[str]] = None,
431
+ options: Optional[List[str]] = None,
432
+ ) -> List[str]:
433
+ """Build the Windows CLI command for creating a volume."""
434
+ self.logger.debug("Creating volume on Windows")
435
+ cmd = [
436
+ os.path.join(self.veracrypt_path, "VeraCrypt Format.exe"),
437
+ "/create",
438
+ volume_path,
439
+ "/password",
440
+ password,
441
+ "/size",
442
+ f"{size}",
443
+ "/encryption",
444
+ encryption.value,
445
+ "/hash",
446
+ hash_alg.value,
447
+ "/filesystem",
448
+ filesystem.value,
449
+ ]
450
+
451
+ if keyfiles:
452
+ for keyfile in keyfiles:
453
+ cmd += ["/keyfile", keyfile]
454
+
455
+ if options:
456
+ cmd += options
457
+ cmd += ["/protectMemory", "/quick", "/silent", "/force"]
458
+ self.logger.debug("Create command generated")
459
+ return cmd
460
+
461
+ def _create_nix(
462
+ self,
463
+ volume_path: str,
464
+ size: int,
465
+ encryption: Encryption = Encryption.AES,
466
+ hash_alg: Hash = Hash.SHA512,
467
+ filesystem: FileSystem = FileSystem.FAT,
468
+ keyfiles: Optional[List[str]] = None,
469
+ hidden: bool = False,
470
+ options: Optional[List[str]] = None,
471
+ ) -> List[str]:
472
+ """Build the Linux/macOS CLI command for creating a volume."""
473
+ self.logger.debug("Creating volume on Linux/MacOS")
474
+ cmd = [
475
+ "sudo",
476
+ self.veracrypt_path,
477
+ "--text",
478
+ "--non-interactive",
479
+ "--create",
480
+ volume_path,
481
+ "--size",
482
+ f"{size}",
483
+ "--encryption",
484
+ encryption.value,
485
+ "--hash",
486
+ hash_alg.value,
487
+ "--filesystem",
488
+ filesystem.value,
489
+ ]
490
+
491
+ if keyfiles:
492
+ for keyfile in keyfiles:
493
+ cmd += ["--keyfiles", keyfile]
494
+
495
+ if hidden:
496
+ cmd += ["--volume-type", "hidden"]
497
+
498
+ if options:
499
+ cmd += options
500
+ cmd += ["--random-source", "/dev/urandom", "--stdin", "--quick", "--force"]
501
+ self.logger.debug("Create command generated")
502
+ return cmd
503
+
504
+ def command(
505
+ self,
506
+ options: Optional[List[str]] = None,
507
+ windows_program: str = "VeraCrypt.exe",
508
+ ) -> subprocess.CompletedProcess:
509
+ """Call the VeraCrypt CLI with custom options.
510
+
511
+ :param options: Options to pass to the VeraCrypt CLI.
512
+ :param windows_program: Windows-only program name to invoke.
513
+ :raises VeraCryptError: If the CLI call fails.
514
+ :return: ``subprocess.CompletedProcess`` for the CLI invocation.
515
+ """
516
+ self.logger.debug("Calling custom command")
517
+ self._validate_options(options, "command")
518
+ if self.os_name == "Windows":
519
+ cmd = self._custom_win(options, windows_program)
520
+ password, index = self._get_password(cmd)
521
+ else:
522
+ password, index = self._get_password(options)
523
+ cmd = self._custom_nix(options)
524
+ self.logger.debug(f"Command created: {cmd}")
525
+
526
+ try:
527
+ result = None
528
+ if self.os_name == "Windows":
529
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
530
+ if password is not None:
531
+ self.logger.debug("Sanitizing password")
532
+ self._mask_password_in_args(result.args, password, index)
533
+ else:
534
+ if password is not None:
535
+ result = subprocess.run(
536
+ cmd,
537
+ input=password + "\n",
538
+ capture_output=True,
539
+ text=True,
540
+ check=True,
541
+ )
542
+ else:
543
+ result = subprocess.run(
544
+ cmd, capture_output=True, text=True, check=True
545
+ )
546
+ self.logger.info(f"Command executed: returned {result.returncode}")
547
+ self.logger.debug(f"{result.stdout}")
548
+ return result
549
+ except subprocess.CalledProcessError as e:
550
+ raise VeraCryptError(f"Error calling custom command: {e.stderr}") from e
551
+
552
+ def _custom_win(
553
+ self,
554
+ options: Optional[List[str]] = None,
555
+ windows_program: str = "VeraCrypt.exe",
556
+ ) -> List[str]:
557
+ """Build a Windows CLI command using an arbitrary VeraCrypt executable."""
558
+ self.logger.debug("Calling custom command on Windows")
559
+ cmd = [os.path.join(self.veracrypt_path, windows_program)]
560
+
561
+ if options:
562
+ cmd += options
563
+ self.logger.debug("Custom command generated")
564
+ return cmd
565
+
566
+ def _custom_nix(self, options: Optional[List[str]] = None) -> List[str]:
567
+ """Build a Linux/macOS CLI command using provided options."""
568
+ self.logger.debug("Calling custom command on Linux/MacOS")
569
+ options_list = list(options) if options else []
570
+ password, p_index = self._get_password(options_list)
571
+ cmd = ["sudo", self.veracrypt_path]
572
+
573
+ if password and options_list:
574
+ self.logger.debug("Removing password from command line options")
575
+ del options_list[p_index - 1 : p_index + 1]
576
+
577
+ if options_list:
578
+ cmd += options_list
579
+
580
+ if password:
581
+ if "--stdin" not in options_list:
582
+ cmd += ["--stdin"]
583
+ self.logger.debug("Custom command generated")
584
+ return cmd
585
+
586
+ def _get_password(self, cmd: Optional[List[str]]) -> Tuple[Optional[str], int]:
587
+ """Extract a password argument from a command list.
588
+
589
+ :param cmd: Command list or ``None``.
590
+ :return: Tuple of password value and index where it was found.
591
+ """
592
+ if not cmd:
593
+ return None, -1
594
+ pword_option = "--password"
595
+ if self.os_name == "Windows":
596
+ pword_option = "/password"
597
+
598
+ try:
599
+ index = cmd.index(pword_option) + 1
600
+ pword = cmd[index]
601
+ if pword == "":
602
+ raise ValueError("Password option provided without a value.")
603
+ except ValueError:
604
+ pword = None
605
+ index = -1
606
+ except IndexError as exc:
607
+ raise ValueError("Password option provided without a value.") from exc
608
+ return pword, index