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.
Files changed (25) hide show
  1. {yace_codec-0.2.0 → yace_codec-0.2.1}/CHANGELOG.md +18 -0
  2. {yace_codec-0.2.0/src/yace_codec.egg-info → yace_codec-0.2.1}/PKG-INFO +90 -75
  3. {yace_codec-0.2.0 → yace_codec-0.2.1}/README.md +88 -73
  4. {yace_codec-0.2.0 → yace_codec-0.2.1}/pyproject.toml +1 -1
  5. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/__init__.py +2 -2
  6. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/cli.py +31 -28
  7. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/codec.py +48 -34
  8. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/errors.py +1 -1
  9. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/keystore.py +1 -1
  10. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/sealed.py +83 -54
  11. {yace_codec-0.2.0 → yace_codec-0.2.1/src/yace_codec.egg-info}/PKG-INFO +90 -75
  12. {yace_codec-0.2.0 → yace_codec-0.2.1}/LICENSE +0 -0
  13. {yace_codec-0.2.0 → yace_codec-0.2.1}/MANIFEST.in +0 -0
  14. {yace_codec-0.2.0 → yace_codec-0.2.1}/examples/bytes_and_unicode.py +0 -0
  15. {yace_codec-0.2.0 → yace_codec-0.2.1}/examples/quickstart.py +0 -0
  16. {yace_codec-0.2.0 → yace_codec-0.2.1}/examples/share_tables.py +0 -0
  17. {yace_codec-0.2.0 → yace_codec-0.2.1}/setup.cfg +0 -0
  18. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/__main__.py +0 -0
  19. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/_text.py +0 -0
  20. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/py.typed +0 -0
  21. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec/tables.py +0 -0
  22. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec.egg-info/SOURCES.txt +0 -0
  23. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec.egg-info/dependency_links.txt +0 -0
  24. {yace_codec-0.2.0 → yace_codec-0.2.1}/src/yace_codec.egg-info/entry_points.txt +0 -0
  25. {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.0
4
- Summary: YACE (Yet Another Custom Encoder): reversible text encoding with random substitution tables, plus authenticated encryption into yace2 tokens, with no dependencies.
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 into `yace2` tokens.**
36
+ **Reversible text encoding with random substitution tables, and authenticated encryption with your own secret token.**
37
37
 
38
38
  [![PyPI](https://img.shields.io/pypi/v/yace-codec)](https://pypi.org/project/yace-codec/)
39
39
  [![Python versions](https://img.shields.io/pypi/pyversions/yace-codec)](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 a `yace2_` token
49
- that reveals nothing but its approximate size, and that only the matching key or
50
- password can open.
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 (yace2 tokens)](#encryption-yace2-tokens)
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. Encrypted `yace2` tokens go further:
91
- without the key or password they cannot be read, linked to their key, or changed
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 key or a password with you;
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 key or password | holders of the key |
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 95 characters | about 1.33x plus a fixed overhead |
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 (yace2 tokens)
381
+ ## Encryption
382
382
 
383
- `encrypt()` turns any text or bytes into a `yace2_` token, and `decrypt()` turns it
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
- token = yace.encrypt("Meet at the north gate at 06:00.")
388
- print(token)
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
- yace2_5a5Hfi0rCH1GDypoUqaZ0C2doMT8ZgvPx3TdjOC4G7Zi4afClySrDuTJDBs38qfnIMITLJWJXXtwE3kzFZl77cPY1vmbl740V6h2Dk7H8DdQAP8CUd2AgCPsrIFJKI8j02wB2jawa0
393
+ 3EH3rNDTPGnDlvRImekvmFAwBLPIwRdlI4aTqyfRXG335c06JkRK6e34mxOWK6VIm1Hin7pN1xddMH3FrG6xPd1t8YLuUIOuWydDezMJAgn7ndD1D8K6quWisJBc9JyVFOWE1Aq5xb
394
394
  Meet at the north gate at 06:00.
395
395
  ```
396
396
 
397
- By default the secret key named `default` in the [key store](#the-key-store) is used,
398
- and it is generated on first use. `decrypt()` tries every key in the key store, so a
399
- token never names its key. Choose another key with `yace.encrypt(data, "team")`.
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
- With a password instead of a stored key, no key store is involved:
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
- token = yace.encrypt("for your eyes only", password="correct horse battery staple")
405
- print(yace.decrypt(token, password="correct horse battery staple"))
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
- for your eyes only
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(token, max_age=3600)` rejects tokens made more than an hour
415
- ago with `TokenExpiredError`.
416
- - Tamper detection: a wrong key or password, or any change to the token, raises
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 read and create tokens for it.
427
+ Anyone with the key can decrypt and encrypt with it.
422
428
 
423
- ### What a yace2 token reveals
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
- Only that it is a yace2 token, and roughly how long the data is: the size is padded
426
- to a Padmé length, which leaks at most a few bits. The key name, the exact length,
427
- the creation time, and whether the data was text or bytes are all encrypted. The same
428
- data gives a different token every time.
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 for every token.
433
- 2. The secret is a 32-byte key from the key store, or the password stretched with
434
- scrypt (N = 2<sup>17</sup>, r = 8, p = 1) and the salt.
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 token gets its own pair.
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 prefix, the salt, and the ciphertext. Decryption
441
- checks it first, with a constant-time comparison, so wrong keys and changed tokens
442
- are rejected before anything is decrypted.
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
- yace2_JHVUvAKzCjE56fROwZRSVk3L7Y4zpVFHXJlRuiF4Mb2lK0IjhXOy7fv6tTg9sDdyRxIYUG5RC0WS7CNhi073vj6AAcx3VS651sTFbvYWVvSEi669m6KEXB52AKvdzmQ7RM9L3sAiO5
486
-
487
- $ yace-codec encrypt secret.pdf -o secret.yace --key team
488
- $ yace-codec decrypt secret.yace -o secret-copy.pdf
489
-
490
- $ yace-codec encrypt --password notes.txt -o notes.yace
491
- Password:
492
- Repeat password:
493
- $ yace-codec decrypt --password notes.yace
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
- `--password` asks for the password (twice when encrypting). Scripts can set
505
- `YACE_CODEC_PASSWORD` instead, but other programs running as you may be able to
506
- read environment variables. `keys import` needs `--name NAME` (or `--all-keys`), for
507
- the same reason as `tables import` below.
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/--password`, `-k/--keystore PATH` |
547
- | `decrypt [INPUT]` | `-t/--text TOKEN`, `-o/--output PATH`, `-p/--password`, `--max-age SECONDS`, `-k/--keystore PATH` |
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, *, password=None, keystore=None) -> str` | Encrypt text or bytes into a `yace2_` token, with a stored key (default `"default"`) or a password. |
570
- | `decrypt(token, *, password=None, max_age=None, keystore=None) -> str \| bytes` | Decrypt a `yace2_` token; returns `str` or `bytes`, as encrypted. |
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 a yace2 token cannot be opened: wrong key or password, or changed
632
- │ └── TokenExpiredError the token is older than max_age
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 token in memory. Tokens
707
- are about 1.4 times the size of the data, plus about 95 characters.
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-token keys, and scrypt for
745
- passwords. These are standard, well-studied constructions, but this project
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
- - A password token is only as strong as its password. scrypt makes each guess slow,
749
- but a short or common password can still be found. Stored keys are 32 random bytes.
750
- - The key store holds the keys: anyone who copies it can open every token made with
751
- its keys. Password tokens do not depend on the key store.
752
- - Tokens hide the key name, the creation time, and the exact length, but not roughly
753
- how long the data is, or that YACE made them.
754
- - There is no forward secrecy (a leaked key opens all earlier tokens made with it),
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 into `yace2` tokens.**
3
+ **Reversible text encoding with random substitution tables, and authenticated encryption with your own secret token.**
4
4
 
5
5
  [![PyPI](https://img.shields.io/pypi/v/yace-codec)](https://pypi.org/project/yace-codec/)
6
6
  [![Python versions](https://img.shields.io/pypi/pyversions/yace-codec)](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 a `yace2_` token
16
- that reveals nothing but its approximate size, and that only the matching key or
17
- password can open.
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 (yace2 tokens)](#encryption-yace2-tokens)
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. Encrypted `yace2` tokens go further:
58
- without the key or password they cannot be read, linked to their key, or changed
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 key or a password with you;
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 key or password | holders of the key |
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 95 characters | about 1.33x plus a fixed overhead |
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 (yace2 tokens)
348
+ ## Encryption
349
349
 
350
- `encrypt()` turns any text or bytes into a `yace2_` token, and `decrypt()` turns it
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
- token = yace.encrypt("Meet at the north gate at 06:00.")
355
- print(token)
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
- yace2_5a5Hfi0rCH1GDypoUqaZ0C2doMT8ZgvPx3TdjOC4G7Zi4afClySrDuTJDBs38qfnIMITLJWJXXtwE3kzFZl77cPY1vmbl740V6h2Dk7H8DdQAP8CUd2AgCPsrIFJKI8j02wB2jawa0
360
+ 3EH3rNDTPGnDlvRImekvmFAwBLPIwRdlI4aTqyfRXG335c06JkRK6e34mxOWK6VIm1Hin7pN1xddMH3FrG6xPd1t8YLuUIOuWydDezMJAgn7ndD1D8K6quWisJBc9JyVFOWE1Aq5xb
361
361
  Meet at the north gate at 06:00.
362
362
  ```
363
363
 
364
- By default the secret key named `default` in the [key store](#the-key-store) is used,
365
- and it is generated on first use. `decrypt()` tries every key in the key store, so a
366
- token never names its key. Choose another key with `yace.encrypt(data, "team")`.
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
- With a password instead of a stored key, no key store is involved:
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
- token = yace.encrypt("for your eyes only", password="correct horse battery staple")
372
- print(yace.decrypt(token, password="correct horse battery staple"))
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
- for your eyes only
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(token, max_age=3600)` rejects tokens made more than an hour
382
- ago with `TokenExpiredError`.
383
- - Tamper detection: a wrong key or password, or any change to the token, raises
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 read and create tokens for it.
394
+ Anyone with the key can decrypt and encrypt with it.
389
395
 
390
- ### What a yace2 token reveals
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
- Only that it is a yace2 token, and roughly how long the data is: the size is padded
393
- to a Padmé length, which leaks at most a few bits. The key name, the exact length,
394
- the creation time, and whether the data was text or bytes are all encrypted. The same
395
- data gives a different token every time.
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 for every token.
400
- 2. The secret is a 32-byte key from the key store, or the password stretched with
401
- scrypt (N = 2<sup>17</sup>, r = 8, p = 1) and the salt.
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 token gets its own pair.
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 prefix, the salt, and the ciphertext. Decryption
408
- checks it first, with a constant-time comparison, so wrong keys and changed tokens
409
- are rejected before anything is decrypted.
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
- yace2_JHVUvAKzCjE56fROwZRSVk3L7Y4zpVFHXJlRuiF4Mb2lK0IjhXOy7fv6tTg9sDdyRxIYUG5RC0WS7CNhi073vj6AAcx3VS651sTFbvYWVvSEi669m6KEXB52AKvdzmQ7RM9L3sAiO5
453
-
454
- $ yace-codec encrypt secret.pdf -o secret.yace --key team
455
- $ yace-codec decrypt secret.yace -o secret-copy.pdf
456
-
457
- $ yace-codec encrypt --password notes.txt -o notes.yace
458
- Password:
459
- Repeat password:
460
- $ yace-codec decrypt --password notes.yace
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
- `--password` asks for the password (twice when encrypting). Scripts can set
472
- `YACE_CODEC_PASSWORD` instead, but other programs running as you may be able to
473
- read environment variables. `keys import` needs `--name NAME` (or `--all-keys`), for
474
- the same reason as `tables import` below.
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/--password`, `-k/--keystore PATH` |
514
- | `decrypt [INPUT]` | `-t/--text TOKEN`, `-o/--output PATH`, `-p/--password`, `--max-age SECONDS`, `-k/--keystore PATH` |
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, *, password=None, keystore=None) -> str` | Encrypt text or bytes into a `yace2_` token, with a stored key (default `"default"`) or a password. |
537
- | `decrypt(token, *, password=None, max_age=None, keystore=None) -> str \| bytes` | Decrypt a `yace2_` token; returns `str` or `bytes`, as encrypted. |
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 a yace2 token cannot be opened: wrong key or password, or changed
599
- │ └── TokenExpiredError the token is older than max_age
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 token in memory. Tokens
674
- are about 1.4 times the size of the data, plus about 95 characters.
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-token keys, and scrypt for
712
- passwords. These are standard, well-studied constructions, but this project
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
- - A password token is only as strong as its password. scrypt makes each guess slow,
716
- but a short or common password can still be found. Stored keys are 32 random bytes.
717
- - The key store holds the keys: anyone who copies it can open every token made with
718
- its keys. Password tokens do not depend on the key store.
719
- - Tokens hide the key name, the creation time, and the exact length, but not roughly
720
- how long the data is, or that YACE made them.
721
- - There is no forward secrecy (a leaked key opens all earlier tokens made with it),
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