pq_crypto-seal 0.1.0

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 (143) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +66 -0
  3. data/CHANGELOG.md +44 -0
  4. data/FORMAT.md +139 -0
  5. data/GET_STARTED.md +30 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +215 -0
  9. data/RELEASING.md +34 -0
  10. data/Rakefile +19 -0
  11. data/SECURITY.md +85 -0
  12. data/VENDORING.md +14 -0
  13. data/ext/pq_crypto_seal/aegis_unused_stubs.c +20 -0
  14. data/ext/pq_crypto_seal/extconf.rb +296 -0
  15. data/ext/pq_crypto_seal/pq_crypto_seal.c +390 -0
  16. data/ext/pq_crypto_seal/vendor/libaegis/LICENSE +21 -0
  17. data/ext/pq_crypto_seal/vendor/libaegis/TREE_SHA256 +1 -0
  18. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l.c +246 -0
  19. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.c +73 -0
  20. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.h +9 -0
  21. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.c +70 -0
  22. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.h +9 -0
  23. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_common.h +691 -0
  24. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.c +77 -0
  25. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.h +9 -0
  26. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.c +86 -0
  27. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.h +9 -0
  28. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.c +63 -0
  29. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.h +9 -0
  30. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/implementations.h +40 -0
  31. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2.c +247 -0
  32. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.c +108 -0
  33. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.h +9 -0
  34. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.c +103 -0
  35. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.h +9 -0
  36. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.c +79 -0
  37. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.h +11 -0
  38. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_common.h +795 -0
  39. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.c +111 -0
  40. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.h +9 -0
  41. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.c +96 -0
  42. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.h +9 -0
  43. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/implementations.h +40 -0
  44. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4.c +252 -0
  45. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.c +117 -0
  46. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.h +9 -0
  47. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.c +111 -0
  48. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.h +9 -0
  49. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.c +112 -0
  50. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.h +11 -0
  51. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.c +85 -0
  52. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.h +11 -0
  53. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_common.h +808 -0
  54. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.c +119 -0
  55. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.h +9 -0
  56. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.c +104 -0
  57. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.h +9 -0
  58. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/implementations.h +41 -0
  59. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256.c +240 -0
  60. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_aesni.c +68 -0
  61. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_aesni.h +9 -0
  62. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_altivec.c +68 -0
  63. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_altivec.h +9 -0
  64. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_common.h +673 -0
  65. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_neon_aes.c +75 -0
  66. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_neon_aes.h +9 -0
  67. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_soft.c +58 -0
  68. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_soft.h +9 -0
  69. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/implementations.h +39 -0
  70. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2.c +247 -0
  71. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.c +103 -0
  72. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.h +9 -0
  73. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.c +101 -0
  74. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.h +9 -0
  75. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.c +74 -0
  76. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.h +11 -0
  77. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_common.h +790 -0
  78. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.c +109 -0
  79. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.h +9 -0
  80. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.c +91 -0
  81. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.h +9 -0
  82. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/implementations.h +41 -0
  83. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4.c +252 -0
  84. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.c +112 -0
  85. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.h +9 -0
  86. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.c +109 -0
  87. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.h +9 -0
  88. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.c +107 -0
  89. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.h +11 -0
  90. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.c +79 -0
  91. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.h +11 -0
  92. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_common.h +805 -0
  93. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.c +117 -0
  94. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.h +9 -0
  95. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.c +99 -0
  96. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.h +9 -0
  97. data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/implementations.h +42 -0
  98. data/ext/pq_crypto_seal/vendor/libaegis/src/common/common.c +81 -0
  99. data/ext/pq_crypto_seal/vendor/libaegis/src/common/common.h +144 -0
  100. data/ext/pq_crypto_seal/vendor/libaegis/src/common/cpu.c +378 -0
  101. data/ext/pq_crypto_seal/vendor/libaegis/src/common/cpu.h +34 -0
  102. data/ext/pq_crypto_seal/vendor/libaegis/src/common/keccak.c +187 -0
  103. data/ext/pq_crypto_seal/vendor/libaegis/src/common/keccak.h +13 -0
  104. data/ext/pq_crypto_seal/vendor/libaegis/src/common/softaes.c +338 -0
  105. data/ext/pq_crypto_seal/vendor/libaegis/src/common/softaes.h +69 -0
  106. data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis.h +77 -0
  107. data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128l.h +311 -0
  108. data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128x2.h +313 -0
  109. data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128x4.h +313 -0
  110. data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256.h +311 -0
  111. data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256x2.h +313 -0
  112. data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256x4.h +313 -0
  113. data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis_raf.h +580 -0
  114. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf.c +87 -0
  115. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128l.c +20 -0
  116. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x2.c +20 -0
  117. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x4.c +20 -0
  118. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256.c +20 -0
  119. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x2.c +20 -0
  120. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x4.c +20 -0
  121. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_internal.h +152 -0
  122. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.c +268 -0
  123. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.h +165 -0
  124. data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_variant.h +1119 -0
  125. data/fuzz/decrypt_fuzz.rb +119 -0
  126. data/lib/pq_crypto/seal/binary.rb +62 -0
  127. data/lib/pq_crypto/seal/core.rb +398 -0
  128. data/lib/pq_crypto/seal/errors.rb +15 -0
  129. data/lib/pq_crypto/seal/format.rb +275 -0
  130. data/lib/pq_crypto/seal/io.rb +336 -0
  131. data/lib/pq_crypto/seal/native.rb +13 -0
  132. data/lib/pq_crypto/seal/padding.rb +47 -0
  133. data/lib/pq_crypto/seal/version.rb +6 -0
  134. data/lib/pq_crypto/seal.rb +32 -0
  135. data/pq_crypto-seal.gemspec +37 -0
  136. data/script/vendor_libs.rb +79 -0
  137. data/test/aegis_vectors_test.rb +54 -0
  138. data/test/golden_vectors_test.rb +183 -0
  139. data/test/io_test.rb +125 -0
  140. data/test/native_test.rb +36 -0
  141. data/test/seal_test.rb +238 -0
  142. data/test/test_helper.rb +6 -0
  143. metadata +244 -0
data/test/seal_test.rb ADDED
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+ require_relative "test_helper"
3
+
4
+ class SealTest < Minitest::Test
5
+ def setup
6
+ @alice = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
7
+ @bob = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
8
+ end
9
+
10
+ def test_multi_recipient_round_trip_and_padding
11
+ data = ("image" * 10_000).b
12
+ envelope = PQCrypto::Seal.encrypt(
13
+ data, to: [@alice.public_key, @bob.public_key],
14
+ metadata: "private", public_metadata: "public"
15
+ )
16
+ alice = PQCrypto::Seal.open(envelope, with: @alice)
17
+ bob = PQCrypto::Seal.open(envelope, with: @bob)
18
+ assert_equal data, alice.data
19
+ assert_equal data, bob.data
20
+ assert_equal "private", alice.metadata
21
+ assert_equal "public", alice.public_metadata
22
+ assert_equal PQCrypto::Seal::Format::PADDING_PADME, alice.padding_policy_id
23
+ assert_operator envelope.bytesize, :>=, data.bytesize
24
+ end
25
+
26
+ def test_wrong_recipient_and_tampering_fail
27
+ envelope = PQCrypto::Seal.encrypt("secret", to: @alice.public_key)
28
+ stranger = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
29
+ assert_raises(PQCrypto::Seal::RecipientNotFoundError) do
30
+ PQCrypto::Seal.decrypt(envelope, with: stranger)
31
+ end
32
+ broken = envelope.dup
33
+ broken.setbyte(broken.bytesize - 1, broken.getbyte(-1) ^ 1)
34
+ assert_raises(PQCrypto::Seal::AuthenticationError) do
35
+ PQCrypto::Seal.decrypt(broken, with: @alice)
36
+ end
37
+ end
38
+
39
+ def test_rebuild_recipients_preserves_payload
40
+ envelope = PQCrypto::Seal.encrypt("document", to: @alice.public_key)
41
+ rebuilt = PQCrypto::Seal.rebuild_recipients(
42
+ envelope, with: @alice,
43
+ recipients: [@alice.public_key, @bob.public_key]
44
+ )
45
+ assert_equal "document", PQCrypto::Seal.decrypt(rebuilt, with: @bob)
46
+ assert_raises(PQCrypto::Seal::RecipientNotFoundError) do
47
+ PQCrypto::Seal.decrypt(envelope, with: @bob)
48
+ end
49
+ end
50
+
51
+ def test_rebuild_randomizes_entire_recipient_section
52
+ envelope = PQCrypto::Seal.encrypt("document", to: @alice.public_key)
53
+ rebuilt = PQCrypto::Seal.rebuild_recipients(
54
+ envelope, with: @alice, recipients: [@alice.public_key]
55
+ )
56
+ original_info = PQCrypto::Seal.inspect_envelope(envelope)
57
+ rebuilt_info = PQCrypto::Seal.inspect_envelope(rebuilt)
58
+ assert_equal original_info.payload_id, rebuilt_info.payload_id
59
+ refute_equal envelope, rebuilt
60
+ assert_equal "document", PQCrypto::Seal.decrypt(rebuilt, with: @alice)
61
+ end
62
+
63
+ def test_padme_and_none_policy_ids
64
+ data = "x" * 12_345
65
+ metadata = "m" * 19
66
+ envelope = PQCrypto::Seal.encrypt(data, to: @alice.public_key, metadata: metadata, padding: :padme)
67
+ info = PQCrypto::Seal.inspect_envelope(envelope)
68
+ assert_equal PQCrypto::Seal::Format::PADDING_PADME, info.padding_policy_id
69
+ assert_equal data, PQCrypto::Seal.decrypt(envelope, with: @alice)
70
+
71
+ none_env = PQCrypto::Seal.encrypt("hi", to: @alice.public_key, padding: :none)
72
+ assert_equal PQCrypto::Seal::Format::PADDING_NONE,
73
+ PQCrypto::Seal.inspect_envelope(none_env).padding_policy_id
74
+ assert_equal "hi", PQCrypto::Seal.decrypt(none_env, with: @alice)
75
+ end
76
+
77
+ def test_truncation_trailing_bytes_and_payload_tamper_fail
78
+ envelope = PQCrypto::Seal.encrypt("secret" * 100, to: @alice.public_key)
79
+ assert_raises(PQCrypto::Seal::FormatError) do
80
+ PQCrypto::Seal.decrypt(envelope.byteslice(0, envelope.bytesize - 1), with: @alice)
81
+ end
82
+ assert_raises(PQCrypto::Seal::FormatError) do
83
+ PQCrypto::Seal.decrypt(envelope + "x", with: @alice)
84
+ end
85
+
86
+ info = PQCrypto::Seal.inspect_envelope(envelope)
87
+ payload_offset = info.envelope_bytes - info.padded_inner_length - PQCrypto::Seal::Format::TAG_BYTES
88
+ broken = envelope.dup
89
+ broken.setbyte(payload_offset, broken.getbyte(payload_offset) ^ 1)
90
+ assert_raises(PQCrypto::Seal::AuthenticationError) do
91
+ PQCrypto::Seal.decrypt(broken, with: @alice)
92
+ end
93
+ end
94
+
95
+ def test_separately_loaded_credentials_and_duplicate_rejection
96
+ credentials = PQCrypto::Seal.credentials(
97
+ secret_key: @alice.secret_key, public_key: @alice.public_key
98
+ )
99
+ envelope = PQCrypto::Seal.encrypt("x", to: @alice.public_key)
100
+ assert_equal "x", PQCrypto::Seal.decrypt(envelope, with: credentials)
101
+ assert_raises(PQCrypto::Seal::InvalidConfigurationError) do
102
+ PQCrypto::Seal.encrypt("x", to: [@alice.public_key, @alice.public_key])
103
+ end
104
+ end
105
+
106
+ def test_rotate_dek_preserves_envelope_size_by_default
107
+ envelope = PQCrypto::Seal.encrypt(
108
+ "document" * 500, to: @alice.public_key, padding: { to: 20_000 }
109
+ )
110
+ rotated = PQCrypto::Seal.rotate_dek(
111
+ envelope, with: @alice, recipients: [@alice.public_key, @bob.public_key]
112
+ )
113
+ assert_equal envelope.bytesize, rotated.bytesize
114
+ assert_equal "document" * 500, PQCrypto::Seal.decrypt(rotated, with: @bob)
115
+ assert_equal PQCrypto::Seal::Format::PADDING_FIXED,
116
+ PQCrypto::Seal.inspect_envelope(rotated).padding_policy_id
117
+ end
118
+
119
+ def test_capacity_is_fixed_policy
120
+ assert_raises(PQCrypto::Seal::RecipientCapacityExceeded) do
121
+ keys = 5.times.map { PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM).public_key }
122
+ PQCrypto::Seal.encrypt("x", to: keys, recipient_capacity: 4)
123
+ end
124
+ end
125
+
126
+ def test_internals_are_not_public
127
+ refute PQCrypto::Seal.respond_to?(:unwrap_dek)
128
+ refute PQCrypto::Seal.respond_to?(:derive_kek)
129
+ refute PQCrypto::Seal.respond_to?(:build_slot)
130
+ refute PQCrypto::Seal.respond_to?(:parse_envelope)
131
+ refute PQCrypto::Seal.respond_to?(:wipe_string!)
132
+ assert PQCrypto::Seal.respond_to?(:encrypt)
133
+ assert PQCrypto::Seal.respond_to?(:decrypt)
134
+ assert PQCrypto::Seal.respond_to?(:open)
135
+ assert PQCrypto::Seal.respond_to?(:digest)
136
+ end
137
+
138
+ def test_resource_limit_rejects_huge_declared_inner
139
+ envelope = PQCrypto::Seal.encrypt("tiny", to: @alice.public_key, padding: :none)
140
+ assert_raises(PQCrypto::Seal::ResourceLimitError) do
141
+ PQCrypto::Seal.decrypt(envelope, with: @alice, max_staging_bytes: 8)
142
+ end
143
+ end
144
+
145
+ def test_malformed_capacity_is_format_error
146
+ envelope = PQCrypto::Seal.encrypt("x", to: @alice.public_key, padding: :none)
147
+ # capacity sits after magic(8)+ver(1)+hdrlen(4)+suite(2)+lookup(1)+flags(2)+pad_policy(1)+pid(32)+nonce(32)
148
+ offset = 8 + 1 + 4 + 2 + 1 + 2 + 1 + 32 + 32
149
+ broken = envelope.dup
150
+ broken.setbyte(offset, 0xff)
151
+ broken.setbyte(offset + 1, 0xff)
152
+ assert_raises(PQCrypto::Seal::FormatError) do
153
+ PQCrypto::Seal.decrypt(broken, with: @alice)
154
+ end
155
+ end
156
+
157
+ def test_array_credentials_form
158
+ envelope = PQCrypto::Seal.encrypt("array-cred", to: @alice.public_key, padding: :none)
159
+ assert_equal "array-cred",
160
+ PQCrypto::Seal.decrypt(envelope, with: [@alice.secret_key, @alice.public_key])
161
+ end
162
+
163
+ def test_fixed_and_bucket_padding_policies
164
+ fixed = PQCrypto::Seal.encrypt("x", to: @alice.public_key, padding: { to: 12_000 })
165
+ assert_equal 12_000, fixed.bytesize
166
+ assert_equal PQCrypto::Seal::Format::PADDING_FIXED,
167
+ PQCrypto::Seal.inspect_envelope(fixed).padding_policy_id
168
+ assert_equal "x", PQCrypto::Seal.decrypt(fixed, with: @alice)
169
+
170
+ bucketed = PQCrypto::Seal.encrypt("y", to: @alice.public_key, padding: { buckets: [8_000, 16_000, 32_000] })
171
+ assert_includes [8_000, 16_000, 32_000], bucketed.bytesize
172
+ assert_equal PQCrypto::Seal::Format::PADDING_BUCKETS,
173
+ PQCrypto::Seal.inspect_envelope(bucketed).padding_policy_id
174
+ assert_equal "y", PQCrypto::Seal.decrypt(bucketed, with: @alice)
175
+ end
176
+
177
+ def test_digest_changes_on_recipient_rebuild
178
+ envelope = PQCrypto::Seal.encrypt("stable-payload", to: @alice.public_key, padding: :none)
179
+ d1 = PQCrypto::Seal.digest(envelope)
180
+ rebuilt = PQCrypto::Seal.rebuild_recipients(
181
+ envelope, with: @alice, recipients: [@alice.public_key, @bob.public_key]
182
+ )
183
+ d2 = PQCrypto::Seal.digest(rebuilt)
184
+ refute_equal d1, d2
185
+ assert_equal PQCrypto::Seal.open(envelope, with: @alice).payload_id,
186
+ PQCrypto::Seal.open(rebuilt, with: @alice).payload_id
187
+ end
188
+
189
+ def test_wrap_kem_algorithm_is_pinned
190
+ assert_equal :ml_kem_768_x25519_xwing, PQCrypto::Seal::WRAP_KEM_ALGORITHM
191
+ assert_equal 1216, PQCrypto::Seal::Format::XWING_PUBLIC_KEY_BYTES
192
+ assert_equal 1120, PQCrypto::Seal::Format::XWING_CIPHERTEXT_BYTES
193
+ assert_equal 32, PQCrypto::Seal::Format::XWING_SHARED_SECRET_BYTES
194
+ refute PQCrypto::Seal::Format.const_defined?(:XWING_SECRET_KEY_BYTES)
195
+ end
196
+
197
+ def test_index_binding_prevents_naive_slot_duplication
198
+ # wrap_ad / derive_kek bind slot_index, so byte-copying a real slot into
199
+ # another index cannot open a second time for the same recipient.
200
+ envelope = PQCrypto::Seal.encrypt(
201
+ "ambig", to: @alice.public_key, recipient_capacity: 2, slot_size: 2048, padding: :none
202
+ )
203
+ header = PQCrypto::Seal::Format.parse_header(envelope)
204
+ section_offset = header.raw.bytesize
205
+ slots_offset = section_offset + 2 + PQCrypto::Seal::Format::SECTION_ID_BYTES
206
+ slot0 = envelope.byteslice(slots_offset, header.slot_size)
207
+ broken = envelope.dup
208
+ broken[slots_offset + header.slot_size, header.slot_size] = slot0
209
+ assert_equal "ambig", PQCrypto::Seal.decrypt(broken, with: @alice)
210
+ end
211
+
212
+ def test_two_valid_stanzas_for_same_recipient_are_ambiguous
213
+ # Defense-in-depth: a crafted envelope with two correctly-built slots for
214
+ # the same public key (different indices) must not pick an arbitrary DEK.
215
+ envelope = PQCrypto::Seal.encrypt(
216
+ "ambig", to: @alice.public_key, recipient_capacity: 2, slot_size: 2048, padding: :none
217
+ )
218
+ header, section, ciphertext, tag = PQCrypto::Seal.send(:parse_envelope, envelope)
219
+ dek = PQCrypto::Seal.send(:unwrap_dek, header, section, @alice)
220
+ crafted_section = PQCrypto::Seal.send(
221
+ :build_recipient_section,
222
+ recipients: [@alice.public_key, @alice.public_key],
223
+ capacity: 2,
224
+ slot_size: header.slot_size,
225
+ payload_id: header.payload_id,
226
+ header_hash: PQCrypto::Seal.const_get(:Native).sha256(header.raw),
227
+ dek: dek,
228
+ wrap_suite_id: PQCrypto::Seal::Format::WRAP_SUITE_MLKEM768_X25519_AEGIS256
229
+ )
230
+ crafted = header.raw + crafted_section + ciphertext + tag
231
+ assert_raises(PQCrypto::Seal::AmbiguousRecipientStanzas) do
232
+ PQCrypto::Seal.decrypt(crafted, with: @alice)
233
+ end
234
+ ensure
235
+ PQCrypto::Seal.send(:wipe_string!, dek) if defined?(dek)
236
+ end
237
+
238
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "tmpdir"
5
+ require "stringio"
6
+ require "pq_crypto/seal"
metadata ADDED
@@ -0,0 +1,244 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pq_crypto-seal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Roman Khaidarov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-07-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pq_crypto
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.6.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.6.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake-compiler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.2'
69
+ description: Versioned multi-recipient document encryption using PQCrypto hybrid KEM,
70
+ HKDF-SHA256, and vendored AEGIS-256.
71
+ email:
72
+ - roman-haidarov@users.noreply.github.com
73
+ executables: []
74
+ extensions:
75
+ - ext/pq_crypto_seal/extconf.rb
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".github/workflows/ci.yml"
79
+ - CHANGELOG.md
80
+ - FORMAT.md
81
+ - GET_STARTED.md
82
+ - Gemfile
83
+ - LICENSE.txt
84
+ - README.md
85
+ - RELEASING.md
86
+ - Rakefile
87
+ - SECURITY.md
88
+ - VENDORING.md
89
+ - ext/pq_crypto_seal/aegis_unused_stubs.c
90
+ - ext/pq_crypto_seal/extconf.rb
91
+ - ext/pq_crypto_seal/pq_crypto_seal.c
92
+ - ext/pq_crypto_seal/vendor/libaegis/LICENSE
93
+ - ext/pq_crypto_seal/vendor/libaegis/TREE_SHA256
94
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l.c
95
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.c
96
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.h
97
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.c
98
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.h
99
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_common.h
100
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.c
101
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.h
102
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.c
103
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.h
104
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.c
105
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.h
106
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/implementations.h
107
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2.c
108
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.c
109
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.h
110
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.c
111
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.h
112
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.c
113
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.h
114
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_common.h
115
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.c
116
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.h
117
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.c
118
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.h
119
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/implementations.h
120
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4.c
121
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.c
122
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.h
123
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.c
124
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.h
125
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.c
126
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.h
127
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.c
128
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.h
129
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_common.h
130
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.c
131
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.h
132
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.c
133
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.h
134
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/implementations.h
135
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256.c
136
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_aesni.c
137
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_aesni.h
138
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_altivec.c
139
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_altivec.h
140
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_common.h
141
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_neon_aes.c
142
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_neon_aes.h
143
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_soft.c
144
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_soft.h
145
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256/implementations.h
146
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2.c
147
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.c
148
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.h
149
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.c
150
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.h
151
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.c
152
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.h
153
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_common.h
154
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.c
155
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.h
156
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.c
157
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.h
158
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/implementations.h
159
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4.c
160
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.c
161
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.h
162
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.c
163
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.h
164
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.c
165
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.h
166
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.c
167
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.h
168
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_common.h
169
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.c
170
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.h
171
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.c
172
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.h
173
+ - ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/implementations.h
174
+ - ext/pq_crypto_seal/vendor/libaegis/src/common/common.c
175
+ - ext/pq_crypto_seal/vendor/libaegis/src/common/common.h
176
+ - ext/pq_crypto_seal/vendor/libaegis/src/common/cpu.c
177
+ - ext/pq_crypto_seal/vendor/libaegis/src/common/cpu.h
178
+ - ext/pq_crypto_seal/vendor/libaegis/src/common/keccak.c
179
+ - ext/pq_crypto_seal/vendor/libaegis/src/common/keccak.h
180
+ - ext/pq_crypto_seal/vendor/libaegis/src/common/softaes.c
181
+ - ext/pq_crypto_seal/vendor/libaegis/src/common/softaes.h
182
+ - ext/pq_crypto_seal/vendor/libaegis/src/include/aegis.h
183
+ - ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128l.h
184
+ - ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128x2.h
185
+ - ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128x4.h
186
+ - ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256.h
187
+ - ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256x2.h
188
+ - ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256x4.h
189
+ - ext/pq_crypto_seal/vendor/libaegis/src/include/aegis_raf.h
190
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf.c
191
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128l.c
192
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x2.c
193
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x4.c
194
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256.c
195
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x2.c
196
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x4.c
197
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_internal.h
198
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.c
199
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.h
200
+ - ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_variant.h
201
+ - fuzz/decrypt_fuzz.rb
202
+ - lib/pq_crypto/seal.rb
203
+ - lib/pq_crypto/seal/binary.rb
204
+ - lib/pq_crypto/seal/core.rb
205
+ - lib/pq_crypto/seal/errors.rb
206
+ - lib/pq_crypto/seal/format.rb
207
+ - lib/pq_crypto/seal/io.rb
208
+ - lib/pq_crypto/seal/native.rb
209
+ - lib/pq_crypto/seal/padding.rb
210
+ - lib/pq_crypto/seal/version.rb
211
+ - pq_crypto-seal.gemspec
212
+ - script/vendor_libs.rb
213
+ - test/aegis_vectors_test.rb
214
+ - test/golden_vectors_test.rb
215
+ - test/io_test.rb
216
+ - test/native_test.rb
217
+ - test/seal_test.rb
218
+ - test/test_helper.rb
219
+ homepage: https://github.com/roman-haidarov/pq_crypto-seal
220
+ licenses:
221
+ - MIT
222
+ metadata:
223
+ source_code_uri: https://github.com/roman-haidarov/pq_crypto-seal
224
+ changelog_uri: https://github.com/roman-haidarov/pq_crypto-seal/blob/main/CHANGELOG.md
225
+ post_install_message:
226
+ rdoc_options: []
227
+ require_paths:
228
+ - lib
229
+ required_ruby_version: !ruby/object:Gem::Requirement
230
+ requirements:
231
+ - - ">="
232
+ - !ruby/object:Gem::Version
233
+ version: 2.7.1
234
+ required_rubygems_version: !ruby/object:Gem::Requirement
235
+ requirements:
236
+ - - ">="
237
+ - !ruby/object:Gem::Version
238
+ version: '0'
239
+ requirements: []
240
+ rubygems_version: 3.4.22
241
+ signing_key:
242
+ specification_version: 4
243
+ summary: Post-quantum envelope encryption for Ruby
244
+ test_files: []