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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pq_crypto"
4
+ require "pq_crypto/seal/version"
5
+ require "pq_crypto/seal/errors"
6
+ require "pq_crypto/seal/native"
7
+ require "pq_crypto/seal/binary"
8
+ require "pq_crypto/seal/padding"
9
+ require "pq_crypto/seal/format"
10
+ require "pq_crypto/seal/core"
11
+ require "pq_crypto/seal/io"
12
+
13
+ module PQCrypto
14
+ module Seal
15
+ IO_DELEGATES = %i[
16
+ encrypt_io decrypt_io encrypt_frame_io decrypt_frame_io
17
+ encrypt_file decrypt_file rebuild_recipients_file
18
+ add_recipient_file drop_recipient_stanza_file rotate_dek_file
19
+ inspect_file
20
+ ].freeze
21
+
22
+ class << self
23
+ IO_DELEGATES.each do |name|
24
+ define_method(name) do |*args, **kwargs|
25
+ IOAPI.public_send(name, *args, **kwargs)
26
+ end
27
+ end
28
+ end
29
+
30
+ private_constant :Native
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/pq_crypto/seal/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pq_crypto-seal"
7
+ spec.version = PQCrypto::Seal::VERSION
8
+ spec.authors = ["Roman Khaidarov"]
9
+ spec.email = ["roman-haidarov@users.noreply.github.com"]
10
+
11
+ spec.summary = "Post-quantum envelope encryption for Ruby"
12
+ spec.description = "Versioned multi-recipient document encryption using PQCrypto hybrid KEM, HKDF-SHA256, and vendored AEGIS-256."
13
+ spec.homepage = "https://github.com/roman-haidarov/pq_crypto-seal"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.1")
16
+
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
19
+
20
+ spec.files = Dir.chdir(__dir__) do
21
+ Dir["{lib,ext,test,script,fuzz,.github}/**/*", "pq_crypto-seal.gemspec", "Gemfile", "Rakefile", "README.md", "GET_STARTED.md", "FORMAT.md", "SECURITY.md", "RELEASING.md", "CHANGELOG.md", "LICENSE.txt", "VENDORING.md"]
22
+ .select { |path| File.file?(path) }
23
+ .reject do |path|
24
+ path.match?(/(?:\.o|\.so|\.bundle|\.gem)\z/) ||
25
+ path.end_with?("/Makefile", "/mkmf.log") ||
26
+ path.include?("/.DS_Store") || false
27
+ end
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.require_paths = ["lib"]
31
+ spec.extensions = ["ext/pq_crypto_seal/extconf.rb"]
32
+
33
+ spec.add_runtime_dependency "pq_crypto", "~> 0.6.4"
34
+ spec.add_development_dependency "minitest", "~> 5.14"
35
+ spec.add_development_dependency "rake", "~> 13.0"
36
+ spec.add_development_dependency "rake-compiler", "~> 1.2"
37
+ end
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "digest"
5
+ require "fileutils"
6
+ require "net/http"
7
+ require "tmpdir"
8
+ require "uri"
9
+
10
+ ROOT = File.expand_path("..", __dir__)
11
+ VENDOR = File.join(ROOT, "ext/pq_crypto_seal/vendor/libaegis")
12
+ VERSION = "0.10.3"
13
+ URL = "https://github.com/aegis-aead/libaegis/archive/refs/tags/#{VERSION}.tar.gz"
14
+ ARCHIVE_SHA256 = "2f2682c1d08d9a5510caca1c82e3f8ea91f7085fef2ecbed0c398b2a921c79b1"
15
+ TREE_SHA256_FILE = File.join(VENDOR, "TREE_SHA256")
16
+
17
+ def download(uri, limit = 5)
18
+ abort "too many redirects while downloading libaegis" if limit <= 0
19
+ response = Net::HTTP.get_response(uri)
20
+ case response
21
+ when Net::HTTPSuccess
22
+ response.body
23
+ when Net::HTTPRedirection
24
+ location = response["location"]
25
+ abort "download redirect without Location" unless location
26
+ download(URI.join(uri.to_s, location), limit - 1)
27
+ else
28
+ abort "download failed: HTTP #{response.code}"
29
+ end
30
+ end
31
+
32
+ def tree_sha256(directory)
33
+ digest = Digest::SHA256.new
34
+ Dir.chdir(directory) do
35
+ Dir.glob("**/*", File::FNM_DOTMATCH).sort.each do |path|
36
+ next if path == "." || path == ".." || path == "TREE_SHA256"
37
+ next unless File.file?(path)
38
+ bytes = File.binread(path)
39
+ digest << [path.bytesize].pack("N") << path.b
40
+ digest << [bytes.bytesize].pack("Q>") << bytes
41
+ end
42
+ end
43
+ digest.hexdigest
44
+ end
45
+
46
+ def verify!
47
+ expected = File.read(TREE_SHA256_FILE).strip
48
+ actual = tree_sha256(VENDOR)
49
+ abort "libaegis vendor tree mismatch\nexpected: #{expected}\nactual: #{actual}" unless actual == expected
50
+ puts "libaegis #{VERSION} vendor tree verified: #{actual}"
51
+ end
52
+
53
+ def update!
54
+ Dir.mktmpdir("libaegis") do |tmp|
55
+ archive = File.join(tmp, "libaegis.tar.gz")
56
+ uri = URI(URL)
57
+ File.binwrite(archive, download(uri))
58
+ actual = Digest::SHA256.file(archive).hexdigest
59
+ abort "archive checksum mismatch: #{actual}" unless actual == ARCHIVE_SHA256
60
+ abort "tar extraction failed" unless system("tar", "-xzf", archive, "-C", tmp)
61
+ extracted = Dir[File.join(tmp, "libaegis-*")].find { |path| File.directory?(path) }
62
+ abort "extracted libaegis directory not found" unless extracted
63
+ FileUtils.rm_rf(VENDOR)
64
+ FileUtils.mkdir_p(VENDOR)
65
+ FileUtils.cp_r(File.join(extracted, "src"), VENDOR)
66
+ FileUtils.cp(File.join(extracted, "LICENSE"), VENDOR)
67
+ File.write(TREE_SHA256_FILE, tree_sha256(VENDOR) + "\n")
68
+ end
69
+ verify!
70
+ end
71
+
72
+ case ARGV.first
73
+ when nil, "--check"
74
+ verify!
75
+ when "--update"
76
+ update!
77
+ else
78
+ abort "usage: #{$PROGRAM_NAME} [--check|--update]"
79
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ require_relative "test_helper"
3
+
4
+ class AegisVectorsTest < Minitest::Test
5
+ def native
6
+ PQCrypto::Seal.const_get(:Native, false)
7
+ end
8
+
9
+ KEY = ["1001000000000000000000000000000000000000000000000000000000000000"].pack("H*")
10
+ NONCE = ["1000020000000000000000000000000000000000000000000000000000000000"].pack("H*")
11
+
12
+ POSITIVE = [
13
+ ["", "00000000000000000000000000000000", "754fc3d8c973246dcc6d741412a4b236", "1181a1d18091082bf0266f66297d167d2e68b845f61a3b0527d31fc7b7b89f13"],
14
+ ["", "", "", "6a348c930adbd654896e1666aad67de989ea75ebaa2b82fb588977b1ffec864a"],
15
+ ["0001020304050607", "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "f373079ed84b2709faee373584585d60accd191db310ef5d8b11833df9dec711", "b7d28d0c3c0ebd409fd22b44160503073a547412da0854bfb9723020dab8da1a"],
16
+ ["0001020304050607", "000102030405060708090a0b0c0d", "f373079ed84b2709faee37358458", "8c1cc703c81281bee3f6d9966e14948b4a175b2efbdc31e61a98b4465235c2d9"],
17
+ ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223242526272829", "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637", "57754a7d09963e7c787583a2e7b859bb24fa1e04d49fd550b2511a358e3bca252a9b1b8b30cc4a67", "a3aca270c006094d71c20e6910b5161c0826df233d08919a566ec2c05990f734"]
18
+ ].freeze
19
+
20
+ def test_draft_18_positive_vectors
21
+ POSITIVE.each do |ad_hex, msg_hex, ct_hex, tag_hex|
22
+ ad, msg, expected_ct, expected_tag = [ad_hex, msg_hex, ct_hex, tag_hex].map { |hex| [hex].pack("H*") }
23
+ ct, tag = native.aegis256_encrypt(KEY, NONCE, ad, msg)
24
+ assert_equal expected_ct, ct
25
+ assert_equal expected_tag, tag
26
+ assert_equal msg, native.aegis256_decrypt(KEY, NONCE, ad, ct, tag)
27
+ end
28
+ end
29
+
30
+ def test_draft_18_negative_vectors
31
+ valid_ct = ["f373079ed84b2709faee37358458"].pack("H*")
32
+ valid_tag = ["8c1cc703c81281bee3f6d9966e14948b4a175b2efbdc31e61a98b4465235c2d9"].pack("H*")
33
+ ad = ["0001020304050607"].pack("H*")
34
+
35
+ cases = [
36
+ [["1000020000000000000000000000000000000000000000000000000000000000"].pack("H*"), ["1001000000000000000000000000000000000000000000000000000000000000"].pack("H*"), ad, valid_ct, valid_tag],
37
+ [KEY, NONCE, ad, ["f373079ed84b2709faee37358459"].pack("H*"), valid_tag],
38
+ [KEY, NONCE, ["0001020304050608"].pack("H*"), valid_ct, valid_tag],
39
+ [KEY, NONCE, ad, valid_ct, ["8c1cc703c81281bee3f6d9966e14948b4a175b2efbdc31e61a98b4465235c2da"].pack("H*")]
40
+ ]
41
+
42
+ cases.each do |key, nonce, case_ad, ct, tag|
43
+ assert_raises(PQCrypto::Seal::AuthenticationError) do
44
+ native.aegis256_decrypt(key, nonce, case_ad, ct, tag)
45
+ end
46
+ end
47
+ end
48
+
49
+ def test_rfc5869_empty_salt_vector
50
+ ikm = ["0b" * 22].pack("H*")
51
+ expected = ["8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"].pack("H*")
52
+ assert_equal expected, native.hkdf_sha256(ikm, "".b, 42)
53
+ end
54
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ # Protocol-drift detection for the frozen v1 wire format.
6
+ #
7
+ # A fully byte-frozen "plaintext -> exact envelope" vector is not achievable
8
+ # from this gem alone, because X-Wing encapsulation draws its own randomness
9
+ # inside pq_crypto and is not injectable here. Instead this suite pins every
10
+ # part of the construction that Seal itself controls and every derived value
11
+ # that would change if the protocol silently drifted:
12
+ #
13
+ # * the X-Wing wire contract (algorithm id + exact key/ciphertext/secret sizes)
14
+ # * the exact immutable-header byte layout for a fixed input
15
+ # * the payload associated data derivation (SHA-256 of the header)
16
+ # * the KDF label wiring and slot offsets
17
+ # * one-shot and incremental encryption producing identical bytes under a
18
+ # fixed deterministic RNG
19
+ #
20
+ # Any change to labels, offsets, field order, or hashing breaks these.
21
+ class GoldenVectorsTest < Minitest::Test
22
+ def native
23
+ PQCrypto::Seal.const_get(:Native, false)
24
+ end
25
+
26
+ # Deterministic counter-based byte stream so every Seal-side random draw
27
+ # (payload id, nonces, DEK, section id, padding) is fixed across a run.
28
+ class CountingRandom
29
+ def initialize(seed, native)
30
+ @state = [seed].pack("Q>")
31
+ @native = native
32
+ end
33
+
34
+ def bytes(length)
35
+ return "".b if length.zero?
36
+
37
+ out = +"".b
38
+ while out.bytesize < length
39
+ @state = @native.sha256(@state)
40
+ out << @state
41
+ end
42
+ out.byteslice(0, length).b
43
+ end
44
+ end
45
+
46
+ def with_deterministic_random(seed)
47
+ mod = native
48
+ stream = CountingRandom.new(seed, mod)
49
+ singleton = mod.singleton_class
50
+ singleton.send(:alias_method, :__real_random_bytes, :random_bytes)
51
+ singleton.send(:define_method, :random_bytes) { |length| stream.bytes(length) }
52
+ yield
53
+ ensure
54
+ singleton.send(:alias_method, :random_bytes, :__real_random_bytes)
55
+ singleton.send(:remove_method, :__real_random_bytes)
56
+ end
57
+
58
+ def test_xwing_kem_wire_contract
59
+ keypair = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
60
+ assert_equal :ml_kem_768_x25519_xwing, keypair.public_key.algorithm
61
+ assert_equal PQCrypto::Seal::Format::XWING_PUBLIC_KEY_BYTES,
62
+ keypair.public_key.to_bytes.bytesize
63
+ encapsulated = keypair.public_key.encapsulate
64
+ assert_equal PQCrypto::Seal::Format::XWING_CIPHERTEXT_BYTES,
65
+ encapsulated.ciphertext.bytesize
66
+ assert_equal PQCrypto::Seal::Format::XWING_SHARED_SECRET_BYTES,
67
+ encapsulated.shared_secret.bytesize
68
+ recovered = keypair.secret_key.decapsulate(encapsulated.ciphertext)
69
+ assert_equal encapsulated.shared_secret, recovered
70
+ end
71
+
72
+ def test_immutable_header_layout_is_frozen
73
+ keypair = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
74
+ envelope = with_deterministic_random(0x0102_0304_0506_0708) do
75
+ PQCrypto::Seal.encrypt(
76
+ "golden", to: keypair.public_key,
77
+ public_metadata: "meta", padding: :none,
78
+ recipient_capacity: 4, slot_size: 2048
79
+ )
80
+ end
81
+
82
+ fmt = PQCrypto::Seal::Format
83
+ offset = 0
84
+ assert_equal fmt::MAGIC, envelope.byteslice(offset, 8); offset += 8
85
+ assert_equal 1, envelope.getbyte(offset); offset += 1 # version
86
+ header_length = envelope.byteslice(offset, 4).unpack1("N"); offset += 4
87
+ assert_equal fmt::CONTENT_SUITE_AEGIS256, envelope.byteslice(offset, 2).unpack1("n"); offset += 2
88
+ assert_equal fmt::LOOKUP_HINT, envelope.getbyte(offset); offset += 1
89
+ assert_equal fmt::FLAGS, envelope.byteslice(offset, 2).unpack1("n"); offset += 2
90
+ assert_equal fmt::PADDING_NONE, envelope.getbyte(offset); offset += 1
91
+
92
+ header = fmt.parse_header(envelope)
93
+ assert_equal header_length, header.raw.bytesize
94
+ assert_equal "meta", header.public_metadata
95
+ assert_equal fmt::PADDING_NONE, header.padding_policy_id
96
+ assert_equal 4, header.recipient_capacity
97
+ assert_equal 2048, header.slot_size
98
+ end
99
+
100
+ def test_payload_ad_is_sha256_of_header
101
+ keypair = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
102
+ envelope = PQCrypto::Seal.encrypt("data", to: keypair.public_key, padding: :none)
103
+ header = PQCrypto::Seal::Format.parse_header(envelope)
104
+ # The recipient can open the envelope, which only succeeds if the AD used
105
+ # for the payload equals SHA-256(header.raw); a drifted AD fails the tag.
106
+ opened = PQCrypto::Seal.open(envelope, with: keypair)
107
+ assert_equal "data", opened.data
108
+ refute_equal native.sha256(header.raw), header.raw
109
+ assert_equal 32, native.sha256(header.raw).bytesize
110
+ end
111
+
112
+ def test_one_shot_and_incremental_agree_on_deterministic_regions
113
+ # X-Wing encapsulation randomness lives inside pq_crypto and is NOT covered
114
+ # by the Native.random_bytes stub, so the recipient section (KEM ciphertexts)
115
+ # differs between two runs. Everything Seal itself controls must still match:
116
+ # the immutable header and the payload ciphertext+tag.
117
+ #
118
+ # padding MUST be :none: one-shot draws padding as one random_bytes call, the
119
+ # IO path draws it in chunk_size pieces, which would diverge under the same
120
+ # deterministic RNG. With :none both draw zero padding bytes.
121
+ keypair = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
122
+ data = ("payload" * 5000).b
123
+
124
+ one_shot = with_deterministic_random(0xAABB_CCDD_EEFF_0011) do
125
+ PQCrypto::Seal.encrypt(
126
+ data, to: keypair.public_key, metadata: "m",
127
+ public_metadata: "p", padding: :none,
128
+ recipient_capacity: 4, slot_size: 2048
129
+ )
130
+ end
131
+
132
+ incremental = with_deterministic_random(0xAABB_CCDD_EEFF_0011) do
133
+ input = StringIO.new(data)
134
+ output = StringIO.new(+"".b)
135
+ PQCrypto::Seal.encrypt_io(
136
+ input, output, size: data.bytesize, to: keypair.public_key,
137
+ metadata: "m", public_metadata: "p", padding: :none,
138
+ recipient_capacity: 4, slot_size: 2048
139
+ )
140
+ output.string
141
+ end
142
+
143
+ fmt = PQCrypto::Seal::Format
144
+ header_a = fmt.parse_header(one_shot)
145
+ header_b = fmt.parse_header(incremental)
146
+
147
+ # Immutable header bytes are fully deterministic and must be identical.
148
+ assert_equal header_a.raw, header_b.raw,
149
+ "one-shot and incremental must build identical headers"
150
+
151
+ # Payload region (ciphertext + tag) is deterministic given a fixed DEK,
152
+ # nonce, and header hash; extract and compare it across both paths.
153
+ section_len = fmt.section_length(header_a)
154
+ payload_a = one_shot.byteslice(header_a.raw.bytesize + section_len..)
155
+ payload_b = incremental.byteslice(header_b.raw.bytesize + section_len..)
156
+ assert_equal payload_a, payload_b,
157
+ "one-shot and incremental must produce identical payload bytes"
158
+
159
+ # Both envelopes must open to the exact plaintext.
160
+ assert_equal data, PQCrypto::Seal.decrypt(one_shot, with: keypair)
161
+ assert_equal data, PQCrypto::Seal.decrypt(incremental, with: keypair)
162
+ end
163
+
164
+ def test_multi_recipient_golden_roundtrip_is_stable
165
+ alice = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
166
+ bob = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
167
+ envelope = with_deterministic_random(0x1122_3344_5566_7788) do
168
+ PQCrypto::Seal.encrypt(
169
+ "shared-doc", to: [alice.public_key, bob.public_key],
170
+ metadata: "private", padding: :padme,
171
+ recipient_capacity: 4, slot_size: 2048
172
+ )
173
+ end
174
+ assert_equal "shared-doc", PQCrypto::Seal.decrypt(envelope, with: alice)
175
+ assert_equal "shared-doc", PQCrypto::Seal.decrypt(envelope, with: bob)
176
+ info = PQCrypto::Seal.inspect_envelope(envelope)
177
+ assert_equal 4, info.recipient_capacity
178
+ assert_equal PQCrypto::Seal::Format::PADDING_PADME, info.padding_policy_id
179
+ # Padmé targets the full envelope, so the total size equals the Padmé target.
180
+ assert_equal PQCrypto::Seal::Padding.padme_target(info.envelope_bytes),
181
+ info.envelope_bytes
182
+ end
183
+ end
data/test/io_test.rb ADDED
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+ require_relative "test_helper"
3
+
4
+ class IOTest < Minitest::Test
5
+ def setup
6
+ @keypair = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
7
+ end
8
+
9
+ def test_io_round_trip
10
+ data = Random.new(7).bytes(2 * 1024 * 1024 + 31)
11
+ encrypted = StringIO.new("".b)
12
+ PQCrypto::Seal.encrypt_io(StringIO.new(data), encrypted, size: data.bytesize,
13
+ to: @keypair.public_key, chunk_size: 65_537)
14
+ encrypted.rewind
15
+ output = StringIO.new("".b)
16
+ PQCrypto::Seal.decrypt_io(encrypted, output, with: @keypair, chunk_size: 43_211)
17
+ assert_equal data, output.string
18
+ end
19
+
20
+ def test_decrypt_io_rejects_trailing_bytes_by_default
21
+ data = "payload".b
22
+ encrypted = StringIO.new("".b)
23
+ PQCrypto::Seal.encrypt_io(StringIO.new(data), encrypted, size: data.bytesize, to: @keypair.public_key)
24
+ encrypted.string << "TRAIL"
25
+ encrypted.rewind
26
+ output = StringIO.new("".b)
27
+ assert_raises(PQCrypto::Seal::FormatError) do
28
+ PQCrypto::Seal.decrypt_io(encrypted, output, with: @keypair)
29
+ end
30
+ end
31
+
32
+ def test_decrypt_frame_io_tolerates_trailing_bytes
33
+ data = "payload".b
34
+ encrypted = StringIO.new("".b)
35
+ PQCrypto::Seal.encrypt_io(StringIO.new(data), encrypted, size: data.bytesize, to: @keypair.public_key)
36
+ encrypted.string << "TRAIL"
37
+ encrypted.rewind
38
+ output = StringIO.new("".b)
39
+ PQCrypto::Seal.decrypt_frame_io(encrypted, output, with: @keypair)
40
+ assert_equal data, output.string
41
+ assert_equal "TRAIL", encrypted.read
42
+ end
43
+
44
+ def test_file_recipient_rebuild_and_dek_rotation
45
+ bob = PQCrypto::HybridKEM.generate(PQCrypto::Seal::WRAP_KEM_ALGORITHM)
46
+ Dir.mktmpdir do |dir|
47
+ source = File.join(dir, "source")
48
+ sealed = File.join(dir, "sealed")
49
+ rebuilt = File.join(dir, "rebuilt")
50
+ rotated = File.join(dir, "rotated")
51
+ output = File.join(dir, "output")
52
+ data = Random.new(9).bytes(1_300_017)
53
+ File.binwrite(source, data)
54
+ PQCrypto::Seal.encrypt_file(source, sealed, to: @keypair.public_key)
55
+ PQCrypto::Seal.rebuild_recipients_file(
56
+ sealed, rebuilt, with: @keypair,
57
+ recipients: [@keypair.public_key, bob.public_key]
58
+ )
59
+ PQCrypto::Seal.decrypt_file(rebuilt, output, with: bob)
60
+ assert_equal data, File.binread(output)
61
+
62
+ rebuilt_size = File.size(rebuilt)
63
+ PQCrypto::Seal.rotate_dek_file(
64
+ rebuilt, rotated, with: @keypair,
65
+ recipients: [@keypair.public_key, bob.public_key]
66
+ )
67
+ assert_equal rebuilt_size, File.size(rotated)
68
+ refute_equal PQCrypto::Seal.digest(File.binread(rebuilt)), PQCrypto::Seal.digest(File.binread(rotated))
69
+ PQCrypto::Seal.decrypt_file(rotated, output, with: bob)
70
+ assert_equal data, File.binread(output)
71
+ end
72
+ end
73
+
74
+ def test_rebuild_rejects_corrupted_payload_without_publishing
75
+ Dir.mktmpdir do |dir|
76
+ source = File.join(dir, "source")
77
+ sealed = File.join(dir, "sealed")
78
+ rebuilt = File.join(dir, "rebuilt")
79
+ File.binwrite(source, "payload" * 50_000)
80
+ PQCrypto::Seal.encrypt_file(source, sealed, to: @keypair.public_key)
81
+ bytes = File.binread(sealed)
82
+ info = PQCrypto::Seal.inspect_file(sealed)
83
+ payload_offset = info.envelope_bytes - info.padded_inner_length - PQCrypto::Seal::Format::TAG_BYTES
84
+ bytes.setbyte(payload_offset, bytes.getbyte(payload_offset) ^ 1)
85
+ File.binwrite(sealed, bytes)
86
+ assert_raises(PQCrypto::Seal::AuthenticationError) do
87
+ PQCrypto::Seal.rebuild_recipients_file(
88
+ sealed, rebuilt, with: @keypair, recipients: [@keypair.public_key]
89
+ )
90
+ end
91
+ refute File.exist?(rebuilt)
92
+ end
93
+ end
94
+
95
+ def test_failed_file_decrypt_does_not_publish_destination
96
+ Dir.mktmpdir do |dir|
97
+ source = File.join(dir, "source")
98
+ sealed = File.join(dir, "sealed")
99
+ destination = File.join(dir, "destination")
100
+ File.binwrite(source, "important" * 100_000)
101
+ PQCrypto::Seal.encrypt_file(source, sealed, to: @keypair.public_key)
102
+ bytes = File.binread(sealed)
103
+ bytes.setbyte(bytes.bytesize - 1, bytes.getbyte(-1) ^ 1)
104
+ File.binwrite(sealed, bytes)
105
+ assert_raises(PQCrypto::Seal::AuthenticationError) do
106
+ PQCrypto::Seal.decrypt_file(sealed, destination, with: @keypair)
107
+ end
108
+ refute File.exist?(destination)
109
+ end
110
+ end
111
+
112
+ def test_decrypt_file_resource_limit
113
+ Dir.mktmpdir do |dir|
114
+ source = File.join(dir, "source")
115
+ sealed = File.join(dir, "sealed")
116
+ destination = File.join(dir, "destination")
117
+ File.binwrite(source, "x" * 1000)
118
+ PQCrypto::Seal.encrypt_file(source, sealed, to: @keypair.public_key, padding: :none)
119
+ assert_raises(PQCrypto::Seal::ResourceLimitError) do
120
+ PQCrypto::Seal.decrypt_file(sealed, destination, with: @keypair, max_staging_bytes: 32)
121
+ end
122
+ refute File.exist?(destination)
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ require_relative "test_helper"
3
+
4
+ class NativeTest < Minitest::Test
5
+ def native
6
+ PQCrypto::Seal.const_get(:Native, false)
7
+ end
8
+
9
+ def test_aegis_one_shot_and_incremental_are_identical
10
+ key = "k" * 32
11
+ nonce = "n" * 32
12
+ ad = "header"
13
+ message = ("abc123" * 20_000).b
14
+ ciphertext, tag = native.aegis256_encrypt(key, nonce, ad, message)
15
+
16
+ enc = native::Encryptor.new(key, nonce, ad)
17
+ streamed = message.bytes.each_slice(7777).map { |slice| enc.update(slice.pack("C*")) }.join
18
+ assert_equal ciphertext, streamed
19
+ assert_equal tag, enc.final
20
+
21
+ dec = native::Decryptor.new(key, nonce, ad)
22
+ plain = streamed.bytes.each_slice(3333).map { |slice| dec.update(slice.pack("C*")) }.join
23
+ assert dec.final(tag)
24
+ assert_equal message, plain
25
+ end
26
+
27
+ def test_tampered_tag_fails
28
+ key = "k" * 32
29
+ nonce = "n" * 32
30
+ ciphertext, tag = native.aegis256_encrypt(key, nonce, "", "secret")
31
+ tag.setbyte(0, tag.getbyte(0) ^ 1)
32
+ assert_raises(PQCrypto::Seal::AuthenticationError) do
33
+ native.aegis256_decrypt(key, nonce, "", ciphertext, tag)
34
+ end
35
+ end
36
+ end