yace-codec 0.2.0__tar.gz → 0.2.1__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.
- {yace_codec-0.2.0 → yace_codec-0.2.1}/CHANGELOG.md +18 -0
- {yace_codec-0.2.0/src/yace_codec.egg-info → yace_codec-0.2.1}/PKG-INFO +90 -75
- {yace_codec-0.2.0 → yace_codec-0.2.1}/README.md +88 -73
- {yace_codec-0.2.0 → yace_codec-0.2.1}/pyproject.toml +1 -1
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/__init__.py +2 -2
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/cli.py +31 -28
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/codec.py +48 -34
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/errors.py +1 -1
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/keystore.py +1 -1
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/sealed.py +83 -54
- {yace_codec-0.2.0 → yace_codec-0.2.1/src/yace_codec.egg-info}/PKG-INFO +90 -75
- {yace_codec-0.2.0 → yace_codec-0.2.1}/LICENSE +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/MANIFEST.in +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/examples/bytes_and_unicode.py +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/examples/quickstart.py +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/examples/share_tables.py +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/setup.cfg +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/__main__.py +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/_text.py +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/py.typed +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/tables.py +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec.egg-info/SOURCES.txt +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec.egg-info/dependency_links.txt +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec.egg-info/entry_points.txt +0 -0
- {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,24 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.2.1] - 2026-10-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `encrypt(data, token=...)` and `decrypt(encrypted, token=...)`: choose your own
|
|
12
|
+
secret token (any non-empty text or bytes). With a token, nothing is generated or
|
|
13
|
+
stored. Without one, a key is generated and kept in the key store, as before.
|
|
14
|
+
- `yace-codec encrypt --token` and `decrypt --token` ask for the secret token, or
|
|
15
|
+
read `YACE_CODEC_TOKEN`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Encrypted output is only letters and digits, with no `yace2_` prefix, so it does
|
|
20
|
+
not reveal that YACE made it. Output from 0.2.0 still decrypts.
|
|
21
|
+
- `decrypt()`'s first parameter is named `encrypted` (it was `token`).
|
|
22
|
+
- `password=`, `--password`, and `YACE_CODEC_PASSWORD` remain as other names for
|
|
23
|
+
`token=`, `--token`, and `YACE_CODEC_TOKEN`.
|
|
24
|
+
|
|
7
25
|
## [0.2.0] - 2026-10-14
|
|
8
26
|
|
|
9
27
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yace-codec
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: YACE (Yet Another Custom Encoder): reversible text encoding with random substitution tables, plus authenticated encryption
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: YACE (Yet Another Custom Encoder): reversible text encoding with random substitution tables, plus authenticated encryption with your own secret token, with no dependencies.
|
|
5
5
|
Author: Mudith Daga
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/thisismudith/YACE
|
|
@@ -33,7 +33,7 @@ Dynamic: license-file
|
|
|
33
33
|
|
|
34
34
|
# YACE
|
|
35
35
|
|
|
36
|
-
**Reversible text encoding with random substitution tables, and authenticated encryption
|
|
36
|
+
**Reversible text encoding with random substitution tables, and authenticated encryption with your own secret token.**
|
|
37
37
|
|
|
38
38
|
[](https://pypi.org/project/yace-codec/)
|
|
39
39
|
[](https://pypi.org/project/yace-codec/)
|
|
@@ -45,9 +45,9 @@ the `yace-codec` command, with no third-party dependencies:
|
|
|
45
45
|
- **Encoding** (`encode`/`decode`) turns text into tokens of random-looking letters
|
|
46
46
|
and digits with YACE's substitution tables. It hides text from casual reading,
|
|
47
47
|
but it is not encryption.
|
|
48
|
-
- **Encryption** (`encrypt`/`decrypt`) turns any text or file into
|
|
49
|
-
that
|
|
50
|
-
|
|
48
|
+
- **Encryption** (`encrypt`/`decrypt`) turns any text or file into letters and
|
|
49
|
+
digits that reveal nothing but their approximate size. Only your secret token
|
|
50
|
+
(or a stored key, if you choose none) can open them.
|
|
51
51
|
|
|
52
52
|
| | |
|
|
53
53
|
|---|---|
|
|
@@ -72,7 +72,7 @@ exporter.
|
|
|
72
72
|
- [How the encoder works](#how-the-encoder-works)
|
|
73
73
|
- [Installation](#installation)
|
|
74
74
|
- [Quick start](#quick-start)
|
|
75
|
-
- [Encryption
|
|
75
|
+
- [Encryption](#encryption)
|
|
76
76
|
- [Command line](#command-line)
|
|
77
77
|
- [API reference](#api-reference)
|
|
78
78
|
- [The key store](#the-key-store)
|
|
@@ -87,8 +87,8 @@ exporter.
|
|
|
87
87
|
|
|
88
88
|
Common encodings such as base64 or hex are public mappings, so anyone can reverse
|
|
89
89
|
them. Decoding a YACE token needs the tables in your key store, and encoding the same
|
|
90
|
-
text twice usually gives two different tokens.
|
|
91
|
-
|
|
90
|
+
text twice usually gives two different tokens. Encryption goes further: without
|
|
91
|
+
your secret token or key, the output cannot be read, linked to its key, or changed
|
|
92
92
|
without detection.
|
|
93
93
|
|
|
94
94
|
You can use YACE to:
|
|
@@ -97,18 +97,18 @@ You can use YACE to:
|
|
|
97
97
|
game content, test fixtures, or messages for people you share tables with);
|
|
98
98
|
- produce plain ASCII output: a short header line followed by a body of letters and
|
|
99
99
|
digits only;
|
|
100
|
-
- encrypt messages and files for people who share a
|
|
100
|
+
- encrypt messages and files for people who share a secret token or key with you;
|
|
101
101
|
- learn how substitution, homophones, nulls, and transposition combine in classical
|
|
102
102
|
ciphers.
|
|
103
103
|
|
|
104
104
|
| | base64 / hex | YACE `encode` | YACE `encrypt` | Fernet (`cryptography`) |
|
|
105
105
|
|---|---|---|---|---|
|
|
106
|
-
| Who can read it | anyone | holders of the tables, or an attacker with enough samples | holders of the
|
|
106
|
+
| Who can read it | anyone | holders of the tables, or an attacker with enough samples | holders of the secret token or key | holders of the key |
|
|
107
107
|
| Same input, same output | yes | usually not | no | no |
|
|
108
108
|
| Resists cryptanalysis | no | no | designed to (standard primitives, not audited) | yes (audited) |
|
|
109
109
|
| Detects tampering | no | no (only some corruption) | yes | yes |
|
|
110
110
|
| Hides the length | no | no | roughly (Padmé padding) | no |
|
|
111
|
-
| Output size | 1.33x / 2x | about 1.25 x length<sup>iterations</sup> | about 1.4x plus about
|
|
111
|
+
| Output size | 1.33x / 2x | about 1.25 x length<sup>iterations</sup> | about 1.4x plus about 90 characters | about 1.33x plus a fixed overhead |
|
|
112
112
|
|
|
113
113
|
## How the encoder works
|
|
114
114
|
|
|
@@ -378,68 +378,77 @@ assert make_token() == make_token()
|
|
|
378
378
|
More runnable scripts are in
|
|
379
379
|
[`examples/`](https://github.com/thisismudith/YACE/tree/main/examples).
|
|
380
380
|
|
|
381
|
-
## Encryption
|
|
381
|
+
## Encryption
|
|
382
382
|
|
|
383
|
-
`encrypt()` turns any text or bytes into
|
|
384
|
-
back:
|
|
383
|
+
`encrypt()` turns any text or bytes into letters and digits, and `decrypt()` turns
|
|
384
|
+
them back. Choose your own secret token, and pass the same one to decrypt:
|
|
385
385
|
|
|
386
386
|
```python
|
|
387
|
-
|
|
388
|
-
print(
|
|
389
|
-
print(yace.decrypt(token))
|
|
387
|
+
secret = yace.encrypt("Meet at the north gate at 06:00.", token="blue-falcon-42")
|
|
388
|
+
print(secret)
|
|
389
|
+
print(yace.decrypt(secret, token="blue-falcon-42"))
|
|
390
390
|
```
|
|
391
391
|
|
|
392
392
|
```text
|
|
393
|
-
|
|
393
|
+
3EH3rNDTPGnDlvRImekvmFAwBLPIwRdlI4aTqyfRXG335c06JkRK6e34mxOWK6VIm1Hin7pN1xddMH3FrG6xPd1t8YLuUIOuWydDezMJAgn7ndD1D8K6quWisJBc9JyVFOWE1Aq5xb
|
|
394
394
|
Meet at the north gate at 06:00.
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
|
|
398
|
-
and
|
|
399
|
-
|
|
397
|
+
The token can be any non-empty text or bytes. With a token, nothing is generated or
|
|
398
|
+
stored, and no key store is involved. Anyone who knows the token can decrypt, so
|
|
399
|
+
choose a long one that is hard to guess.
|
|
400
400
|
|
|
401
|
-
|
|
401
|
+
Without a token, YACE generates a random key for you, stores it in the
|
|
402
|
+
[key store](#the-key-store) as `default`, and reuses it:
|
|
402
403
|
|
|
403
404
|
```python
|
|
404
|
-
|
|
405
|
-
print(
|
|
405
|
+
secret = yace.encrypt("Meet at the north gate at 06:00.")
|
|
406
|
+
print(secret)
|
|
407
|
+
print(yace.decrypt(secret))
|
|
406
408
|
```
|
|
407
409
|
|
|
408
410
|
```text
|
|
409
|
-
|
|
411
|
+
Htun1NlqKRe8UnQ6To3lBB8ZK9f5NYpzDFkIW9RQrpgwEFQzSMJVYUv8n9AevttNK84oVKc8nEK9C1vX5BAXxunX9VJCVfXTN062Fh3Fk8jivV3mRkvxsUwWRARfBnTaIdq61OSTbb
|
|
412
|
+
Meet at the north gate at 06:00.
|
|
410
413
|
```
|
|
411
414
|
|
|
415
|
+
`decrypt()` tries every key in the key store, so the output never names its key.
|
|
416
|
+
Choose another stored key with `yace.encrypt(data, "team")`.
|
|
417
|
+
|
|
412
418
|
`decrypt()` returns `str` for text and `bytes` for bytes. It also offers:
|
|
413
419
|
|
|
414
|
-
- `max_age`: `yace.decrypt(
|
|
415
|
-
ago with `TokenExpiredError`.
|
|
416
|
-
- Tamper detection: a wrong
|
|
420
|
+
- `max_age`: `yace.decrypt(secret, max_age=3600)` rejects data encrypted more than an
|
|
421
|
+
hour ago with `TokenExpiredError`.
|
|
422
|
+
- Tamper detection: a wrong token or key, or any change to the encrypted text, raises
|
|
417
423
|
`DecryptionError`.
|
|
418
|
-
- Key sharing: export a key with `KeyStore.export_key("team")` (CLI:
|
|
424
|
+
- Key sharing: export a stored key with `KeyStore.export_key("team")` (CLI:
|
|
419
425
|
`yace-codec keys export team`) and import it with
|
|
420
426
|
`import_keys(data, name="team")` (CLI: `yace-codec keys import FILE --name team`).
|
|
421
|
-
Anyone with the key can
|
|
427
|
+
Anyone with the key can decrypt and encrypt with it.
|
|
422
428
|
|
|
423
|
-
|
|
429
|
+
`password=` is another name for `token=`, kept from version 0.2.0. Output from 0.2.0
|
|
430
|
+
starts with `yace2_`; it still decrypts.
|
|
424
431
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
432
|
+
### What the encrypted text reveals
|
|
433
|
+
|
|
434
|
+
Only roughly how long the data is: the size is padded to a Padmé length, which leaks
|
|
435
|
+
at most a few bits. It has no prefix or header, so it does not say that YACE made it.
|
|
436
|
+
The key name, the exact length, the creation time, and whether the data was text or
|
|
437
|
+
bytes are all encrypted. The same data gives different output every time.
|
|
429
438
|
|
|
430
439
|
### How encryption works
|
|
431
440
|
|
|
432
|
-
1. A random 16-byte salt is drawn
|
|
433
|
-
2. The secret is
|
|
434
|
-
|
|
441
|
+
1. A random 16-byte salt is drawn every time.
|
|
442
|
+
2. The secret is your token stretched with scrypt (N = 2<sup>17</sup>, r = 8, p = 1)
|
|
443
|
+
and the salt, or a 32-byte key from the key store.
|
|
435
444
|
3. HKDF-SHA256 (RFC 5869) derives two keys from the secret and the salt: one to
|
|
436
|
-
encrypt and one to authenticate. Every
|
|
445
|
+
encrypt and one to authenticate. Every output gets its own pair.
|
|
437
446
|
4. A frame holds the data type, the creation time, the length, the data, and zero
|
|
438
447
|
padding up to a Padmé length.
|
|
439
448
|
5. The frame is XORed with a keystream of HMAC-SHA256 blocks in counter mode.
|
|
440
|
-
6. An HMAC-SHA256 tag covers the
|
|
441
|
-
|
|
442
|
-
|
|
449
|
+
6. An HMAC-SHA256 tag covers the salt and the ciphertext. Decryption checks it
|
|
450
|
+
first, with a constant-time comparison, so wrong secrets and changed text are
|
|
451
|
+
rejected before anything is decrypted.
|
|
443
452
|
7. The salt, ciphertext, and tag are written as letters and digits: every 8 bytes
|
|
444
453
|
become 11 characters ("YACE62").
|
|
445
454
|
|
|
@@ -481,30 +490,34 @@ $ yace-codec decode --binary photo.yace -o photo-copy.jpg
|
|
|
481
490
|
Encrypt and decrypt (any file works, byte for byte):
|
|
482
491
|
|
|
483
492
|
```console
|
|
484
|
-
$ yace-codec encrypt --text "Meet at the north gate at 06:00."
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
Password:
|
|
493
|
+
$ yace-codec encrypt --token --text "Meet at the north gate at 06:00."
|
|
494
|
+
Secret token:
|
|
495
|
+
Repeat secret token:
|
|
496
|
+
3EH3rNDTPGnDlvRImekvmFAwBLPIwRdlI4aTqyfRXG335c06JkRK6e34mxOWK6VIm1Hin7pN1xddMH3FrG6xPd1t8YLuUIOuWydDezMJAgn7ndD1D8K6quWisJBc9JyVFOWE1Aq5xb
|
|
497
|
+
|
|
498
|
+
$ yace-codec encrypt --token notes.txt -o notes.yace
|
|
499
|
+
Secret token:
|
|
500
|
+
Repeat secret token:
|
|
501
|
+
$ yace-codec decrypt --token notes.yace
|
|
502
|
+
Secret token:
|
|
495
503
|
Shopping list:
|
|
496
504
|
- milk
|
|
497
505
|
- 12 eggs
|
|
498
506
|
|
|
507
|
+
$ yace-codec encrypt secret.pdf -o secret.yace --key team
|
|
508
|
+
$ yace-codec decrypt secret.yace -o secret-copy.pdf
|
|
509
|
+
|
|
499
510
|
$ yace-codec keys list
|
|
500
511
|
default
|
|
501
512
|
team
|
|
502
513
|
```
|
|
503
514
|
|
|
504
|
-
`--
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
515
|
+
`--token` (or `-p`) asks for your secret token (twice when encrypting), so it never
|
|
516
|
+
appears in your shell history. Without `--token`, a stored key is used. Scripts can
|
|
517
|
+
set `YACE_CODEC_TOKEN` instead, but other programs running as you may be able to
|
|
518
|
+
read environment variables. `--password` and `YACE_CODEC_PASSWORD`, from 0.2.0,
|
|
519
|
+
still work. `keys import` needs `--name NAME` (or `--all-keys`), for the same
|
|
520
|
+
reason as `tables import` below.
|
|
508
521
|
|
|
509
522
|
Inspect and share tables:
|
|
510
523
|
|
|
@@ -543,8 +556,8 @@ yace-codec: error: groceries.json holds tables for: groceries. Name the family y
|
|
|
543
556
|
| `tables list` | `-k/--keystore PATH` |
|
|
544
557
|
| `tables export NAME` | `-o/--output PATH`, `-k/--keystore PATH` |
|
|
545
558
|
| `tables import FILE` | `--family NAME` or `--all-families` (one is required), `--overwrite`, `-k/--keystore PATH` (use `-` as `FILE` for standard input) |
|
|
546
|
-
| `encrypt [INPUT]` | `-t/--text TEXT`, `-o/--output PATH`, `--key NAME` (default `default`) or `-p/--
|
|
547
|
-
| `decrypt [INPUT]` | `-t/--text
|
|
559
|
+
| `encrypt [INPUT]` | `-t/--text TEXT`, `-o/--output PATH`, `--key NAME` (default `default`) or `-p/--token`, `-k/--keystore PATH` |
|
|
560
|
+
| `decrypt [INPUT]` | `-t/--text TEXT`, `-o/--output PATH`, `-p/--token`, `--max-age SECONDS`, `-k/--keystore PATH` |
|
|
548
561
|
| `keys list` | `-k/--keystore PATH` |
|
|
549
562
|
| `keys export NAME` | `-o/--output PATH`, `-k/--keystore PATH` |
|
|
550
563
|
| `keys import FILE` | `--name NAME` or `--all-keys` (one is required), `--overwrite`, `-k/--keystore PATH` |
|
|
@@ -566,8 +579,8 @@ Everything below is importable from `yace_codec`.
|
|
|
566
579
|
| `decode(token, *, keystore=None) -> str` | Decode a token: a `str`, or `bytes` in UTF-8 (UTF-16 and UTF-32 need a byte order mark). |
|
|
567
580
|
| `encode_bytes(data, name="default", *, length=2, iterations=3, keystore=None) -> str` | Encode any bytes (base64 first). |
|
|
568
581
|
| `decode_bytes(token, *, keystore=None) -> bytes` | Decode a token made by `encode_bytes`. |
|
|
569
|
-
| `encrypt(data, key=None, *,
|
|
570
|
-
| `decrypt(
|
|
582
|
+
| `encrypt(data, key=None, *, token=None, keystore=None) -> str` | Encrypt text or bytes into letters and digits, with your own secret `token` (text or bytes) or a stored key (default `"default"`, generated on first use). |
|
|
583
|
+
| `decrypt(encrypted, *, token=None, max_age=None, keystore=None) -> str \| bytes` | Decrypt the output of `encrypt`; returns `str` or `bytes`, as encrypted. |
|
|
571
584
|
| `parse_header(token) -> Header` | Read a token's header without decoding it. |
|
|
572
585
|
| `default_keystore_path() -> pathlib.Path` | Where the default key store lives (an absolute path). |
|
|
573
586
|
|
|
@@ -628,8 +641,8 @@ YaceError
|
|
|
628
641
|
├── EncodingError invalid settings (also a ValueError)
|
|
629
642
|
│ └── UnsupportedCharacterError .characters, .table_name
|
|
630
643
|
├── DecodingError malformed or corrupted token, or wrong tables (also a ValueError)
|
|
631
|
-
│ └── DecryptionError
|
|
632
|
-
│ └── TokenExpiredError
|
|
644
|
+
│ └── DecryptionError encrypted text cannot be opened: wrong token or key, or changed
|
|
645
|
+
│ └── TokenExpiredError it was encrypted longer ago than max_age
|
|
633
646
|
├── TableNotFoundError a needed table is missing; .table_name (also a LookupError)
|
|
634
647
|
└── KeyStoreError unreadable, invalid, or unwritable key store
|
|
635
648
|
```
|
|
@@ -703,8 +716,8 @@ encrypted with its keys becomes unreadable. Anyone who has it can read those tok
|
|
|
703
716
|
token character, so tokens near the size limit need 1–2 GB. YACE is meant for
|
|
704
717
|
strings and small to medium files, not streams.
|
|
705
718
|
- **Empty input.** Encoding `""` gives a header-only token, which decodes to `""`.
|
|
706
|
-
- **Encryption size.** `encrypt()` holds the data and the
|
|
707
|
-
|
|
719
|
+
- **Encryption size.** `encrypt()` holds the data and the output in memory. The
|
|
720
|
+
output is about 1.4 times the size of the data, plus about 90 characters.
|
|
708
721
|
- **Whitespace.** Decoding ignores whitespace in a token, so wrapped lines, CRLF line
|
|
709
722
|
endings, and leading blank lines are fine.
|
|
710
723
|
- **Corruption.** Decoding raises `DecodingError` when a chunk is not a valid code,
|
|
@@ -741,17 +754,19 @@ encrypted with its keys becomes unreadable. Anyone who has it can read those tok
|
|
|
741
754
|
### Encryption (`encrypt`)
|
|
742
755
|
|
|
743
756
|
- Confidentiality and integrity rest on HMAC-SHA256 as a pseudorandom function (for
|
|
744
|
-
the keystream and the tag), HKDF-SHA256 for per-
|
|
745
|
-
|
|
757
|
+
the keystream and the tag), HKDF-SHA256 for per-output keys, and scrypt for
|
|
758
|
+
secret tokens. These are standard, well-studied constructions, but this project
|
|
746
759
|
assembled them and nobody has audited the result. For high-stakes secrets, use an
|
|
747
760
|
audited library such as [`cryptography`](https://cryptography.io/).
|
|
748
|
-
-
|
|
749
|
-
but a short or common
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
761
|
+
- Encryption with your own token is only as strong as the token. scrypt makes each
|
|
762
|
+
guess slow, but a short or common token can still be found. Stored keys are 32
|
|
763
|
+
random bytes.
|
|
764
|
+
- The key store holds the keys: anyone who copies it can open everything encrypted
|
|
765
|
+
with its keys. Encryption with your own token does not depend on the key store.
|
|
766
|
+
- The output hides the key name, the creation time, and the exact length, but not
|
|
767
|
+
roughly how long the data is.
|
|
768
|
+
- There is no forward secrecy (a leaked token or key opens everything encrypted
|
|
769
|
+
with it earlier),
|
|
755
770
|
and pure Python gives no protection against attackers who can measure timing or
|
|
756
771
|
memory on your machine.
|
|
757
772
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# YACE
|
|
2
2
|
|
|
3
|
-
**Reversible text encoding with random substitution tables, and authenticated encryption
|
|
3
|
+
**Reversible text encoding with random substitution tables, and authenticated encryption with your own secret token.**
|
|
4
4
|
|
|
5
5
|
[](https://pypi.org/project/yace-codec/)
|
|
6
6
|
[](https://pypi.org/project/yace-codec/)
|
|
@@ -12,9 +12,9 @@ the `yace-codec` command, with no third-party dependencies:
|
|
|
12
12
|
- **Encoding** (`encode`/`decode`) turns text into tokens of random-looking letters
|
|
13
13
|
and digits with YACE's substitution tables. It hides text from casual reading,
|
|
14
14
|
but it is not encryption.
|
|
15
|
-
- **Encryption** (`encrypt`/`decrypt`) turns any text or file into
|
|
16
|
-
that
|
|
17
|
-
|
|
15
|
+
- **Encryption** (`encrypt`/`decrypt`) turns any text or file into letters and
|
|
16
|
+
digits that reveal nothing but their approximate size. Only your secret token
|
|
17
|
+
(or a stored key, if you choose none) can open them.
|
|
18
18
|
|
|
19
19
|
| | |
|
|
20
20
|
|---|---|
|
|
@@ -39,7 +39,7 @@ exporter.
|
|
|
39
39
|
- [How the encoder works](#how-the-encoder-works)
|
|
40
40
|
- [Installation](#installation)
|
|
41
41
|
- [Quick start](#quick-start)
|
|
42
|
-
- [Encryption
|
|
42
|
+
- [Encryption](#encryption)
|
|
43
43
|
- [Command line](#command-line)
|
|
44
44
|
- [API reference](#api-reference)
|
|
45
45
|
- [The key store](#the-key-store)
|
|
@@ -54,8 +54,8 @@ exporter.
|
|
|
54
54
|
|
|
55
55
|
Common encodings such as base64 or hex are public mappings, so anyone can reverse
|
|
56
56
|
them. Decoding a YACE token needs the tables in your key store, and encoding the same
|
|
57
|
-
text twice usually gives two different tokens.
|
|
58
|
-
|
|
57
|
+
text twice usually gives two different tokens. Encryption goes further: without
|
|
58
|
+
your secret token or key, the output cannot be read, linked to its key, or changed
|
|
59
59
|
without detection.
|
|
60
60
|
|
|
61
61
|
You can use YACE to:
|
|
@@ -64,18 +64,18 @@ You can use YACE to:
|
|
|
64
64
|
game content, test fixtures, or messages for people you share tables with);
|
|
65
65
|
- produce plain ASCII output: a short header line followed by a body of letters and
|
|
66
66
|
digits only;
|
|
67
|
-
- encrypt messages and files for people who share a
|
|
67
|
+
- encrypt messages and files for people who share a secret token or key with you;
|
|
68
68
|
- learn how substitution, homophones, nulls, and transposition combine in classical
|
|
69
69
|
ciphers.
|
|
70
70
|
|
|
71
71
|
| | base64 / hex | YACE `encode` | YACE `encrypt` | Fernet (`cryptography`) |
|
|
72
72
|
|---|---|---|---|---|
|
|
73
|
-
| Who can read it | anyone | holders of the tables, or an attacker with enough samples | holders of the
|
|
73
|
+
| Who can read it | anyone | holders of the tables, or an attacker with enough samples | holders of the secret token or key | holders of the key |
|
|
74
74
|
| Same input, same output | yes | usually not | no | no |
|
|
75
75
|
| Resists cryptanalysis | no | no | designed to (standard primitives, not audited) | yes (audited) |
|
|
76
76
|
| Detects tampering | no | no (only some corruption) | yes | yes |
|
|
77
77
|
| Hides the length | no | no | roughly (Padmé padding) | no |
|
|
78
|
-
| Output size | 1.33x / 2x | about 1.25 x length<sup>iterations</sup> | about 1.4x plus about
|
|
78
|
+
| Output size | 1.33x / 2x | about 1.25 x length<sup>iterations</sup> | about 1.4x plus about 90 characters | about 1.33x plus a fixed overhead |
|
|
79
79
|
|
|
80
80
|
## How the encoder works
|
|
81
81
|
|
|
@@ -345,68 +345,77 @@ assert make_token() == make_token()
|
|
|
345
345
|
More runnable scripts are in
|
|
346
346
|
[`examples/`](https://github.com/thisismudith/YACE/tree/main/examples).
|
|
347
347
|
|
|
348
|
-
## Encryption
|
|
348
|
+
## Encryption
|
|
349
349
|
|
|
350
|
-
`encrypt()` turns any text or bytes into
|
|
351
|
-
back:
|
|
350
|
+
`encrypt()` turns any text or bytes into letters and digits, and `decrypt()` turns
|
|
351
|
+
them back. Choose your own secret token, and pass the same one to decrypt:
|
|
352
352
|
|
|
353
353
|
```python
|
|
354
|
-
|
|
355
|
-
print(
|
|
356
|
-
print(yace.decrypt(token))
|
|
354
|
+
secret = yace.encrypt("Meet at the north gate at 06:00.", token="blue-falcon-42")
|
|
355
|
+
print(secret)
|
|
356
|
+
print(yace.decrypt(secret, token="blue-falcon-42"))
|
|
357
357
|
```
|
|
358
358
|
|
|
359
359
|
```text
|
|
360
|
-
|
|
360
|
+
3EH3rNDTPGnDlvRImekvmFAwBLPIwRdlI4aTqyfRXG335c06JkRK6e34mxOWK6VIm1Hin7pN1xddMH3FrG6xPd1t8YLuUIOuWydDezMJAgn7ndD1D8K6quWisJBc9JyVFOWE1Aq5xb
|
|
361
361
|
Meet at the north gate at 06:00.
|
|
362
362
|
```
|
|
363
363
|
|
|
364
|
-
|
|
365
|
-
and
|
|
366
|
-
|
|
364
|
+
The token can be any non-empty text or bytes. With a token, nothing is generated or
|
|
365
|
+
stored, and no key store is involved. Anyone who knows the token can decrypt, so
|
|
366
|
+
choose a long one that is hard to guess.
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
Without a token, YACE generates a random key for you, stores it in the
|
|
369
|
+
[key store](#the-key-store) as `default`, and reuses it:
|
|
369
370
|
|
|
370
371
|
```python
|
|
371
|
-
|
|
372
|
-
print(
|
|
372
|
+
secret = yace.encrypt("Meet at the north gate at 06:00.")
|
|
373
|
+
print(secret)
|
|
374
|
+
print(yace.decrypt(secret))
|
|
373
375
|
```
|
|
374
376
|
|
|
375
377
|
```text
|
|
376
|
-
|
|
378
|
+
Htun1NlqKRe8UnQ6To3lBB8ZK9f5NYpzDFkIW9RQrpgwEFQzSMJVYUv8n9AevttNK84oVKc8nEK9C1vX5BAXxunX9VJCVfXTN062Fh3Fk8jivV3mRkvxsUwWRARfBnTaIdq61OSTbb
|
|
379
|
+
Meet at the north gate at 06:00.
|
|
377
380
|
```
|
|
378
381
|
|
|
382
|
+
`decrypt()` tries every key in the key store, so the output never names its key.
|
|
383
|
+
Choose another stored key with `yace.encrypt(data, "team")`.
|
|
384
|
+
|
|
379
385
|
`decrypt()` returns `str` for text and `bytes` for bytes. It also offers:
|
|
380
386
|
|
|
381
|
-
- `max_age`: `yace.decrypt(
|
|
382
|
-
ago with `TokenExpiredError`.
|
|
383
|
-
- Tamper detection: a wrong
|
|
387
|
+
- `max_age`: `yace.decrypt(secret, max_age=3600)` rejects data encrypted more than an
|
|
388
|
+
hour ago with `TokenExpiredError`.
|
|
389
|
+
- Tamper detection: a wrong token or key, or any change to the encrypted text, raises
|
|
384
390
|
`DecryptionError`.
|
|
385
|
-
- Key sharing: export a key with `KeyStore.export_key("team")` (CLI:
|
|
391
|
+
- Key sharing: export a stored key with `KeyStore.export_key("team")` (CLI:
|
|
386
392
|
`yace-codec keys export team`) and import it with
|
|
387
393
|
`import_keys(data, name="team")` (CLI: `yace-codec keys import FILE --name team`).
|
|
388
|
-
Anyone with the key can
|
|
394
|
+
Anyone with the key can decrypt and encrypt with it.
|
|
389
395
|
|
|
390
|
-
|
|
396
|
+
`password=` is another name for `token=`, kept from version 0.2.0. Output from 0.2.0
|
|
397
|
+
starts with `yace2_`; it still decrypts.
|
|
391
398
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
399
|
+
### What the encrypted text reveals
|
|
400
|
+
|
|
401
|
+
Only roughly how long the data is: the size is padded to a Padmé length, which leaks
|
|
402
|
+
at most a few bits. It has no prefix or header, so it does not say that YACE made it.
|
|
403
|
+
The key name, the exact length, the creation time, and whether the data was text or
|
|
404
|
+
bytes are all encrypted. The same data gives different output every time.
|
|
396
405
|
|
|
397
406
|
### How encryption works
|
|
398
407
|
|
|
399
|
-
1. A random 16-byte salt is drawn
|
|
400
|
-
2. The secret is
|
|
401
|
-
|
|
408
|
+
1. A random 16-byte salt is drawn every time.
|
|
409
|
+
2. The secret is your token stretched with scrypt (N = 2<sup>17</sup>, r = 8, p = 1)
|
|
410
|
+
and the salt, or a 32-byte key from the key store.
|
|
402
411
|
3. HKDF-SHA256 (RFC 5869) derives two keys from the secret and the salt: one to
|
|
403
|
-
encrypt and one to authenticate. Every
|
|
412
|
+
encrypt and one to authenticate. Every output gets its own pair.
|
|
404
413
|
4. A frame holds the data type, the creation time, the length, the data, and zero
|
|
405
414
|
padding up to a Padmé length.
|
|
406
415
|
5. The frame is XORed with a keystream of HMAC-SHA256 blocks in counter mode.
|
|
407
|
-
6. An HMAC-SHA256 tag covers the
|
|
408
|
-
|
|
409
|
-
|
|
416
|
+
6. An HMAC-SHA256 tag covers the salt and the ciphertext. Decryption checks it
|
|
417
|
+
first, with a constant-time comparison, so wrong secrets and changed text are
|
|
418
|
+
rejected before anything is decrypted.
|
|
410
419
|
7. The salt, ciphertext, and tag are written as letters and digits: every 8 bytes
|
|
411
420
|
become 11 characters ("YACE62").
|
|
412
421
|
|
|
@@ -448,30 +457,34 @@ $ yace-codec decode --binary photo.yace -o photo-copy.jpg
|
|
|
448
457
|
Encrypt and decrypt (any file works, byte for byte):
|
|
449
458
|
|
|
450
459
|
```console
|
|
451
|
-
$ yace-codec encrypt --text "Meet at the north gate at 06:00."
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
Password:
|
|
460
|
+
$ yace-codec encrypt --token --text "Meet at the north gate at 06:00."
|
|
461
|
+
Secret token:
|
|
462
|
+
Repeat secret token:
|
|
463
|
+
3EH3rNDTPGnDlvRImekvmFAwBLPIwRdlI4aTqyfRXG335c06JkRK6e34mxOWK6VIm1Hin7pN1xddMH3FrG6xPd1t8YLuUIOuWydDezMJAgn7ndD1D8K6quWisJBc9JyVFOWE1Aq5xb
|
|
464
|
+
|
|
465
|
+
$ yace-codec encrypt --token notes.txt -o notes.yace
|
|
466
|
+
Secret token:
|
|
467
|
+
Repeat secret token:
|
|
468
|
+
$ yace-codec decrypt --token notes.yace
|
|
469
|
+
Secret token:
|
|
462
470
|
Shopping list:
|
|
463
471
|
- milk
|
|
464
472
|
- 12 eggs
|
|
465
473
|
|
|
474
|
+
$ yace-codec encrypt secret.pdf -o secret.yace --key team
|
|
475
|
+
$ yace-codec decrypt secret.yace -o secret-copy.pdf
|
|
476
|
+
|
|
466
477
|
$ yace-codec keys list
|
|
467
478
|
default
|
|
468
479
|
team
|
|
469
480
|
```
|
|
470
481
|
|
|
471
|
-
`--
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
482
|
+
`--token` (or `-p`) asks for your secret token (twice when encrypting), so it never
|
|
483
|
+
appears in your shell history. Without `--token`, a stored key is used. Scripts can
|
|
484
|
+
set `YACE_CODEC_TOKEN` instead, but other programs running as you may be able to
|
|
485
|
+
read environment variables. `--password` and `YACE_CODEC_PASSWORD`, from 0.2.0,
|
|
486
|
+
still work. `keys import` needs `--name NAME` (or `--all-keys`), for the same
|
|
487
|
+
reason as `tables import` below.
|
|
475
488
|
|
|
476
489
|
Inspect and share tables:
|
|
477
490
|
|
|
@@ -510,8 +523,8 @@ yace-codec: error: groceries.json holds tables for: groceries. Name the family y
|
|
|
510
523
|
| `tables list` | `-k/--keystore PATH` |
|
|
511
524
|
| `tables export NAME` | `-o/--output PATH`, `-k/--keystore PATH` |
|
|
512
525
|
| `tables import FILE` | `--family NAME` or `--all-families` (one is required), `--overwrite`, `-k/--keystore PATH` (use `-` as `FILE` for standard input) |
|
|
513
|
-
| `encrypt [INPUT]` | `-t/--text TEXT`, `-o/--output PATH`, `--key NAME` (default `default`) or `-p/--
|
|
514
|
-
| `decrypt [INPUT]` | `-t/--text
|
|
526
|
+
| `encrypt [INPUT]` | `-t/--text TEXT`, `-o/--output PATH`, `--key NAME` (default `default`) or `-p/--token`, `-k/--keystore PATH` |
|
|
527
|
+
| `decrypt [INPUT]` | `-t/--text TEXT`, `-o/--output PATH`, `-p/--token`, `--max-age SECONDS`, `-k/--keystore PATH` |
|
|
515
528
|
| `keys list` | `-k/--keystore PATH` |
|
|
516
529
|
| `keys export NAME` | `-o/--output PATH`, `-k/--keystore PATH` |
|
|
517
530
|
| `keys import FILE` | `--name NAME` or `--all-keys` (one is required), `--overwrite`, `-k/--keystore PATH` |
|
|
@@ -533,8 +546,8 @@ Everything below is importable from `yace_codec`.
|
|
|
533
546
|
| `decode(token, *, keystore=None) -> str` | Decode a token: a `str`, or `bytes` in UTF-8 (UTF-16 and UTF-32 need a byte order mark). |
|
|
534
547
|
| `encode_bytes(data, name="default", *, length=2, iterations=3, keystore=None) -> str` | Encode any bytes (base64 first). |
|
|
535
548
|
| `decode_bytes(token, *, keystore=None) -> bytes` | Decode a token made by `encode_bytes`. |
|
|
536
|
-
| `encrypt(data, key=None, *,
|
|
537
|
-
| `decrypt(
|
|
549
|
+
| `encrypt(data, key=None, *, token=None, keystore=None) -> str` | Encrypt text or bytes into letters and digits, with your own secret `token` (text or bytes) or a stored key (default `"default"`, generated on first use). |
|
|
550
|
+
| `decrypt(encrypted, *, token=None, max_age=None, keystore=None) -> str \| bytes` | Decrypt the output of `encrypt`; returns `str` or `bytes`, as encrypted. |
|
|
538
551
|
| `parse_header(token) -> Header` | Read a token's header without decoding it. |
|
|
539
552
|
| `default_keystore_path() -> pathlib.Path` | Where the default key store lives (an absolute path). |
|
|
540
553
|
|
|
@@ -595,8 +608,8 @@ YaceError
|
|
|
595
608
|
├── EncodingError invalid settings (also a ValueError)
|
|
596
609
|
│ └── UnsupportedCharacterError .characters, .table_name
|
|
597
610
|
├── DecodingError malformed or corrupted token, or wrong tables (also a ValueError)
|
|
598
|
-
│ └── DecryptionError
|
|
599
|
-
│ └── TokenExpiredError
|
|
611
|
+
│ └── DecryptionError encrypted text cannot be opened: wrong token or key, or changed
|
|
612
|
+
│ └── TokenExpiredError it was encrypted longer ago than max_age
|
|
600
613
|
├── TableNotFoundError a needed table is missing; .table_name (also a LookupError)
|
|
601
614
|
└── KeyStoreError unreadable, invalid, or unwritable key store
|
|
602
615
|
```
|
|
@@ -670,8 +683,8 @@ encrypted with its keys becomes unreadable. Anyone who has it can read those tok
|
|
|
670
683
|
token character, so tokens near the size limit need 1–2 GB. YACE is meant for
|
|
671
684
|
strings and small to medium files, not streams.
|
|
672
685
|
- **Empty input.** Encoding `""` gives a header-only token, which decodes to `""`.
|
|
673
|
-
- **Encryption size.** `encrypt()` holds the data and the
|
|
674
|
-
|
|
686
|
+
- **Encryption size.** `encrypt()` holds the data and the output in memory. The
|
|
687
|
+
output is about 1.4 times the size of the data, plus about 90 characters.
|
|
675
688
|
- **Whitespace.** Decoding ignores whitespace in a token, so wrapped lines, CRLF line
|
|
676
689
|
endings, and leading blank lines are fine.
|
|
677
690
|
- **Corruption.** Decoding raises `DecodingError` when a chunk is not a valid code,
|
|
@@ -708,17 +721,19 @@ encrypted with its keys becomes unreadable. Anyone who has it can read those tok
|
|
|
708
721
|
### Encryption (`encrypt`)
|
|
709
722
|
|
|
710
723
|
- Confidentiality and integrity rest on HMAC-SHA256 as a pseudorandom function (for
|
|
711
|
-
the keystream and the tag), HKDF-SHA256 for per-
|
|
712
|
-
|
|
724
|
+
the keystream and the tag), HKDF-SHA256 for per-output keys, and scrypt for
|
|
725
|
+
secret tokens. These are standard, well-studied constructions, but this project
|
|
713
726
|
assembled them and nobody has audited the result. For high-stakes secrets, use an
|
|
714
727
|
audited library such as [`cryptography`](https://cryptography.io/).
|
|
715
|
-
-
|
|
716
|
-
but a short or common
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
728
|
+
- Encryption with your own token is only as strong as the token. scrypt makes each
|
|
729
|
+
guess slow, but a short or common token can still be found. Stored keys are 32
|
|
730
|
+
random bytes.
|
|
731
|
+
- The key store holds the keys: anyone who copies it can open everything encrypted
|
|
732
|
+
with its keys. Encryption with your own token does not depend on the key store.
|
|
733
|
+
- The output hides the key name, the creation time, and the exact length, but not
|
|
734
|
+
roughly how long the data is.
|
|
735
|
+
- There is no forward secrecy (a leaked token or key opens everything encrypted
|
|
736
|
+
with it earlier),
|
|
722
737
|
and pure Python gives no protection against attackers who can measure timing or
|
|
723
738
|
memory on your machine.
|
|
724
739
|
|