yace-codec 0.1.0__tar.gz → 0.2.0__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.1.0 → yace_codec-0.2.0}/CHANGELOG.md +28 -0
  2. {yace_codec-0.1.0/src/yace_codec.egg-info → yace_codec-0.2.0}/PKG-INFO +176 -30
  3. {yace_codec-0.1.0 → yace_codec-0.2.0}/README.md +172 -27
  4. {yace_codec-0.1.0 → yace_codec-0.2.0}/pyproject.toml +4 -1
  5. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec/__init__.py +13 -2
  6. yace_codec-0.2.0/src/yace_codec/_text.py +39 -0
  7. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec/cli.py +141 -4
  8. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec/codec.py +98 -24
  9. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec/errors.py +10 -0
  10. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec/keystore.py +144 -16
  11. yace_codec-0.2.0/src/yace_codec/sealed.py +228 -0
  12. {yace_codec-0.1.0 → yace_codec-0.2.0/src/yace_codec.egg-info}/PKG-INFO +176 -30
  13. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec.egg-info/SOURCES.txt +2 -0
  14. {yace_codec-0.1.0 → yace_codec-0.2.0}/LICENSE +0 -0
  15. {yace_codec-0.1.0 → yace_codec-0.2.0}/MANIFEST.in +0 -0
  16. {yace_codec-0.1.0 → yace_codec-0.2.0}/examples/bytes_and_unicode.py +0 -0
  17. {yace_codec-0.1.0 → yace_codec-0.2.0}/examples/quickstart.py +0 -0
  18. {yace_codec-0.1.0 → yace_codec-0.2.0}/examples/share_tables.py +0 -0
  19. {yace_codec-0.1.0 → yace_codec-0.2.0}/setup.cfg +0 -0
  20. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec/__main__.py +0 -0
  21. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec/py.typed +0 -0
  22. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec/tables.py +0 -0
  23. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec.egg-info/dependency_links.txt +0 -0
  24. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec.egg-info/entry_points.txt +0 -0
  25. {yace_codec-0.1.0 → yace_codec-0.2.0}/src/yace_codec.egg-info/top_level.txt +0 -0
@@ -4,6 +4,34 @@ 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.0] - 2026-10-14
8
+
9
+ ### Added
10
+
11
+ - Encryption: `encrypt()` and `decrypt()` (also `Codec.encrypt()` /
12
+ `Codec.decrypt()`) turn text or bytes into `yace2_` tokens and back. Tokens are
13
+ authenticated (HMAC-SHA256, encrypt-then-MAC), use per-token keys derived with
14
+ HKDF-SHA256 from a random salt, and are padded to a Padmé length. They reveal neither
15
+ the key name, the creation time, the exact length, nor the data type. Everything
16
+ comes from Python's standard library.
17
+ - Secret keys in the key store (`key:<name>` entries): `ensure_key()`, `get_key()`,
18
+ `find_key()`, `key_names()`, `export_key()`, and `import_keys(..., name=...)`.
19
+ `decrypt()` tries every stored key.
20
+ - Password mode: `encrypt(data, password=...)` stretches the password with scrypt and
21
+ does not touch the key store.
22
+ - `decrypt(..., max_age=SECONDS)` rejects old tokens with `TokenExpiredError`.
23
+ - `DecryptionError` and `TokenExpiredError` (both subclasses of `DecodingError`), and
24
+ the `DEFAULT_KEY` constant.
25
+ - `yace-codec encrypt`, `yace-codec decrypt` (with `--password`, `--key`, and
26
+ `--max-age`), and `yace-codec keys list|export|import`. `keys import` needs
27
+ `--name NAME` or `--all-keys`. `YACE_CODEC_PASSWORD` can supply the password.
28
+
29
+ ### Changed
30
+
31
+ - `decode()` on a `yace2_` token points to `decrypt()`, and `decrypt()` on an
32
+ encoded token points to `decode()`.
33
+ - `import_tables()` refuses secret key entries; import them with `import_keys()`.
34
+
7
35
  ## [0.1.0] - 2026-10-14
8
36
 
9
37
  First release on PyPI, as `yace-codec` (import name `yace_codec`).
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yace-codec
3
- Version: 0.1.0
4
- Summary: YACE (Yet Another Custom Encoder): a reversible text encoder built on random substitution tables, layered re-encoding, noise chunks and chunk shuffling (obfuscation, not encryption).
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.
5
5
  Author: Mudith Daga
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/thisismudith/YACE
8
8
  Project-URL: Source, https://github.com/thisismudith/YACE
9
9
  Project-URL: Issues, https://github.com/thisismudith/YACE/issues
10
10
  Project-URL: Changelog, https://github.com/thisismudith/YACE/blob/main/CHANGELOG.md
11
- Keywords: yace,encoder,decoder,encoding,codec,substitution,obfuscation,text,cli
11
+ Keywords: yace,encoder,decoder,encoding,codec,substitution,obfuscation,encryption,decryption,text,cli
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Intended Audience :: Developers
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
23
  Classifier: Programming Language :: Python :: 3.14
24
24
  Classifier: Programming Language :: Python :: 3.15
25
+ Classifier: Topic :: Security :: Cryptography
25
26
  Classifier: Topic :: Text Processing
26
27
  Classifier: Topic :: Utilities
27
28
  Classifier: Typing :: Typed
@@ -32,16 +33,21 @@ Dynamic: license-file
32
33
 
33
34
  # YACE
34
35
 
35
- **Reversible text encoding with random substitution tables, layered re-encoding, noise chunks, and chunk shuffling.**
36
+ **Reversible text encoding with random substitution tables, and authenticated encryption into `yace2` tokens.**
36
37
 
37
38
  [![PyPI](https://img.shields.io/pypi/v/yace-codec)](https://pypi.org/project/yace-codec/)
38
39
  [![Python versions](https://img.shields.io/pypi/pyversions/yace-codec)](https://pypi.org/project/yace-codec/)
39
40
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/thisismudith/YACE/blob/main/LICENSE)
40
41
 
41
- YACE (Yet Another Custom Encoder) turns text into tokens made of random-looking
42
- letters and digits. Decoding a token needs the substitution tables that produced
43
- it, which you keep in a key store. It ships as a Python library and a command-line
44
- tool with no third-party dependencies.
42
+ YACE (Yet Another Custom Encoder) has two modes. Both come as a Python API and as
43
+ the `yace-codec` command, with no third-party dependencies:
44
+
45
+ - **Encoding** (`encode`/`decode`) turns text into tokens of random-looking letters
46
+ and digits with YACE's substitution tables. It hides text from casual reading,
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.
45
51
 
46
52
  | | |
47
53
  |---|---|
@@ -53,11 +59,12 @@ This project is not affiliated with the `yace` package on PyPI ("Yet Another Cod
53
59
  Emitter") or its `yace-*` companions, nor with YACE, the Prometheus CloudWatch
54
60
  exporter.
55
61
 
56
- > **Warning: YACE obfuscates; it does not encrypt.** At its core it is a substitution
57
- > code with extra noise. Frequency analysis, or a couple of tokens whose text an
58
- > attacker knows, can break it. Do not use it to protect passwords, personal data,
59
- > API keys, or anything else that must stay secret. Use a vetted library such as
60
- > [`cryptography`](https://cryptography.io/) for that.
62
+ > **Use `encrypt()` for secrets, never `encode()`.** Encoding is a substitution code
63
+ > with extra noise: frequency analysis, or a couple of tokens whose text an attacker
64
+ > knows, can break it. Encryption is built from standard primitives (scrypt,
65
+ > HKDF-SHA256, and HMAC-SHA256 from Python's standard library), but this project
66
+ > assembled them and nobody has audited the result. For high-stakes secrets, use an
67
+ > audited library such as [`cryptography`](https://cryptography.io/).
61
68
 
62
69
  ## Contents
63
70
 
@@ -65,6 +72,7 @@ exporter.
65
72
  - [How the encoder works](#how-the-encoder-works)
66
73
  - [Installation](#installation)
67
74
  - [Quick start](#quick-start)
75
+ - [Encryption (yace2 tokens)](#encryption-yace2-tokens)
68
76
  - [Command line](#command-line)
69
77
  - [API reference](#api-reference)
70
78
  - [The key store](#the-key-store)
@@ -79,7 +87,9 @@ exporter.
79
87
 
80
88
  Common encodings such as base64 or hex are public mappings, so anyone can reverse
81
89
  them. Decoding a YACE token needs the tables in your key store, and encoding the same
82
- text twice usually gives two different tokens.
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
92
+ without detection.
83
93
 
84
94
  You can use YACE to:
85
95
 
@@ -87,16 +97,18 @@ You can use YACE to:
87
97
  game content, test fixtures, or messages for people you share tables with);
88
98
  - produce plain ASCII output: a short header line followed by a body of letters and
89
99
  digits only;
100
+ - encrypt messages and files for people who share a key or a password with you;
90
101
  - learn how substitution, homophones, nulls, and transposition combine in classical
91
102
  ciphers.
92
103
 
93
- | | base64 / hex | YACE | Encryption (e.g. Fernet) |
94
- |---|---|---|---|
95
- | Who can decode | anyone | holders of the tables, or an attacker with enough samples | holders of the key |
96
- | Same input, same output | yes | usually not | no |
97
- | Resists cryptanalysis | no | no | yes |
98
- | Detects tampering | no | no (only some corruption) | yes |
99
- | Output size | 1.33x / 2x | about 1.25 x length<sup>iterations</sup> | about 1.33x plus a fixed overhead |
104
+ | | base64 / hex | YACE `encode` | YACE `encrypt` | Fernet (`cryptography`) |
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 |
107
+ | Same input, same output | yes | usually not | no | no |
108
+ | Resists cryptanalysis | no | no | designed to (standard primitives, not audited) | yes (audited) |
109
+ | Detects tampering | no | no (only some corruption) | yes | yes |
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 |
100
112
 
101
113
  ## How the encoder works
102
114
 
@@ -366,6 +378,74 @@ assert make_token() == make_token()
366
378
  More runnable scripts are in
367
379
  [`examples/`](https://github.com/thisismudith/YACE/tree/main/examples).
368
380
 
381
+ ## Encryption (yace2 tokens)
382
+
383
+ `encrypt()` turns any text or bytes into a `yace2_` token, and `decrypt()` turns it
384
+ back:
385
+
386
+ ```python
387
+ token = yace.encrypt("Meet at the north gate at 06:00.")
388
+ print(token)
389
+ print(yace.decrypt(token))
390
+ ```
391
+
392
+ ```text
393
+ yace2_5a5Hfi0rCH1GDypoUqaZ0C2doMT8ZgvPx3TdjOC4G7Zi4afClySrDuTJDBs38qfnIMITLJWJXXtwE3kzFZl77cPY1vmbl740V6h2Dk7H8DdQAP8CUd2AgCPsrIFJKI8j02wB2jawa0
394
+ Meet at the north gate at 06:00.
395
+ ```
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")`.
400
+
401
+ With a password instead of a stored key, no key store is involved:
402
+
403
+ ```python
404
+ token = yace.encrypt("for your eyes only", password="correct horse battery staple")
405
+ print(yace.decrypt(token, password="correct horse battery staple"))
406
+ ```
407
+
408
+ ```text
409
+ for your eyes only
410
+ ```
411
+
412
+ `decrypt()` returns `str` for text and `bytes` for bytes. It also offers:
413
+
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
417
+ `DecryptionError`.
418
+ - Key sharing: export a key with `KeyStore.export_key("team")` (CLI:
419
+ `yace-codec keys export team`) and import it with
420
+ `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.
422
+
423
+ ### What a yace2 token reveals
424
+
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.
429
+
430
+ ### How encryption works
431
+
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.
435
+ 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.
437
+ 4. A frame holds the data type, the creation time, the length, the data, and zero
438
+ padding up to a Padmé length.
439
+ 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.
443
+ 7. The salt, ciphertext, and tag are written as letters and digits: every 8 bytes
444
+ become 11 characters ("YACE62").
445
+
446
+ Every building block comes from Python's standard library (`hashlib`, `hmac`, and
447
+ `secrets`).
448
+
369
449
  ## Command line
370
450
 
371
451
  Installing the package adds the `yace-codec` command (`python -m yace_codec` works
@@ -398,6 +478,34 @@ $ yace-codec encode --binary photo.jpg -o photo.yace
398
478
  $ yace-codec decode --binary photo.yace -o photo-copy.jpg
399
479
  ```
400
480
 
481
+ Encrypt and decrypt (any file works, byte for byte):
482
+
483
+ ```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:
495
+ Shopping list:
496
+ - milk
497
+ - 12 eggs
498
+
499
+ $ yace-codec keys list
500
+ default
501
+ team
502
+ ```
503
+
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.
508
+
401
509
  Inspect and share tables:
402
510
 
403
511
  ```console
@@ -435,6 +543,11 @@ yace-codec: error: groceries.json holds tables for: groceries. Name the family y
435
543
  | `tables list` | `-k/--keystore PATH` |
436
544
  | `tables export NAME` | `-o/--output PATH`, `-k/--keystore PATH` |
437
545
  | `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` |
548
+ | `keys list` | `-k/--keystore PATH` |
549
+ | `keys export NAME` | `-o/--output PATH`, `-k/--keystore PATH` |
550
+ | `keys import FILE` | `--name NAME` or `--all-keys` (one is required), `--overwrite`, `-k/--keystore PATH` |
438
551
 
439
552
  Text mode reads and writes UTF-8 and keeps line endings unchanged, so `decode`
440
553
  returns exactly the bytes that were encoded. Token and JSON files saved by Windows
@@ -453,6 +566,8 @@ Everything below is importable from `yace_codec`.
453
566
  | `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). |
454
567
  | `encode_bytes(data, name="default", *, length=2, iterations=3, keystore=None) -> str` | Encode any bytes (base64 first). |
455
568
  | `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. |
456
571
  | `parse_header(token) -> Header` | Read a token's header without decoding it. |
457
572
  | `default_keystore_path() -> pathlib.Path` | Where the default key store lives (an absolute path). |
458
573
 
@@ -461,9 +576,10 @@ default key store.
461
576
 
462
577
  ### `Codec(keystore=None, *, rng=None)`
463
578
 
464
- Encodes and decodes with one key store. It has the same `encode`, `decode`,
465
- `encode_bytes`, and `decode_bytes` methods as the functions above (without the
466
- `keystore` argument), and a `keystore` attribute. `rng` replaces the system random
579
+ Encodes, decodes, encrypts, and decrypts with one key store. It has the same
580
+ `encode`, `decode`, `encode_bytes`, `decode_bytes`, `encrypt`, and `decrypt`
581
+ methods as the functions above (without the `keystore` argument), and a
582
+ `keystore` attribute. `rng` replaces the system random
467
583
  source with a `random.Random`, for reproducible tests only.
468
584
 
469
585
  ### `KeyStore(path)`
@@ -483,6 +599,11 @@ the `KeyStore` is created. The file is created on the first write.
483
599
  | `families() -> list[Family]` | Tables grouped as `Family(name, length, layers)`. |
484
600
  | `export_family(family) -> dict` | All tables of a family, as JSON-ready data. |
485
601
  | `import_tables(tables, *, overwrite=False, family=None) -> ImportResult` | Add tables; with `family`, only that family's. Returns `ImportResult(added, unchanged, replaced)`. |
602
+ | `ensure_key(name, *, rng=None) -> bytes` | Return a secret key for `encrypt`, generating it if missing. |
603
+ | `get_key(name) -> bytes`, `find_key(name) -> bytes \| None` | Return a secret key. |
604
+ | `key_names() -> list[str]` | Names of the stored secret keys. |
605
+ | `export_key(name) -> dict` | A secret key as JSON-ready data. |
606
+ | `import_keys(keys, *, overwrite=False, name=None) -> ImportResult` | Add secret keys; with `name`, only that key. |
486
607
  | `reload()` | Drop cached data and re-read the file on next use. |
487
608
  | `name in store`, `len(store)` | Membership test and table count. |
488
609
 
@@ -507,13 +628,15 @@ YaceError
507
628
  ├── EncodingError invalid settings (also a ValueError)
508
629
  │ └── UnsupportedCharacterError .characters, .table_name
509
630
  ├── 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
510
633
  ├── TableNotFoundError a needed table is missing; .table_name (also a LookupError)
511
634
  └── KeyStoreError unreadable, invalid, or unwritable key store
512
635
  ```
513
636
 
514
637
  ### Constants
515
638
 
516
- `DEFAULT_FAMILY` (`"default"`), `DEFAULT_LENGTH` (2), `DEFAULT_ITERATIONS` (3),
639
+ `DEFAULT_FAMILY` (`"default"`), `DEFAULT_KEY` (`"default"`), `DEFAULT_LENGTH` (2), `DEFAULT_ITERATIONS` (3),
517
640
  `MIN_LENGTH` (2), `MAX_LENGTH` (62), `MAX_ITERATIONS` (32), `MAX_ENCODED_CHARS`
518
641
  (2<sup>25</sup>), `SUPPORTED_CHARACTERS`, `ENV_VAR` (`"YACE_CODEC_KEYSTORE"`), and
519
642
  `__version__`.
@@ -538,6 +661,9 @@ original scripts' `encodings.json`:
538
661
  {"team-3:1": {"!": "B1x", "a": "yI4", "...": "...", " ": ["6nM", "..."], "bin": ["EbC", "..."]}}
539
662
  ```
540
663
 
664
+ Secret keys for `encrypt` live in the same file, as entries named `key:<name>` that
665
+ hold a base64 `secret` of 32 random bytes.
666
+
541
667
  How the key store behaves:
542
668
 
543
669
  - **Existing tables are never changed.** New tables are only added, except through
@@ -557,8 +683,8 @@ How the key store behaves:
557
683
  `KeyStoreError` and the file is not overwritten. Entries the package does not
558
684
  understand are kept.
559
685
 
560
- **Back up your key store.** If you lose it, every token encoded with its tables
561
- becomes undecodable. Anyone who has it can decode your tokens.
686
+ **Back up your key store.** If you lose it, every token encoded with its tables or
687
+ encrypted with its keys becomes unreadable. Anyone who has it can read those tokens.
562
688
 
563
689
  ## Limits and edge cases
564
690
 
@@ -577,6 +703,8 @@ becomes undecodable. Anyone who has it can decode your tokens.
577
703
  token character, so tokens near the size limit need 1–2 GB. YACE is meant for
578
704
  strings and small to medium files, not streams.
579
705
  - **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.
580
708
  - **Whitespace.** Decoding ignores whitespace in a token, so wrapped lines, CRLF line
581
709
  endings, and leading blank lines are fine.
582
710
  - **Corruption.** Decoding raises `DecodingError` when a chunk is not a valid code,
@@ -595,7 +723,9 @@ becomes undecodable. Anyone who has it can decode your tokens.
595
723
 
596
724
  ## Security
597
725
 
598
- - YACE is a randomised substitution code with a keyless, public chunk shuffle.
726
+ ### Encoding (`encode`)
727
+
728
+ - YACE encoding is a randomised substitution code with a keyless, public chunk shuffle.
599
729
  Within a family, every character except the space always has the same code. So
600
730
  frequency analysis over enough tokens recovers the text, and a couple of tokens
601
731
  whose text an attacker knows can be enough to decode other tokens.
@@ -606,8 +736,24 @@ becomes undecodable. Anyone who has it can decode your tokens.
606
736
  - The key store is the key. Store it like a credential, share single families with
607
737
  `tables export` instead of sharing the whole file, and import only the family you
608
738
  expect from files other people send you.
609
- - Use real encryption (for example AES-GCM or Fernet from
610
- [`cryptography`](https://cryptography.io/)) whenever confidentiality matters.
739
+ - Use `encrypt()` whenever confidentiality matters.
740
+
741
+ ### Encryption (`encrypt`)
742
+
743
+ - 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
746
+ assembled them and nobody has audited the result. For high-stakes secrets, use an
747
+ 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),
755
+ and pure Python gives no protection against attackers who can measure timing or
756
+ memory on your machine.
611
757
 
612
758
  To report a bug, including a security issue in the code, please
613
759
  [open an issue](https://github.com/thisismudith/YACE/issues).