rtty-soda 0.3.2__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.
rtty_soda/__init__.py ADDED
File without changes
rtty_soda/archivers.py ADDED
@@ -0,0 +1,59 @@
1
+ from collections.abc import Callable
2
+ from compression import bz2, lzma, zlib, zstd
3
+
4
+ __all__ = ["ARCHIVERS", "UNARCHIVERS", "Archiver"]
5
+
6
+
7
+ type Archiver = Callable[[bytes], bytes]
8
+
9
+
10
+ def compress_zstd(data: bytes) -> bytes:
11
+ return zstd.compress(data, level=20)
12
+
13
+
14
+ def compress_zlib(data: bytes) -> bytes:
15
+ return zlib.compress(data, level=9)
16
+
17
+
18
+ def compress_bz2(data: bytes) -> bytes:
19
+ return bz2.compress(data, compresslevel=9)
20
+
21
+
22
+ def compress_lzma(data: bytes) -> bytes:
23
+ return lzma.compress(
24
+ data,
25
+ format=lzma.FORMAT_ALONE,
26
+ check=lzma.CHECK_NONE,
27
+ preset=lzma.PRESET_EXTREME,
28
+ )
29
+
30
+
31
+ def decompress_zstd(data: bytes) -> bytes:
32
+ return zstd.decompress(data)
33
+
34
+
35
+ def decompress_zlib(data: bytes) -> bytes:
36
+ return zlib.decompress(data)
37
+
38
+
39
+ def decompress_bz2(data: bytes) -> bytes:
40
+ return bz2.decompress(data)
41
+
42
+
43
+ def decompress_lzma(data: bytes) -> bytes:
44
+ return lzma.decompress(data, format=lzma.FORMAT_ALONE)
45
+
46
+
47
+ ARCHIVERS: dict[str, Archiver] = {
48
+ "zstd": compress_zstd,
49
+ "zlib": compress_zlib,
50
+ "bz2": compress_bz2,
51
+ "lzma": compress_lzma,
52
+ }
53
+
54
+ UNARCHIVERS: dict[str, Archiver] = {
55
+ "zstd": decompress_zstd,
56
+ "zlib": decompress_zlib,
57
+ "bz2": decompress_bz2,
58
+ "lzma": decompress_lzma,
59
+ }
@@ -0,0 +1,6 @@
1
+ from .cli_options import CliOptions
2
+ from .cli_reader import CliReader
3
+ from .cli_writer import CliWriter
4
+ from .main import cli
5
+
6
+ __all__ = ["CliOptions", "CliReader", "CliWriter", "cli"]
@@ -0,0 +1,115 @@
1
+ from pathlib import Path
2
+ from typing import TYPE_CHECKING
3
+
4
+ import click
5
+
6
+ if TYPE_CHECKING:
7
+ from click.decorators import FC
8
+
9
+ __all__ = ["CliOptions", "out_path"]
10
+
11
+ out_path = click.Path(
12
+ file_okay=True, dir_okay=False, writable=True, allow_dash=True, path_type=Path
13
+ )
14
+
15
+
16
+ class CliOptions:
17
+ @staticmethod
18
+ def compression(function: FC) -> FC:
19
+ return click.option(
20
+ "--compression",
21
+ "-c",
22
+ default="zstd",
23
+ show_default=True,
24
+ envvar="COMPRESSION",
25
+ )(function)
26
+
27
+ @staticmethod
28
+ def data_encoding(function: FC) -> FC:
29
+ return click.option(
30
+ "--data-encoding",
31
+ "-e",
32
+ default="base64",
33
+ show_default=True,
34
+ envvar="DATA_ENCODING",
35
+ )(function)
36
+
37
+ @staticmethod
38
+ def key_encoding(function: FC) -> FC:
39
+ return click.option(
40
+ "--key-encoding", default="base64", show_default=True, envvar="KEY_ENCODING"
41
+ )(function)
42
+
43
+ @staticmethod
44
+ def short_key_encoding(function: FC) -> FC:
45
+ return click.option(
46
+ "--encoding",
47
+ "-e",
48
+ default="base64",
49
+ show_default=True,
50
+ envvar="KEY_ENCODING",
51
+ )(function)
52
+
53
+ @staticmethod
54
+ def kdf_profile(function: FC) -> FC:
55
+ return click.option(
56
+ "--kdf-profile",
57
+ "-p",
58
+ default="sensitive",
59
+ show_default=True,
60
+ envvar="KDF_PROFILE",
61
+ )(function)
62
+
63
+ @staticmethod
64
+ def short_kdf_profile(function: FC) -> FC:
65
+ return click.option(
66
+ "--profile",
67
+ "-p",
68
+ default="sensitive",
69
+ show_default=True,
70
+ envvar="KDF_PROFILE",
71
+ )(function)
72
+
73
+ @staticmethod
74
+ def verbose(function: FC) -> FC:
75
+ return click.option(
76
+ "--verbose",
77
+ "-v",
78
+ is_flag=True,
79
+ envvar="VERBOSE",
80
+ help="Show verbose output.",
81
+ )(function)
82
+
83
+ @staticmethod
84
+ def text(function: FC) -> FC:
85
+ return click.option(
86
+ "--text",
87
+ "-t",
88
+ is_flag=True,
89
+ envvar="TEXT",
90
+ help="Treat message as text (binary if not specified).",
91
+ )(function)
92
+
93
+ @staticmethod
94
+ def group_len(function: FC) -> FC:
95
+ return click.option(
96
+ "--group-len", default=0, show_default=True, envvar="GROUP_LEN"
97
+ )(function)
98
+
99
+ @staticmethod
100
+ def line_len(function: FC) -> FC:
101
+ return click.option(
102
+ "--line-len", default=80, show_default=True, envvar="LINE_LEN"
103
+ )(function)
104
+
105
+ @staticmethod
106
+ def output_file(function: FC) -> FC:
107
+ return click.option(
108
+ "--output-file", "-o", type=out_path, help="Write output to file."
109
+ )(function)
110
+
111
+ @staticmethod
112
+ def padding(function: FC) -> FC:
113
+ return click.option(
114
+ "--padding", default=0, show_default=True, envvar="PADDING"
115
+ )(function)
@@ -0,0 +1,25 @@
1
+ from typing import TYPE_CHECKING, BinaryIO, TextIO, cast
2
+
3
+ from rtty_soda.interfaces import Reader
4
+
5
+ if TYPE_CHECKING:
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ __all__ = ["CliReader"]
11
+
12
+
13
+ class CliReader(Reader):
14
+ def __init__(self, source: Path) -> None:
15
+ self.source = source
16
+
17
+ def read_str(self) -> str:
18
+ with click.open_file(
19
+ self.source, mode="rt", encoding="utf-8", errors="strict"
20
+ ) as fd:
21
+ return cast("TextIO", fd).read()
22
+
23
+ def read_bytes(self) -> bytes:
24
+ with click.open_file(self.source, mode="rb") as fd:
25
+ return cast("BinaryIO", fd).read()
@@ -0,0 +1,41 @@
1
+ import random
2
+ import string
3
+ from typing import TYPE_CHECKING
4
+
5
+ from rtty_soda.interfaces import Writer
6
+
7
+ if TYPE_CHECKING:
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ __all__ = ["CliWriter"]
13
+
14
+
15
+ class CliWriter(Writer):
16
+ def __init__(self, target: Path | None) -> None:
17
+ self.target = target
18
+
19
+ @staticmethod
20
+ def write_bytes_atomic(target: Path, data: bytes) -> None:
21
+ temp_name = "".join(random.choices(string.ascii_lowercase, k=10)) # noqa: S311
22
+ temp_path = target.parent / temp_name
23
+ temp_path.write_bytes(data)
24
+ temp_path.replace(target)
25
+
26
+ def write_bytes(self, data: bytes) -> None:
27
+ if self.target is None or self.target.stem == "-":
28
+ add_nl = not data.endswith(b"\n")
29
+ click.echo(data, nl=add_nl)
30
+ else:
31
+ if self.target.exists():
32
+ click.confirm(
33
+ f"Overwrite the output file? ({self.target})",
34
+ default=False,
35
+ abort=True,
36
+ )
37
+
38
+ self.write_bytes_atomic(self.target, data)
39
+
40
+ def write_diag(self, message: str) -> None:
41
+ click.echo(message, err=True)
rtty_soda/cli/main.py ADDED
@@ -0,0 +1,417 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+ from click_aliases import ClickAliasedGroup
5
+
6
+ from rtty_soda.cli import CliOptions, CliReader, CliWriter
7
+ from rtty_soda.formatters import FixedFormatter
8
+ from rtty_soda.services import EncodingService, EncryptionService, KeyService
9
+
10
+ __all__ = ["cli", "in_path"]
11
+
12
+ in_path = click.Path(
13
+ exists=True,
14
+ file_okay=True,
15
+ dir_okay=False,
16
+ readable=True,
17
+ allow_dash=True,
18
+ path_type=Path,
19
+ )
20
+
21
+ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
22
+
23
+
24
+ @click.group(cls=ClickAliasedGroup, context_settings=CONTEXT_SETTINGS)
25
+ @click.version_option(package_name="rtty-soda")
26
+ def cli() -> None:
27
+ pass
28
+
29
+
30
+ @cli.command() # pyright: ignore[reportAny]
31
+ @CliOptions.short_key_encoding
32
+ @CliOptions.output_file
33
+ @CliOptions.group_len
34
+ @CliOptions.line_len
35
+ @CliOptions.padding
36
+ @CliOptions.verbose
37
+ def genkey_cmd(
38
+ encoding: str,
39
+ output_file: Path | None,
40
+ group_len: int,
41
+ line_len: int,
42
+ padding: int,
43
+ verbose: bool,
44
+ ) -> None:
45
+ """Generate Private Key.
46
+
47
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
48
+ """
49
+ formatter = FixedFormatter(group_len, line_len, padding)
50
+ writer = CliWriter(output_file)
51
+ service = KeyService(encoding, formatter, writer, verbose)
52
+ service.genkey()
53
+
54
+
55
+ @cli.command() # pyright: ignore[reportAny]
56
+ @click.argument("private_key_file", type=in_path)
57
+ @CliOptions.short_key_encoding
58
+ @CliOptions.output_file
59
+ @CliOptions.group_len
60
+ @CliOptions.line_len
61
+ @CliOptions.padding
62
+ @CliOptions.verbose
63
+ def pubkey_cmd(
64
+ private_key_file: Path,
65
+ encoding: str,
66
+ output_file: Path | None,
67
+ group_len: int,
68
+ line_len: int,
69
+ padding: int,
70
+ verbose: bool,
71
+ ) -> None:
72
+ """Get Public Key.
73
+
74
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
75
+ """
76
+ formatter = FixedFormatter(group_len, line_len, padding)
77
+ writer = CliWriter(output_file)
78
+ service = KeyService(encoding, formatter, writer, verbose)
79
+ priv = CliReader(private_key_file)
80
+ service.pubkey(priv)
81
+
82
+
83
+ @cli.command() # pyright: ignore[reportAny]
84
+ @click.argument("password_file", type=in_path)
85
+ @CliOptions.short_key_encoding
86
+ @CliOptions.short_kdf_profile
87
+ @CliOptions.output_file
88
+ @CliOptions.group_len
89
+ @CliOptions.line_len
90
+ @CliOptions.padding
91
+ @CliOptions.verbose
92
+ def kdf_cmd(
93
+ password_file: Path,
94
+ encoding: str,
95
+ profile: str,
96
+ output_file: Path | None,
97
+ group_len: int,
98
+ line_len: int,
99
+ padding: int,
100
+ verbose: bool,
101
+ ) -> None:
102
+ """Key Derivation Function.
103
+
104
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
105
+
106
+ Profile: interactive | moderate | sensitive
107
+ """
108
+ formatter = FixedFormatter(group_len, line_len, padding)
109
+ writer = CliWriter(output_file)
110
+ service = KeyService(encoding, formatter, writer, verbose)
111
+ password = CliReader(password_file)
112
+ service.kdf(password=password, kdf_profile=profile)
113
+
114
+
115
+ @cli.command(aliases=["e"]) # pyright: ignore[reportAny]
116
+ @click.argument("private_key_file", type=in_path)
117
+ @click.argument("public_key_file", type=in_path)
118
+ @click.argument("message_file", type=in_path)
119
+ @CliOptions.text
120
+ @CliOptions.key_encoding
121
+ @CliOptions.data_encoding
122
+ @CliOptions.compression
123
+ @CliOptions.output_file
124
+ @CliOptions.group_len
125
+ @CliOptions.line_len
126
+ @CliOptions.padding
127
+ @CliOptions.verbose
128
+ def encrypt_public_cmd(
129
+ private_key_file: Path,
130
+ public_key_file: Path,
131
+ message_file: Path,
132
+ text: bool,
133
+ key_encoding: str,
134
+ data_encoding: str,
135
+ compression: str,
136
+ output_file: Path | None,
137
+ group_len: int,
138
+ line_len: int,
139
+ padding: int,
140
+ verbose: bool,
141
+ ) -> None:
142
+ """Encrypt Message (Public).
143
+
144
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
145
+
146
+ Compression: zstd | zlib | bz2 | lzma | raw
147
+ """
148
+ formatter = FixedFormatter(group_len, line_len, padding)
149
+ writer = CliWriter(output_file)
150
+ service = EncryptionService(
151
+ text_mode=text,
152
+ key_encoding=key_encoding,
153
+ data_encoding=data_encoding,
154
+ compression=compression,
155
+ formatter=formatter,
156
+ writer=writer,
157
+ verbose=verbose,
158
+ )
159
+ priv = CliReader(private_key_file)
160
+ pub = CliReader(public_key_file)
161
+ message = CliReader(message_file)
162
+ service.encrypt_public(private_key=priv, public_key=pub, message=message)
163
+
164
+
165
+ @cli.command(aliases=["es"]) # pyright: ignore[reportAny]
166
+ @click.argument("key_file", type=in_path)
167
+ @click.argument("message_file", type=in_path)
168
+ @CliOptions.text
169
+ @CliOptions.key_encoding
170
+ @CliOptions.data_encoding
171
+ @CliOptions.compression
172
+ @CliOptions.output_file
173
+ @CliOptions.group_len
174
+ @CliOptions.line_len
175
+ @CliOptions.padding
176
+ @CliOptions.verbose
177
+ def encrypt_secret_cmd(
178
+ key_file: Path,
179
+ message_file: Path,
180
+ text: bool,
181
+ key_encoding: str,
182
+ data_encoding: str,
183
+ compression: str,
184
+ output_file: Path | None,
185
+ group_len: int,
186
+ line_len: int,
187
+ padding: int,
188
+ verbose: bool,
189
+ ) -> None:
190
+ """Encrypt Message (Secret).
191
+
192
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
193
+
194
+ Compression: zstd | zlib | bz2 | lzma | raw
195
+ """
196
+ formatter = FixedFormatter(group_len, line_len, padding)
197
+ writer = CliWriter(output_file)
198
+ service = EncryptionService(
199
+ text_mode=text,
200
+ key_encoding=key_encoding,
201
+ data_encoding=data_encoding,
202
+ compression=compression,
203
+ formatter=formatter,
204
+ writer=writer,
205
+ verbose=verbose,
206
+ )
207
+ key = CliReader(key_file)
208
+ message = CliReader(message_file)
209
+ service.encrypt_secret(key, message)
210
+
211
+
212
+ @cli.command(aliases=["ep"]) # pyright: ignore[reportAny]
213
+ @click.argument("password_file", type=in_path)
214
+ @click.argument("message_file", type=in_path)
215
+ @CliOptions.text
216
+ @CliOptions.kdf_profile
217
+ @CliOptions.data_encoding
218
+ @CliOptions.compression
219
+ @CliOptions.output_file
220
+ @CliOptions.group_len
221
+ @CliOptions.line_len
222
+ @CliOptions.padding
223
+ @CliOptions.verbose
224
+ def encrypt_password_cmd(
225
+ password_file: Path,
226
+ message_file: Path,
227
+ text: bool,
228
+ kdf_profile: str,
229
+ data_encoding: str,
230
+ compression: str,
231
+ output_file: Path | None,
232
+ group_len: int,
233
+ line_len: int,
234
+ padding: int,
235
+ verbose: bool,
236
+ ) -> None:
237
+ """Encrypt Message (Password).
238
+
239
+ KDF profile: interactive | moderate | sensitive
240
+
241
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
242
+
243
+ Compression: zstd | zlib | bz2 | lzma | raw
244
+ """
245
+ formatter = FixedFormatter(group_len, line_len, padding)
246
+ writer = CliWriter(output_file)
247
+ service = EncryptionService(
248
+ text_mode=text,
249
+ key_encoding="binary",
250
+ data_encoding=data_encoding,
251
+ compression=compression,
252
+ formatter=formatter,
253
+ writer=writer,
254
+ verbose=verbose,
255
+ )
256
+ pw = CliReader(password_file)
257
+ message = CliReader(message_file)
258
+ service.encrypt_password(password=pw, message=message, kdf_profile=kdf_profile)
259
+
260
+
261
+ @cli.command(aliases=["d"]) # pyright: ignore[reportAny]
262
+ @click.argument("private_key_file", type=in_path)
263
+ @click.argument("public_key_file", type=in_path)
264
+ @click.argument("message_file", type=in_path)
265
+ @CliOptions.text
266
+ @CliOptions.key_encoding
267
+ @CliOptions.data_encoding
268
+ @CliOptions.compression
269
+ @CliOptions.output_file
270
+ def decrypt_public_cmd(
271
+ private_key_file: Path,
272
+ public_key_file: Path,
273
+ message_file: Path,
274
+ text: bool,
275
+ key_encoding: str,
276
+ data_encoding: str,
277
+ compression: str,
278
+ output_file: Path | None,
279
+ ) -> None:
280
+ """Decrypt Message (Public).
281
+
282
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
283
+
284
+ Compression: zstd | zlib | bz2 | lzma | raw
285
+ """
286
+ writer = CliWriter(output_file)
287
+ service = EncryptionService(
288
+ text_mode=text,
289
+ key_encoding=key_encoding,
290
+ data_encoding=data_encoding,
291
+ compression=compression,
292
+ formatter=None,
293
+ writer=writer,
294
+ verbose=False,
295
+ )
296
+ priv = CliReader(private_key_file)
297
+ pub = CliReader(public_key_file)
298
+ message = CliReader(message_file)
299
+ service.decrypt_public(private_key=priv, public_key=pub, message=message)
300
+
301
+
302
+ @cli.command(aliases=["ds"]) # pyright: ignore[reportAny]
303
+ @click.argument("key_file", type=in_path)
304
+ @click.argument("message_file", type=in_path)
305
+ @CliOptions.text
306
+ @CliOptions.key_encoding
307
+ @CliOptions.data_encoding
308
+ @CliOptions.compression
309
+ @CliOptions.output_file
310
+ def decrypt_secret_cmd(
311
+ key_file: Path,
312
+ message_file: Path,
313
+ text: bool,
314
+ key_encoding: str,
315
+ data_encoding: str,
316
+ compression: str,
317
+ output_file: Path | None,
318
+ ) -> None:
319
+ """Decrypt Message (Secret).
320
+
321
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
322
+
323
+ Compression: zstd | zlib | bz2 | lzma | raw
324
+ """
325
+ writer = CliWriter(output_file)
326
+ service = EncryptionService(
327
+ text_mode=text,
328
+ key_encoding=key_encoding,
329
+ data_encoding=data_encoding,
330
+ compression=compression,
331
+ formatter=None,
332
+ writer=writer,
333
+ verbose=False,
334
+ )
335
+ key = CliReader(key_file)
336
+ message = CliReader(message_file)
337
+ service.decrypt_secret(key, message)
338
+
339
+
340
+ @cli.command(aliases=["dp"]) # pyright: ignore[reportAny]
341
+ @click.argument("password_file", type=in_path)
342
+ @click.argument("message_file", type=in_path)
343
+ @CliOptions.text
344
+ @CliOptions.kdf_profile
345
+ @CliOptions.data_encoding
346
+ @CliOptions.compression
347
+ @CliOptions.output_file
348
+ def decrypt_password_cmd(
349
+ password_file: Path,
350
+ message_file: Path,
351
+ text: bool,
352
+ kdf_profile: str,
353
+ data_encoding: str,
354
+ compression: str,
355
+ output_file: Path | None,
356
+ ) -> None:
357
+ """Decrypt Message (Password).
358
+
359
+ KDF profile: interactive | moderate | sensitive
360
+
361
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
362
+
363
+ Compression: zstd | zlib | bz2 | lzma | raw
364
+ """
365
+ writer = CliWriter(output_file)
366
+ service = EncryptionService(
367
+ text_mode=text,
368
+ key_encoding="binary",
369
+ data_encoding=data_encoding,
370
+ compression=compression,
371
+ formatter=None,
372
+ writer=writer,
373
+ verbose=False,
374
+ )
375
+ pw = CliReader(password_file)
376
+ message = CliReader(message_file)
377
+ service.decrypt_password(password=pw, message=message, kdf_profile=kdf_profile)
378
+
379
+
380
+ @cli.command() # pyright: ignore[reportAny]
381
+ @click.argument("in_encoding")
382
+ @click.argument("out_encoding")
383
+ @click.argument("file", type=in_path)
384
+ @CliOptions.output_file
385
+ @CliOptions.group_len
386
+ @CliOptions.line_len
387
+ @CliOptions.padding
388
+ @CliOptions.verbose
389
+ def encode_cmd(
390
+ in_encoding: str,
391
+ out_encoding: str,
392
+ file: Path,
393
+ output_file: Path | None,
394
+ group_len: int,
395
+ line_len: int,
396
+ padding: int,
397
+ verbose: bool,
398
+ ) -> None:
399
+ """Encode File.
400
+
401
+ Encoding: base26 | base31 | base36 | base64 | base94 | binary
402
+ """
403
+ writer = CliWriter(output_file)
404
+ formatter = FixedFormatter(group_len, line_len, padding)
405
+ service = EncodingService(
406
+ in_encoding=in_encoding,
407
+ out_encoding=out_encoding,
408
+ formatter=formatter,
409
+ writer=writer,
410
+ verbose=verbose,
411
+ )
412
+ data = CliReader(file)
413
+ service.encode(data)
414
+
415
+
416
+ if __name__ == "__main__":
417
+ cli()