pq_crypto-seal 0.1.0 → 0.1.1

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/FORMAT.md +10 -1
  4. data/GET_STARTED.md +10 -0
  5. data/README.md +19 -3
  6. data/RELEASING.md +1 -1
  7. data/SECURITY.md +18 -2
  8. data/VENDORING.md +6 -0
  9. data/lib/pq_crypto/seal/binary.rb +50 -41
  10. data/lib/pq_crypto/seal/core.rb +184 -337
  11. data/lib/pq_crypto/seal/envelope.rb +158 -0
  12. data/lib/pq_crypto/seal/files.rb +142 -0
  13. data/lib/pq_crypto/seal/format.rb +193 -165
  14. data/lib/pq_crypto/seal/io.rb +85 -290
  15. data/lib/pq_crypto/seal/io_helpers.rb +184 -0
  16. data/lib/pq_crypto/seal/models.rb +46 -0
  17. data/lib/pq_crypto/seal/padding.rb +183 -28
  18. data/lib/pq_crypto/seal/recipients.rb +245 -0
  19. data/lib/pq_crypto/seal/resource_limits.rb +56 -0
  20. data/lib/pq_crypto/seal/secrets.rb +21 -0
  21. data/lib/pq_crypto/seal/streaming.rb +244 -0
  22. data/lib/pq_crypto/seal/version.rb +2 -1
  23. data/lib/pq_crypto/seal.rb +17 -5
  24. data/pq_crypto-seal.gemspec +23 -8
  25. metadata +11 -94
  26. data/.github/workflows/ci.yml +0 -66
  27. data/Gemfile +0 -7
  28. data/Rakefile +0 -19
  29. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l.c +0 -246
  30. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.c +0 -73
  31. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.h +0 -9
  32. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.c +0 -70
  33. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.h +0 -9
  34. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_common.h +0 -691
  35. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.c +0 -77
  36. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.h +0 -9
  37. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.c +0 -86
  38. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.h +0 -9
  39. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.c +0 -63
  40. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.h +0 -9
  41. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/implementations.h +0 -40
  42. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2.c +0 -247
  43. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.c +0 -108
  44. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.h +0 -9
  45. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.c +0 -103
  46. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.h +0 -9
  47. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.c +0 -79
  48. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.h +0 -11
  49. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_common.h +0 -795
  50. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.c +0 -111
  51. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.h +0 -9
  52. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.c +0 -96
  53. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.h +0 -9
  54. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/implementations.h +0 -40
  55. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4.c +0 -252
  56. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.c +0 -117
  57. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.h +0 -9
  58. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.c +0 -111
  59. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.h +0 -9
  60. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.c +0 -112
  61. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.h +0 -11
  62. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.c +0 -85
  63. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.h +0 -11
  64. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_common.h +0 -808
  65. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.c +0 -119
  66. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.h +0 -9
  67. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.c +0 -104
  68. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.h +0 -9
  69. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/implementations.h +0 -41
  70. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2.c +0 -247
  71. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.c +0 -103
  72. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.h +0 -9
  73. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.c +0 -101
  74. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.h +0 -9
  75. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.c +0 -74
  76. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.h +0 -11
  77. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_common.h +0 -790
  78. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.c +0 -109
  79. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.h +0 -9
  80. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.c +0 -91
  81. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.h +0 -9
  82. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/implementations.h +0 -41
  83. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4.c +0 -252
  84. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.c +0 -112
  85. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.h +0 -9
  86. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.c +0 -109
  87. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.h +0 -9
  88. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.c +0 -107
  89. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.h +0 -11
  90. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.c +0 -79
  91. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.h +0 -11
  92. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_common.h +0 -805
  93. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.c +0 -117
  94. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.h +0 -9
  95. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.c +0 -99
  96. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.h +0 -9
  97. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/implementations.h +0 -42
  98. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf.c +0 -87
  99. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128l.c +0 -20
  100. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x2.c +0 -20
  101. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x4.c +0 -20
  102. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256.c +0 -20
  103. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x2.c +0 -20
  104. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x4.c +0 -20
  105. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_internal.h +0 -152
  106. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.c +0 -268
  107. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.h +0 -165
  108. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_variant.h +0 -1119
  109. data/fuzz/decrypt_fuzz.rb +0 -119
  110. data/script/vendor_libs.rb +0 -79
  111. data/test/aegis_vectors_test.rb +0 -54
  112. data/test/golden_vectors_test.rb +0 -183
  113. data/test/io_test.rb +0 -125
  114. data/test/native_test.rb +0 -36
  115. data/test/seal_test.rb +0 -238
  116. data/test/test_helper.rb +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f0508e0149f076a9f8d6c9a829b516a41e2998dc5af916db9d0d4696349d36c
4
- data.tar.gz: ce0dd7ba92b94ae212f22ea73cc9754b2028da7037a46539c5408dd60bae961b
3
+ metadata.gz: aa301e0c50869cf3f9e1fec6924920573f895a4aa3ba9cf66cf63675a1ed0f60
4
+ data.tar.gz: 73f8cb33894474e63326d0ad43cad5e4e230c4c3050f5e145a7e67e9966ca027
5
5
  SHA512:
6
- metadata.gz: 248f9187168dd55054827e08dbfcf771555101e00f748cdcef55dbaed31ba4c4fb98ce6f6017c05c00bc5f78de23cfcd4c457541eda93cc2537be8be749711e6
7
- data.tar.gz: b96cd381d599e6e9a52e0eba84de72500e6746e744183c5d383ed61d31305874393d640c8a1a79dbf288c7db1daeb1379ffc3c125443b5c0c4b2a39b14088402
6
+ metadata.gz: 6749ce2e5b6e26d85bf33cd0aa93a4ce82b8fb710bbfc550404551567eab4006649e03f14088cc266786dbc9a6eb358367605eaf1145c6482857c0f6be1ba595
7
+ data.tar.gz: 61fbbb576c26adb08279357ed800d5245e80d3e6b264992e4afcc7a30972b8fdeae9621fd30bc9a5fcac407d004d72a07925c26e0614cfaaa38e2d504e3d5bf6
data/CHANGELOG.md CHANGED
@@ -1,7 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1
4
+
5
+ - Release hygiene: CI Actions pinned by commit SHA, `permissions: contents: read`,
6
+ fuzz + gem-smoke + `release_contract_check` jobs; ignore `.DS_Store` / binaries.
7
+ - Aggressive internal refactor: parsing, recipient wrapping, one-shot operations,
8
+ streaming, atomic files, padding policies, and resource limits now live in
9
+ focused objects instead of large branching methods. Public API and wire v1
10
+ remain unchanged.
11
+ - Padding enforcement now validates both policy id and the canonical target;
12
+ parameterized fixed/bucket requirements cannot masquerade as one another.
13
+ - Added exact deterministic envelope/stream equivalence, official X-Wing
14
+ draft-10 decapsulation KATs, partial-read/write regression tests, and a
15
+ release-contract checker.
16
+ - Release gem contains production code plus only the AEGIS-256 sources needed
17
+ to compile; tests, CI, fuzz harnesses, RAF, and unused AEGIS families stay out.
18
+ - CI restores pinned Actions, sanitizer fuzz smoke, and isolated built-gem E2E.
19
+
20
+ - IO: all output paths use `write_all` (reject silent partial writes).
21
+ - Decrypt APIs accept `required_padding:` and can enforce Padmé / none / fixed /
22
+ buckets after successful AEAD (wire format unchanged).
23
+ - Runtime dependency pinned to `pq_crypto = 0.6.4`.
24
+ - Resource limits applied to `inspect_envelope`, `rebuild_recipients`,
25
+ `rotate_dek`, and `rebuild_recipients_file`.
26
+ - Default one-shot limits lowered to 64 MiB (use file/IO APIs for larger docs).
27
+ - Document that `rotate_dek` allocates a new `payload_id`.
28
+ - CI: Actions pinned by commit SHA; `permissions: contents: read`; fuzz and
29
+ gem-smoke jobs restored.
30
+ - Repository hygiene: ignore `.DS_Store` and built binaries.
31
+
3
32
  ## 0.1.0
4
33
 
34
+
5
35
  - AEGIS streaming state rejects `update`/`final` before `initialize` (`mode` guard).
6
36
  - Encrypt path wipes the internal plaintext copy in `ensure` (caller buffers unchanged).
7
37
  - CI: Actions pinned by commit SHA, `permissions: contents: read`, fuzz + gem-smoke jobs.
data/FORMAT.md CHANGED
@@ -131,9 +131,18 @@ slot_size: 2048 bytes (configurable 2048..8192, multiple of 256)
131
131
  recipient_capacity: 4 (maximum 32; never derived automatically from ACL size)
132
132
  padding: Padmé over the complete final envelope size
133
133
  max_staging_bytes: ~1 GiB + private-metadata ceiling
134
- max_plaintext_bytes:~1 GiB
134
+ max_plaintext_bytes:64 MiB by default
135
135
  ```
136
136
 
137
137
  Parsers reject trailing bytes on the one-shot and default IO paths. Framed
138
138
  stream consumers that intentionally embed one envelope inside a larger stream
139
139
  must use `encrypt_frame_io` / `decrypt_frame_io`.
140
+
141
+
142
+ ## Receiver-side padding checks (0.1.1+)
143
+
144
+ `padding_policy_id` is authenticated but does not by itself encode fixed targets
145
+ or bucket lists. Decryptors that need enforcement pass `required_padding:`. The
146
+ receiver verifies the authenticated policy id and recomputes the canonical target
147
+ from the authenticated inner lengths. Fixed and bucket policies require the
148
+ expected target or bucket list from the application.
data/GET_STARTED.md CHANGED
@@ -28,3 +28,13 @@ Recipient-section rebuilds always require the application's complete,
28
28
  authoritative public-key ACL. They do not revoke access to plaintext or old
29
29
  envelope copies. Use `rotate_dek`/`rotate_dek_file` when a new payload key and
30
30
  full re-encryption are required.
31
+
32
+ ## Large documents
33
+
34
+ Default one-shot limits are 64 MiB (`max_plaintext_bytes` / related staging and
35
+ envelope ceilings). For larger objects use `encrypt_file` / `decrypt_file` or
36
+ `encrypt_io` / `decrypt_io` and raise the limits explicitly, for example:
37
+
38
+ ```ruby
39
+ PQCrypto::Seal.decrypt_file(src, dst, with: credentials, max_plaintext_bytes: 512 * 1024 * 1024)
40
+ ```
data/README.md CHANGED
@@ -12,10 +12,10 @@ random DEK → AEGIS-256 → document
12
12
  `libaegis` 0.10.3 is vendored and compiled into the extension. The existing
13
13
  `pq_crypto` gem supplies the hybrid KEM and OpenSSL-backed primitives.
14
14
 
15
- > **Status:** 0.1.0 is experimental cryptographic software; format v1; not
15
+ > **Status:** 0.1.1 is experimental cryptographic software; format v1; not
16
16
  > independently audited. Read `SECURITY.md` before using it for irreplaceable data.
17
17
  >
18
- > `wrap_suite_id = 1` is pinned to `:ml_kem_768_x25519_xwing` and `pq_crypto ~> 0.6.4`.
18
+ > `wrap_suite_id = 1` is pinned to `:ml_kem_768_x25519_xwing` and `pq_crypto = 0.6.4`.
19
19
 
20
20
  ## Installation
21
21
 
@@ -129,7 +129,7 @@ rotated = PQCrypto::Seal.rotate_dek(
129
129
 
130
130
  `rebuild_recipients` preserves the DEK and encrypted payload. It is an
131
131
  operational migration of the current canonical copy, not protection against old
132
- saved envelopes. `rotate_dek` creates a new DEK and re-encrypts the payload; it
132
+ saved envelopes. `rotate_dek` creates a new DEK, a **new payload_id**, and re-encrypts the payload; it
133
133
  still cannot erase knowledge from prior copies. Rotation preserves the current
134
134
  final envelope size by default (`padding: :preserve`); pass `:padme`, `:none`, or
135
135
  an explicit padding policy to recalculate it.
@@ -213,3 +213,19 @@ Set `PQC_SEAL_SANITIZE=1` when compiling under ASan/UBSan. The suite covers
213
213
  AEGIS one-shot/incremental equivalence, tampering, multi-recipient opening,
214
214
  full-envelope padding, recipient rebuilds, staged file publication, and parser
215
215
  limits.
216
+
217
+
218
+ ## Padding enforcement on decrypt
219
+
220
+ After AEAD verification you may require a padding policy:
221
+
222
+ ```ruby
223
+ PQCrypto::Seal.decrypt(envelope, with: credentials, required_padding: :padme)
224
+ PQCrypto::Seal.decrypt(envelope, with: credentials, required_padding: :from_header)
225
+ PQCrypto::Seal.decrypt(envelope, with: credentials, required_padding: { to: 4096 })
226
+ ```
227
+
228
+ Omitting `required_padding` keeps padding enforcement optional. When supplied,
229
+ the decryptor verifies both the authenticated policy id and the canonical target
230
+ computed from the authenticated content and metadata lengths. Fixed and bucket
231
+ policies require the expected target or bucket list from the application.
data/RELEASING.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Releasing
2
2
 
3
3
  1. Run `bundle exec ruby script/vendor_libs.rb --check`.
4
- 2. Confirm `pq_crypto` dependency remains `~> 0.6.4` and `WRAP_KEM_ALGORITHM`
4
+ 2. Confirm `pq_crypto` dependency remains `= 0.6.4` and `WRAP_KEM_ALGORITHM`
5
5
  is still `:ml_kem_768_x25519_xwing` with the documented sizes.
6
6
  3. Run the full CI matrix and sanitizer job.
7
7
  4. Build with `gem build pq_crypto-seal.gemspec`.
data/SECURITY.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Security
2
2
 
3
3
  This project has not received an independent cryptographic or implementation
4
- audit. Do not treat version 0.1.0 as a substitute for a reviewed storage design.
4
+ audit. Do not treat version 0.1.1 as a substitute for a reviewed storage design.
5
5
 
6
6
  Experimental cryptographic software; format v1; not independently audited.
7
7
 
@@ -44,6 +44,22 @@ callers that need residual-data hygiene must clear their own buffers.
44
44
  `Opened#data` / `Opened#metadata` returned from `open` are the caller's
45
45
  responsibility after use.
46
46
 
47
+
48
+ ## payload_id stability
49
+
50
+ `payload_id` is stable across `rebuild_recipients` / add / drop operations.
51
+ `rotate_dek` re-encrypts the payload with a fresh DEK and **allocates a new
52
+ `payload_id`** — treat the result as a new document identity. Applications that
53
+ need a stable external id across DEK rotation must track it outside the envelope.
54
+
55
+ ## Padding policy enforcement
56
+
57
+ The authenticated header carries `padding_policy_id` only. Receivers that require
58
+ a specific length-hiding policy should pass `required_padding:` to decrypt/open
59
+ (for example `required_padding: :padme` or `required_padding: :from_header`).
60
+ The receiver then checks both the policy id and the canonical target computed from
61
+ authenticated inner lengths. Without that argument the field is informational.
62
+
47
63
  ## Envelope digests
48
64
 
49
65
  `Seal.digest` hashes the complete envelope, including the mutable recipient
@@ -77,7 +93,7 @@ forensics may still retain remnants; use a suitably protected staging filesystem
77
93
 
78
94
  `wrap_suite_id = 1` is permanently bound to MLKEM768-X25519 / X-Wing (draft-10
79
95
  compatible) with fixed public-key (1216), ciphertext (1120), and shared-secret
80
- (32) sizes, as implemented by `pq_crypto ~> 0.6.4`. Seal does not follow
96
+ (32) sizes, as implemented by `pq_crypto = 0.6.4`. Seal does not follow
81
97
  `HybridKEM::CANONICAL_ALGORITHM`; a future change of that constant must not
82
98
  alter suite 1 wire bytes. Always generate keys with
83
99
  `PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)`.
data/VENDORING.md CHANGED
@@ -12,3 +12,9 @@ libaegis 0.10.3.
12
12
  The complete source snapshot is retained, while the Ruby extension compiles only the AEGIS-256 and common implementation files it exposes. No system libaegis is
13
13
  loaded at runtime. Update the version, archive checksum, source snapshot, KATs,
14
14
  and cross-backend tests together.
15
+
16
+ The git tree keeps the full upstream snapshot so `script/vendor_libs.rb --check`
17
+ can prove byte-identity against the audited archive. The published gem package
18
+ ships only the AEGIS-256 family plus shared `common`/`include` sources required
19
+ to compile the extension (see `pq_crypto-seal.gemspec`); unused families and RAF
20
+ remain in the repository for provenance and are not loaded at runtime.
@@ -3,59 +3,68 @@
3
3
  module PQCrypto
4
4
  module Seal
5
5
  module Binary
6
- module_function
6
+ INTEGER_TYPES = {
7
+ u8: [1, "C", 0xff],
8
+ u16: [2, "n", 0xffff],
9
+ u32: [4, "N", 0xffff_ffff],
10
+ u64: [8, "Q>", 0xffff_ffff_ffff_ffff]
11
+ }.freeze
7
12
 
8
- def u8(value)
9
- [integer!(value, 0xff)].pack("C")
10
- end
13
+ class Reader
14
+ attr_reader :offset
11
15
 
12
- def u16(value)
13
- [integer!(value, 0xffff)].pack("n")
14
- end
16
+ def initialize(bytes, offset = 0)
17
+ @bytes = String(bytes).b
18
+ @offset = Integer(offset)
19
+ end
15
20
 
16
- def u32(value)
17
- [integer!(value, 0xffff_ffff)].pack("N")
18
- end
21
+ INTEGER_TYPES.each do |name, (length, directive, _max)|
22
+ define_method(name) { bytes(length).unpack1(directive) }
23
+ end
19
24
 
20
- def u64(value)
21
- [integer!(value, 0xffff_ffff_ffff_ffff)].pack("Q>")
22
- end
25
+ def bytes(length)
26
+ length = Integer(length)
27
+ Binary.ensure_available!(@bytes, offset, length)
28
+ value = @bytes.byteslice(offset, length).b
29
+ @offset += length
30
+ value
31
+ end
23
32
 
24
- def read_u8(bytes, offset)
25
- ensure_available!(bytes, offset, 1)
26
- [bytes.getbyte(offset), offset + 1]
33
+ def finished?
34
+ offset == @bytes.bytesize
35
+ end
27
36
  end
28
37
 
29
- def read_u16(bytes, offset)
30
- ensure_available!(bytes, offset, 2)
31
- [bytes.byteslice(offset, 2).unpack1("n"), offset + 2]
32
- end
38
+ class << self
39
+ INTEGER_TYPES.each do |name, (_length, directive, max)|
40
+ define_method(name) { |value| [integer!(value, max)].pack(directive) }
41
+ define_method("read_#{name}") { |bytes, offset| read_with(bytes, offset, &name) }
42
+ end
33
43
 
34
- def read_u32(bytes, offset)
35
- ensure_available!(bytes, offset, 4)
36
- [bytes.byteslice(offset, 4).unpack1("N"), offset + 4]
37
- end
44
+ def read_bytes(bytes, offset, length)
45
+ read_with(bytes, offset) { |reader| reader.bytes(length) }
46
+ end
38
47
 
39
- def read_u64(bytes, offset)
40
- ensure_available!(bytes, offset, 8)
41
- [bytes.byteslice(offset, 8).unpack1("Q>"), offset + 8]
42
- end
48
+ def ensure_available!(bytes, offset, length)
49
+ offset, length = Integer(offset), Integer(length)
43
50
 
44
- def read_bytes(bytes, offset, length)
45
- raise FormatError, "negative length" if length.negative?
46
- ensure_available!(bytes, offset, length)
47
- [bytes.byteslice(offset, length).b, offset + length]
48
- end
51
+ raise FormatError, "invalid offset" if offset.negative? || length.negative?
52
+ raise FormatError, "truncated envelope" if offset > bytes.bytesize || length > bytes.bytesize - offset
53
+ end
49
54
 
50
- def ensure_available!(bytes, offset, length)
51
- raise FormatError, "invalid offset" if offset.negative? || length.negative?
52
- raise FormatError, "truncated envelope" if offset > bytes.bytesize || length > bytes.bytesize - offset
53
- end
55
+ def integer!(value, max)
56
+ number = Integer(value)
57
+ raise RangeError, "integer is out of range" if number.negative? || number > max
58
+
59
+ number
60
+ end
61
+
62
+ private
54
63
 
55
- def integer!(value, max)
56
- number = Integer(value)
57
- raise RangeError, "integer is out of range" if number.negative? || number > max
58
- number
64
+ def read_with(bytes, offset)
65
+ reader = Reader.new(bytes, offset)
66
+ [yield(reader), reader.offset]
67
+ end
59
68
  end
60
69
  end
61
70
  end