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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +66 -0
- data/CHANGELOG.md +44 -0
- data/FORMAT.md +139 -0
- data/GET_STARTED.md +30 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +215 -0
- data/RELEASING.md +34 -0
- data/Rakefile +19 -0
- data/SECURITY.md +85 -0
- data/VENDORING.md +14 -0
- data/ext/pq_crypto_seal/aegis_unused_stubs.c +20 -0
- data/ext/pq_crypto_seal/extconf.rb +296 -0
- data/ext/pq_crypto_seal/pq_crypto_seal.c +390 -0
- data/ext/pq_crypto_seal/vendor/libaegis/LICENSE +21 -0
- data/ext/pq_crypto_seal/vendor/libaegis/TREE_SHA256 +1 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l.c +246 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.c +73 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.c +70 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_common.h +691 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.c +77 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.c +86 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_neon_sha3.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.c +63 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/aegis128l_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128l/implementations.h +40 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2.c +247 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.c +108 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.c +103 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.c +79 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_avx2.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_common.h +795 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.c +111 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.c +96 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/aegis128x2_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x2/implementations.h +40 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4.c +252 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.c +117 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.c +111 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.c +112 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx2.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.c +85 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_avx512.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_common.h +808 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.c +119 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.c +104 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/aegis128x4_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis128x4/implementations.h +41 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256.c +240 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_aesni.c +68 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_altivec.c +68 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_common.h +673 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_neon_aes.c +75 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_soft.c +58 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/aegis256_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256/implementations.h +39 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2.c +247 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.c +103 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.c +101 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.c +74 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_avx2.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_common.h +790 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.c +109 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.c +91 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/aegis256x2_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x2/implementations.h +41 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4.c +252 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.c +112 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_aesni.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.c +109 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_altivec.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.c +107 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx2.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.c +79 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_avx512.h +11 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_common.h +805 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.c +117 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_neon_aes.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.c +99 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/aegis256x4_soft.h +9 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/aegis256x4/implementations.h +42 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/common.c +81 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/common.h +144 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/cpu.c +378 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/cpu.h +34 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/keccak.c +187 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/keccak.h +13 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/softaes.c +338 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/common/softaes.h +69 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis.h +77 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128l.h +311 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128x2.h +313 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis128x4.h +313 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256.h +311 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256x2.h +313 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis256x4.h +313 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/include/aegis_raf.h +580 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf.c +87 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128l.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x2.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis128x4.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x2.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_aegis256x4.c +20 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_internal.h +152 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.c +268 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_merkle.h +165 -0
- data/ext/pq_crypto_seal/vendor/libaegis/src/raf/raf_variant.h +1119 -0
- data/fuzz/decrypt_fuzz.rb +119 -0
- data/lib/pq_crypto/seal/binary.rb +62 -0
- data/lib/pq_crypto/seal/core.rb +398 -0
- data/lib/pq_crypto/seal/errors.rb +15 -0
- data/lib/pq_crypto/seal/format.rb +275 -0
- data/lib/pq_crypto/seal/io.rb +336 -0
- data/lib/pq_crypto/seal/native.rb +13 -0
- data/lib/pq_crypto/seal/padding.rb +47 -0
- data/lib/pq_crypto/seal/version.rb +6 -0
- data/lib/pq_crypto/seal.rb +32 -0
- data/pq_crypto-seal.gemspec +37 -0
- data/script/vendor_libs.rb +79 -0
- data/test/aegis_vectors_test.rb +54 -0
- data/test/golden_vectors_test.rb +183 -0
- data/test/io_test.rb +125 -0
- data/test/native_test.rb +36 -0
- data/test/seal_test.rb +238 -0
- data/test/test_helper.rb +6 -0
- metadata +244 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PQCrypto
|
|
4
|
+
module Seal
|
|
5
|
+
module Format
|
|
6
|
+
MAGIC = "PQCSEAL1".b
|
|
7
|
+
VERSION = 1
|
|
8
|
+
CONTENT_SUITE_AEGIS256 = 1
|
|
9
|
+
WRAP_SUITE_MLKEM768_X25519_AEGIS256 = 1
|
|
10
|
+
LOOKUP_HINT = 1
|
|
11
|
+
FLAGS = 0
|
|
12
|
+
INNER_VERSION = 1
|
|
13
|
+
INNER_FLAGS = 0
|
|
14
|
+
|
|
15
|
+
PADDING_NONE = 0
|
|
16
|
+
PADDING_PADME = 1
|
|
17
|
+
PADDING_FIXED = 2
|
|
18
|
+
PADDING_BUCKETS = 3
|
|
19
|
+
PADDING_POLICY_IDS = {
|
|
20
|
+
PADDING_NONE => true,
|
|
21
|
+
PADDING_PADME => true,
|
|
22
|
+
PADDING_FIXED => true,
|
|
23
|
+
PADDING_BUCKETS => true
|
|
24
|
+
}.freeze
|
|
25
|
+
PADDING_POLICY_BY_SYMBOL = {
|
|
26
|
+
nil => PADDING_NONE,
|
|
27
|
+
:none => PADDING_NONE,
|
|
28
|
+
:padme => PADDING_PADME
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
PAYLOAD_ID_BYTES = 32
|
|
32
|
+
NONCE_BYTES = 32
|
|
33
|
+
TAG_BYTES = 32
|
|
34
|
+
HINT_BYTES = 32
|
|
35
|
+
SECTION_ID_BYTES = 32
|
|
36
|
+
|
|
37
|
+
XWING_PUBLIC_KEY_BYTES = 1216
|
|
38
|
+
XWING_CIPHERTEXT_BYTES = 1120
|
|
39
|
+
XWING_SHARED_SECRET_BYTES = 32
|
|
40
|
+
|
|
41
|
+
DEK_BYTES = 32
|
|
42
|
+
STANZA_BYTES = HINT_BYTES + XWING_CIPHERTEXT_BYTES + NONCE_BYTES + DEK_BYTES + TAG_BYTES
|
|
43
|
+
DEFAULT_SLOT_SIZE = 2048
|
|
44
|
+
MIN_SLOT_SIZE = 2048
|
|
45
|
+
MAX_SLOT_SIZE = 8192
|
|
46
|
+
SLOT_GRANULARITY = 256
|
|
47
|
+
DEFAULT_RECIPIENT_CAPACITY = 4
|
|
48
|
+
MAX_RECIPIENT_CAPACITY = 32
|
|
49
|
+
MAX_PUBLIC_METADATA_BYTES = 1 * 1024 * 1024
|
|
50
|
+
MAX_PRIVATE_METADATA_BYTES = 16 * 1024 * 1024
|
|
51
|
+
MAX_HEADER_BYTES = 2 * 1024 * 1024
|
|
52
|
+
|
|
53
|
+
DEFAULT_MAX_PLAINTEXT_BYTES = 1 * 1024 * 1024 * 1024
|
|
54
|
+
DEFAULT_MAX_STAGING_BYTES = DEFAULT_MAX_PLAINTEXT_BYTES + MAX_PRIVATE_METADATA_BYTES + 64
|
|
55
|
+
DEFAULT_MAX_ENVELOPE_BYTES =
|
|
56
|
+
MAX_HEADER_BYTES +
|
|
57
|
+
(2 + SECTION_ID_BYTES + MAX_RECIPIENT_CAPACITY * MAX_SLOT_SIZE) +
|
|
58
|
+
DEFAULT_MAX_STAGING_BYTES +
|
|
59
|
+
TAG_BYTES
|
|
60
|
+
|
|
61
|
+
LIMIT_DEFAULTS = {
|
|
62
|
+
max_staging_bytes: DEFAULT_MAX_STAGING_BYTES,
|
|
63
|
+
max_plaintext_bytes: DEFAULT_MAX_PLAINTEXT_BYTES,
|
|
64
|
+
max_envelope_bytes: DEFAULT_MAX_ENVELOPE_BYTES
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
Header = Struct.new(
|
|
68
|
+
:raw, :content_suite_id, :lookup_mode, :flags, :padding_policy_id,
|
|
69
|
+
:payload_id, :payload_nonce, :recipient_capacity, :slot_size,
|
|
70
|
+
:padded_inner_length, :public_metadata,
|
|
71
|
+
keyword_init: true
|
|
72
|
+
)
|
|
73
|
+
Section = Struct.new(:wrap_suite_id, :section_id, :raw, :slots_offset, :slots_length, keyword_init: true)
|
|
74
|
+
|
|
75
|
+
SLOT_FIELDS = [
|
|
76
|
+
[:hint, HINT_BYTES],
|
|
77
|
+
[:kem_ciphertext, XWING_CIPHERTEXT_BYTES],
|
|
78
|
+
[:wrap_nonce, NONCE_BYTES],
|
|
79
|
+
[:wrapped_dek, DEK_BYTES],
|
|
80
|
+
[:wrap_tag, TAG_BYTES]
|
|
81
|
+
].freeze
|
|
82
|
+
|
|
83
|
+
module_function
|
|
84
|
+
|
|
85
|
+
def validate_slot_size!(slot_size, error: InvalidConfigurationError)
|
|
86
|
+
n = Integer(slot_size)
|
|
87
|
+
unless n.between?(MIN_SLOT_SIZE, MAX_SLOT_SIZE) && (n % SLOT_GRANULARITY).zero?
|
|
88
|
+
raise error, "slot_size must be #{MIN_SLOT_SIZE}..#{MAX_SLOT_SIZE} and divisible by #{SLOT_GRANULARITY}"
|
|
89
|
+
end
|
|
90
|
+
raise error, "slot_size is too small for current wrap suite" if n < STANZA_BYTES
|
|
91
|
+
n
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_capacity!(capacity, recipient_count = nil, error: InvalidConfigurationError)
|
|
95
|
+
n = Integer(capacity)
|
|
96
|
+
unless n.between?(1, MAX_RECIPIENT_CAPACITY)
|
|
97
|
+
raise error, "recipient_capacity must be 1..#{MAX_RECIPIENT_CAPACITY}"
|
|
98
|
+
end
|
|
99
|
+
if recipient_count && recipient_count > n
|
|
100
|
+
raise RecipientCapacityExceeded, "#{recipient_count} recipients do not fit capacity #{n}"
|
|
101
|
+
end
|
|
102
|
+
n
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def validate_slot_size_from_wire!(slot_size)
|
|
106
|
+
validate_slot_size!(slot_size, error: FormatError)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_capacity_from_wire!(capacity)
|
|
110
|
+
validate_capacity!(capacity, error: FormatError)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def validate_padding_policy_id!(policy_id)
|
|
114
|
+
n = Integer(policy_id)
|
|
115
|
+
raise FormatError, "unsupported padding policy id #{n}" unless PADDING_POLICY_IDS.key?(n)
|
|
116
|
+
n
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def padding_policy_id_for(policy)
|
|
120
|
+
return PADDING_POLICY_BY_SYMBOL.fetch(policy) if PADDING_POLICY_BY_SYMBOL.key?(policy)
|
|
121
|
+
case policy
|
|
122
|
+
when :preserve
|
|
123
|
+
raise InvalidConfigurationError, "padding: :preserve is only valid for rotate_dek"
|
|
124
|
+
when Hash
|
|
125
|
+
return PADDING_FIXED if policy.key?(:to)
|
|
126
|
+
return PADDING_BUCKETS if policy.key?(:buckets)
|
|
127
|
+
raise InvalidConfigurationError, "padding hash must contain :to or :buckets"
|
|
128
|
+
else
|
|
129
|
+
raise InvalidConfigurationError, "unsupported padding policy: #{policy.inspect}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_header(payload_id:, payload_nonce:, recipient_capacity:, slot_size:,
|
|
134
|
+
padded_inner_length:, public_metadata:, padding_policy_id:)
|
|
135
|
+
public_metadata = String(public_metadata).b
|
|
136
|
+
raise InvalidConfigurationError, "public metadata is too large" if public_metadata.bytesize > MAX_PUBLIC_METADATA_BYTES
|
|
137
|
+
raise InvalidConfigurationError, "invalid payload_id" unless payload_id.bytesize == PAYLOAD_ID_BYTES
|
|
138
|
+
raise InvalidConfigurationError, "invalid payload nonce" unless payload_nonce.bytesize == NONCE_BYTES
|
|
139
|
+
validate_padding_policy_id!(padding_policy_id)
|
|
140
|
+
|
|
141
|
+
body = +"".b
|
|
142
|
+
body << Binary.u16(CONTENT_SUITE_AEGIS256)
|
|
143
|
+
body << Binary.u8(LOOKUP_HINT)
|
|
144
|
+
body << Binary.u16(FLAGS)
|
|
145
|
+
body << Binary.u8(Integer(padding_policy_id))
|
|
146
|
+
body << payload_id
|
|
147
|
+
body << payload_nonce
|
|
148
|
+
body << Binary.u16(recipient_capacity)
|
|
149
|
+
body << Binary.u16(slot_size)
|
|
150
|
+
body << Binary.u64(padded_inner_length)
|
|
151
|
+
body << Binary.u32(public_metadata.bytesize)
|
|
152
|
+
body << public_metadata
|
|
153
|
+
header = MAGIC + Binary.u8(VERSION) + Binary.u32(MAGIC.bytesize + 1 + 4 + body.bytesize) + body
|
|
154
|
+
raise InvalidConfigurationError, "header is too large" if header.bytesize > MAX_HEADER_BYTES
|
|
155
|
+
header
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_header(bytes)
|
|
159
|
+
bytes = String(bytes).b
|
|
160
|
+
offset = 0
|
|
161
|
+
magic, offset = Binary.read_bytes(bytes, offset, MAGIC.bytesize)
|
|
162
|
+
raise FormatError, "invalid envelope magic" unless magic == MAGIC
|
|
163
|
+
version, offset = Binary.read_u8(bytes, offset)
|
|
164
|
+
raise FormatError, "unsupported format version #{version}" unless version == VERSION
|
|
165
|
+
header_length, offset = Binary.read_u32(bytes, offset)
|
|
166
|
+
raise FormatError, "invalid header length" if header_length < offset || header_length > MAX_HEADER_BYTES
|
|
167
|
+
Binary.ensure_available!(bytes, 0, header_length)
|
|
168
|
+
raw = bytes.byteslice(0, header_length).b
|
|
169
|
+
|
|
170
|
+
content_suite, offset = Binary.read_u16(raw, offset)
|
|
171
|
+
raise UnsupportedSuiteError, "unsupported content suite #{content_suite}" unless content_suite == CONTENT_SUITE_AEGIS256
|
|
172
|
+
lookup_mode, offset = Binary.read_u8(raw, offset)
|
|
173
|
+
raise UnsupportedSuiteError, "unsupported lookup mode #{lookup_mode}" unless lookup_mode == LOOKUP_HINT
|
|
174
|
+
flags, offset = Binary.read_u16(raw, offset)
|
|
175
|
+
raise FormatError, "unknown header flags" unless flags == FLAGS
|
|
176
|
+
padding_policy_id, offset = Binary.read_u8(raw, offset)
|
|
177
|
+
validate_padding_policy_id!(padding_policy_id)
|
|
178
|
+
payload_id, offset = Binary.read_bytes(raw, offset, PAYLOAD_ID_BYTES)
|
|
179
|
+
payload_nonce, offset = Binary.read_bytes(raw, offset, NONCE_BYTES)
|
|
180
|
+
capacity, offset = Binary.read_u16(raw, offset)
|
|
181
|
+
validate_capacity_from_wire!(capacity)
|
|
182
|
+
slot_size, offset = Binary.read_u16(raw, offset)
|
|
183
|
+
validate_slot_size_from_wire!(slot_size)
|
|
184
|
+
padded_inner_length, offset = Binary.read_u64(raw, offset)
|
|
185
|
+
raise FormatError, "padded inner frame is too short" if padded_inner_length < 14
|
|
186
|
+
public_length, offset = Binary.read_u32(raw, offset)
|
|
187
|
+
raise FormatError, "public metadata is too large" if public_length > MAX_PUBLIC_METADATA_BYTES
|
|
188
|
+
public_metadata, offset = Binary.read_bytes(raw, offset, public_length)
|
|
189
|
+
raise FormatError, "non-canonical header length" unless offset == raw.bytesize
|
|
190
|
+
|
|
191
|
+
Header.new(
|
|
192
|
+
raw: raw, content_suite_id: content_suite, lookup_mode: lookup_mode,
|
|
193
|
+
flags: flags, padding_policy_id: padding_policy_id,
|
|
194
|
+
payload_id: payload_id, payload_nonce: payload_nonce,
|
|
195
|
+
recipient_capacity: capacity, slot_size: slot_size,
|
|
196
|
+
padded_inner_length: padded_inner_length, public_metadata: public_metadata
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def section_length(header)
|
|
201
|
+
section_length_for(header.recipient_capacity, header.slot_size)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def section_length_for(capacity, slot_size)
|
|
205
|
+
2 + SECTION_ID_BYTES + Integer(capacity) * Integer(slot_size)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def split_slot(slot, slot_size)
|
|
209
|
+
fields = {}
|
|
210
|
+
offset = 0
|
|
211
|
+
SLOT_FIELDS.each do |name, length|
|
|
212
|
+
fields[name] = slot.byteslice(offset, length)
|
|
213
|
+
offset += length
|
|
214
|
+
end
|
|
215
|
+
fields[:padding] = slot.byteslice(offset, slot_size - offset)
|
|
216
|
+
fields
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def parse_section(bytes, offset, header)
|
|
220
|
+
length = section_length(header)
|
|
221
|
+
raw, = Binary.read_bytes(bytes, offset, length)
|
|
222
|
+
wrap_suite, slot_offset = Binary.read_u16(raw, 0)
|
|
223
|
+
section_id, slot_offset = Binary.read_bytes(raw, slot_offset, SECTION_ID_BYTES)
|
|
224
|
+
unless wrap_suite == WRAP_SUITE_MLKEM768_X25519_AEGIS256
|
|
225
|
+
raise UnsupportedSuiteError, "unsupported wrap suite #{wrap_suite}"
|
|
226
|
+
end
|
|
227
|
+
Section.new(
|
|
228
|
+
wrap_suite_id: wrap_suite, section_id: section_id, raw: raw,
|
|
229
|
+
slots_offset: slot_offset, slots_length: raw.bytesize - slot_offset
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def inner_prefix(content_length, private_metadata_length)
|
|
234
|
+
Binary.u8(INNER_VERSION) + Binary.u8(INNER_FLAGS) +
|
|
235
|
+
Binary.u64(content_length) + Binary.u32(private_metadata_length)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def parse_verified_inner(bytes)
|
|
239
|
+
bytes = String(bytes).b
|
|
240
|
+
offset = 0
|
|
241
|
+
version, offset = Binary.read_u8(bytes, offset)
|
|
242
|
+
raise FormatError, "unsupported inner version" unless version == INNER_VERSION
|
|
243
|
+
flags, offset = Binary.read_u8(bytes, offset)
|
|
244
|
+
raise FormatError, "unknown inner flags" unless flags == INNER_FLAGS
|
|
245
|
+
content_length, offset = Binary.read_u64(bytes, offset)
|
|
246
|
+
metadata_length, offset = Binary.read_u32(bytes, offset)
|
|
247
|
+
raise FormatError, "private metadata is too large" if metadata_length > MAX_PRIVATE_METADATA_BYTES
|
|
248
|
+
metadata, offset = Binary.read_bytes(bytes, offset, metadata_length)
|
|
249
|
+
content, offset = Binary.read_bytes(bytes, offset, content_length)
|
|
250
|
+
[content, metadata, bytes.bytesize - offset]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def check_resource_limits!(padded_inner_length:, envelope_bytes: nil,
|
|
254
|
+
max_staging_bytes: DEFAULT_MAX_STAGING_BYTES,
|
|
255
|
+
max_envelope_bytes: DEFAULT_MAX_ENVELOPE_BYTES)
|
|
256
|
+
if padded_inner_length > Integer(max_staging_bytes)
|
|
257
|
+
raise ResourceLimitError,
|
|
258
|
+
"padded_inner_length #{padded_inner_length} exceeds max_staging_bytes #{max_staging_bytes}"
|
|
259
|
+
end
|
|
260
|
+
if envelope_bytes && envelope_bytes > Integer(max_envelope_bytes)
|
|
261
|
+
raise ResourceLimitError,
|
|
262
|
+
"envelope #{envelope_bytes} exceeds max_envelope_bytes #{max_envelope_bytes}"
|
|
263
|
+
end
|
|
264
|
+
true
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def pre_auth_limits(limits)
|
|
268
|
+
{
|
|
269
|
+
max_staging_bytes: limits.fetch(:max_staging_bytes, DEFAULT_MAX_STAGING_BYTES),
|
|
270
|
+
max_envelope_bytes: limits.fetch(:max_envelope_bytes, DEFAULT_MAX_ENVELOPE_BYTES)
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module PQCrypto
|
|
7
|
+
module Seal
|
|
8
|
+
module IOAPI
|
|
9
|
+
DEFAULT_CHUNK_SIZE = 1024 * 1024
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def encrypt_io(input, output, size:, to:, metadata: "".b, public_metadata: "".b,
|
|
13
|
+
recipient_capacity: Format::DEFAULT_RECIPIENT_CAPACITY,
|
|
14
|
+
slot_size: Format::DEFAULT_SLOT_SIZE, padding: :padme,
|
|
15
|
+
chunk_size: DEFAULT_CHUNK_SIZE, strict_size: true)
|
|
16
|
+
content_size = Integer(size)
|
|
17
|
+
raise ArgumentError, "size must be non-negative" if content_size.negative?
|
|
18
|
+
metadata = String(metadata).b
|
|
19
|
+
seal(:validate_private_metadata!, metadata)
|
|
20
|
+
recipients = seal(:normalize_public_keys, to)
|
|
21
|
+
capacity = Format.validate_capacity!(recipient_capacity, recipients.length)
|
|
22
|
+
slot_size = Format.validate_slot_size!(slot_size)
|
|
23
|
+
chunk_size = validate_chunk_size(chunk_size)
|
|
24
|
+
|
|
25
|
+
parts = nil
|
|
26
|
+
parts = seal(
|
|
27
|
+
:materialize_crypto_parts,
|
|
28
|
+
recipients: recipients, capacity: capacity, slot_size: slot_size,
|
|
29
|
+
padding: padding, public_metadata: public_metadata,
|
|
30
|
+
content_size: content_size, metadata_size: metadata.bytesize
|
|
31
|
+
)
|
|
32
|
+
output.write(parts[:header])
|
|
33
|
+
output.write(parts[:section])
|
|
34
|
+
encryptor = Native::Encryptor.new(parts[:dek], parts[:payload_nonce], parts[:header_hash])
|
|
35
|
+
output.write(encryptor.update(parts[:inner_prefix]))
|
|
36
|
+
output.write(encryptor.update(metadata)) unless metadata.empty?
|
|
37
|
+
copied = copy_exact_encrypted(input, output, encryptor, content_size, chunk_size)
|
|
38
|
+
raise EOFError, "input ended after #{copied} of #{content_size} bytes" unless copied == content_size
|
|
39
|
+
if strict_size
|
|
40
|
+
raise ArgumentError, "input contains more bytes than declared size" unless input.read(1).nil?
|
|
41
|
+
end
|
|
42
|
+
write_random_encrypted(output, encryptor, parts[:padded_inner_length] - parts[:raw_inner_length], chunk_size)
|
|
43
|
+
output.write(encryptor.final)
|
|
44
|
+
output
|
|
45
|
+
ensure
|
|
46
|
+
seal(:wipe_string!, parts[:dek]) if parts
|
|
47
|
+
seal(:wipe_string!, metadata) if metadata.is_a?(String) && !metadata.frozen?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def decrypt_io(input, output, with:, staging_directory: nil, chunk_size: DEFAULT_CHUNK_SIZE,
|
|
51
|
+
strict_eof: true, **limits)
|
|
52
|
+
limits = Format::LIMIT_DEFAULTS.merge(limits)
|
|
53
|
+
chunk_size = validate_chunk_size(chunk_size)
|
|
54
|
+
staging = new_staging("pqcrypto-seal-inner", staging_directory)
|
|
55
|
+
opened = decrypt_to_staging(
|
|
56
|
+
input, staging, with: with, chunk_size: chunk_size, strict_eof: strict_eof, **limits
|
|
57
|
+
)
|
|
58
|
+
staging.flush
|
|
59
|
+
staging.rewind
|
|
60
|
+
content_offset, content_length, metadata = parse_verified_staging(staging, opened[:header].padded_inner_length)
|
|
61
|
+
if content_length > Integer(limits[:max_plaintext_bytes])
|
|
62
|
+
seal(:wipe_string!, metadata)
|
|
63
|
+
raise ResourceLimitError,
|
|
64
|
+
"plaintext #{content_length} exceeds max_plaintext_bytes #{limits[:max_plaintext_bytes]}"
|
|
65
|
+
end
|
|
66
|
+
staging.seek(content_offset, ::IO::SEEK_SET)
|
|
67
|
+
copy_exact(staging, output, content_length, chunk_size)
|
|
68
|
+
Opened.from_header(opened[:header], opened[:section], data: nil, metadata: metadata)
|
|
69
|
+
ensure
|
|
70
|
+
staging.close! if defined?(staging) && staging
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def decrypt_frame_io(input, output, with:, **options)
|
|
74
|
+
decrypt_io(input, output, with: with, strict_eof: false, **options)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def encrypt_frame_io(input, output, size:, to:, **options)
|
|
78
|
+
encrypt_io(input, output, size: size, to: to, strict_size: false, **options)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def encrypt_file(source, destination, **options)
|
|
82
|
+
File.open(source, "rb") do |input|
|
|
83
|
+
size = input.stat.size
|
|
84
|
+
atomic_destination(destination) do |tmp|
|
|
85
|
+
encrypt_io(input, tmp, size: size, strict_size: true, **options)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
destination
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def decrypt_file(source, destination, with:, staging_directory: nil,
|
|
92
|
+
chunk_size: DEFAULT_CHUNK_SIZE, **limits)
|
|
93
|
+
File.open(source, "rb") do |input|
|
|
94
|
+
atomic_destination(destination) do |tmp|
|
|
95
|
+
decrypt_io(
|
|
96
|
+
input, tmp, with: with, staging_directory: staging_directory,
|
|
97
|
+
chunk_size: chunk_size, strict_eof: true, **limits
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
destination
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def rebuild_recipients_file(source, destination, with:, recipients:, chunk_size: DEFAULT_CHUNK_SIZE)
|
|
105
|
+
chunk_size = validate_chunk_size(chunk_size)
|
|
106
|
+
File.open(source, "rb") do |input|
|
|
107
|
+
header = read_header(input)
|
|
108
|
+
old_section = Format.parse_section(read_exact(input, Format.section_length(header)), 0, header)
|
|
109
|
+
dek = seal(:unwrap_dek, header, old_section, with)
|
|
110
|
+
public_keys = seal(:normalize_public_keys, recipients)
|
|
111
|
+
Format.validate_capacity!(header.recipient_capacity, public_keys.length)
|
|
112
|
+
new_section = seal(
|
|
113
|
+
:build_recipient_section,
|
|
114
|
+
recipients: public_keys, capacity: header.recipient_capacity,
|
|
115
|
+
slot_size: header.slot_size, payload_id: header.payload_id,
|
|
116
|
+
header_hash: Native.sha256(header.raw), dek: dek,
|
|
117
|
+
wrap_suite_id: Format::WRAP_SUITE_MLKEM768_X25519_AEGIS256
|
|
118
|
+
)
|
|
119
|
+
atomic_destination(destination) do |tmp|
|
|
120
|
+
tmp.write(header.raw)
|
|
121
|
+
tmp.write(new_section)
|
|
122
|
+
verifier = Native::Decryptor.new(dek, header.payload_nonce, Native.sha256(header.raw))
|
|
123
|
+
remaining = header.padded_inner_length
|
|
124
|
+
while remaining.positive?
|
|
125
|
+
take = [remaining, chunk_size].min
|
|
126
|
+
ciphertext_chunk = read_exact(input, take)
|
|
127
|
+
tmp.write(ciphertext_chunk)
|
|
128
|
+
seal(:wipe_string!, verifier.update(ciphertext_chunk))
|
|
129
|
+
remaining -= take
|
|
130
|
+
end
|
|
131
|
+
payload_tag = read_exact(input, Format::TAG_BYTES)
|
|
132
|
+
raise FormatError, "trailing bytes after envelope" unless input.read(1).nil?
|
|
133
|
+
verifier.final(payload_tag)
|
|
134
|
+
tmp.write(payload_tag)
|
|
135
|
+
end
|
|
136
|
+
ensure
|
|
137
|
+
seal(:wipe_string!, dek) if defined?(dek)
|
|
138
|
+
end
|
|
139
|
+
destination
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def add_recipient_file(source, destination, with:, recipient:, current_recipients:, **options)
|
|
143
|
+
rebuild_recipients_file(
|
|
144
|
+
source, destination, with: with,
|
|
145
|
+
recipients: Array(current_recipients) + [recipient], **options
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def drop_recipient_stanza_file(source, destination, with:, remaining_recipients:, **options)
|
|
150
|
+
rebuild_recipients_file(source, destination, with: with, recipients: remaining_recipients, **options)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def rotate_dek_file(source, destination, with:, recipients:, padding: :preserve,
|
|
154
|
+
staging_directory: nil, chunk_size: DEFAULT_CHUNK_SIZE)
|
|
155
|
+
plaintext = new_staging("pqcrypto-seal-rotation", staging_directory)
|
|
156
|
+
opened = nil
|
|
157
|
+
File.open(source, "rb") do |input|
|
|
158
|
+
opened = decrypt_io(input, plaintext, with: with,
|
|
159
|
+
staging_directory: staging_directory, chunk_size: chunk_size, strict_eof: true)
|
|
160
|
+
end
|
|
161
|
+
plaintext.flush
|
|
162
|
+
plaintext.rewind
|
|
163
|
+
info = inspect_file(source)
|
|
164
|
+
padding = { to: info.envelope_bytes } if padding == :preserve
|
|
165
|
+
atomic_destination(destination) do |tmp|
|
|
166
|
+
encrypt_io(
|
|
167
|
+
plaintext, tmp, size: plaintext.stat.size, to: recipients,
|
|
168
|
+
metadata: opened.metadata, public_metadata: opened.public_metadata,
|
|
169
|
+
recipient_capacity: info.recipient_capacity, slot_size: info.slot_size,
|
|
170
|
+
padding: padding, chunk_size: chunk_size, strict_size: true
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
destination
|
|
174
|
+
ensure
|
|
175
|
+
plaintext.close! if defined?(plaintext) && plaintext
|
|
176
|
+
seal(:wipe_string!, opened.metadata) if defined?(opened) && opened && opened.metadata
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def inspect_file(source)
|
|
180
|
+
File.open(source, "rb") do |input|
|
|
181
|
+
header = read_header(input)
|
|
182
|
+
section = Format.parse_section(read_exact(input, Format.section_length(header)), 0, header)
|
|
183
|
+
total = header.raw.bytesize + section.raw.bytesize + header.padded_inner_length + Format::TAG_BYTES
|
|
184
|
+
raise FormatError, "file length does not match envelope" unless File.size(source) == total
|
|
185
|
+
Inspection.from_header(header, section, envelope_bytes: total)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def decrypt_to_staging(input, staging, with:, chunk_size:, strict_eof:, **limits)
|
|
190
|
+
limits = Format::LIMIT_DEFAULTS.merge(limits)
|
|
191
|
+
header = read_header(input)
|
|
192
|
+
Format.check_resource_limits!(
|
|
193
|
+
padded_inner_length: header.padded_inner_length, **Format.pre_auth_limits(limits)
|
|
194
|
+
)
|
|
195
|
+
section = Format.parse_section(read_exact(input, Format.section_length(header)), 0, header)
|
|
196
|
+
estimated = header.raw.bytesize + section.raw.bytesize + header.padded_inner_length + Format::TAG_BYTES
|
|
197
|
+
Format.check_resource_limits!(
|
|
198
|
+
padded_inner_length: header.padded_inner_length,
|
|
199
|
+
envelope_bytes: estimated, **Format.pre_auth_limits(limits)
|
|
200
|
+
)
|
|
201
|
+
dek = seal(:unwrap_dek, header, section, with)
|
|
202
|
+
decryptor = Native::Decryptor.new(dek, header.payload_nonce, Native.sha256(header.raw))
|
|
203
|
+
remaining = header.padded_inner_length
|
|
204
|
+
while remaining.positive?
|
|
205
|
+
take = [remaining, chunk_size].min
|
|
206
|
+
staging.write(decryptor.update(read_exact(input, take)))
|
|
207
|
+
remaining -= take
|
|
208
|
+
end
|
|
209
|
+
tag = read_exact(input, Format::TAG_BYTES)
|
|
210
|
+
raise FormatError, "trailing bytes after envelope" if strict_eof && !input.read(1).nil?
|
|
211
|
+
decryptor.final(tag)
|
|
212
|
+
{ header: header, section: section }
|
|
213
|
+
ensure
|
|
214
|
+
seal(:wipe_string!, dek) if defined?(dek)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse_verified_staging(staging, expected_size)
|
|
218
|
+
raise FormatError, "staging size mismatch" unless staging.stat.size == expected_size
|
|
219
|
+
staging.rewind
|
|
220
|
+
prefix = read_exact(staging, 14)
|
|
221
|
+
version = prefix.getbyte(0)
|
|
222
|
+
flags = prefix.getbyte(1)
|
|
223
|
+
raise FormatError, "unsupported inner version" unless version == Format::INNER_VERSION
|
|
224
|
+
raise FormatError, "unknown inner flags" unless flags == Format::INNER_FLAGS
|
|
225
|
+
hi, lo, metadata_length = prefix.byteslice(2, 12).unpack("NNN")
|
|
226
|
+
content_length = (hi << 32) | lo
|
|
227
|
+
raise FormatError, "private metadata is too large" if metadata_length > Format::MAX_PRIVATE_METADATA_BYTES
|
|
228
|
+
minimum = 14 + metadata_length + content_length
|
|
229
|
+
raise FormatError, "inner lengths exceed authenticated frame" if minimum > expected_size
|
|
230
|
+
metadata = read_exact(staging, metadata_length)
|
|
231
|
+
[14 + metadata_length, content_length, metadata]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def read_header(input)
|
|
235
|
+
prefix_len = Format::MAGIC.bytesize + 1 + 4
|
|
236
|
+
initial = read_exact(input, prefix_len)
|
|
237
|
+
header_length = initial.byteslice(Format::MAGIC.bytesize + 1, 4).unpack1("N")
|
|
238
|
+
raise FormatError, "invalid header length" if header_length < initial.bytesize || header_length > Format::MAX_HEADER_BYTES
|
|
239
|
+
Format.parse_header(initial + read_exact(input, header_length - initial.bytesize))
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def seal(name, *args, **kwargs)
|
|
243
|
+
Seal.send(name, *args, **kwargs)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def atomic_destination(destination)
|
|
247
|
+
path = File.expand_path(destination)
|
|
248
|
+
directory = File.dirname(path)
|
|
249
|
+
FileUtils.mkdir_p(directory)
|
|
250
|
+
temp = Tempfile.new([".#{File.basename(path)}.", ".pqcseal"], directory)
|
|
251
|
+
temp.binmode
|
|
252
|
+
temp.chmod(0o600)
|
|
253
|
+
begin
|
|
254
|
+
yield temp
|
|
255
|
+
temp.flush
|
|
256
|
+
temp.fsync
|
|
257
|
+
temp.close
|
|
258
|
+
File.rename(temp.path, path)
|
|
259
|
+
fsync_directory(directory)
|
|
260
|
+
ensure
|
|
261
|
+
temp.close! rescue nil
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def fsync_directory(directory)
|
|
266
|
+
File.open(directory, "r") { |dir| dir.fsync }
|
|
267
|
+
rescue SystemCallError, IOError
|
|
268
|
+
nil
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def copy_exact_encrypted(input, output, encryptor, length, chunk_size)
|
|
272
|
+
copied = 0
|
|
273
|
+
while copied < length
|
|
274
|
+
chunk = input.read([chunk_size, length - copied].min)
|
|
275
|
+
break unless chunk && !chunk.empty?
|
|
276
|
+
chunk = chunk.b
|
|
277
|
+
output.write(encryptor.update(chunk))
|
|
278
|
+
copied += chunk.bytesize
|
|
279
|
+
end
|
|
280
|
+
copied
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def write_random_encrypted(output, encryptor, length, chunk_size)
|
|
284
|
+
remaining = length
|
|
285
|
+
while remaining.positive?
|
|
286
|
+
take = [remaining, chunk_size].min
|
|
287
|
+
random = Native.random_bytes(take)
|
|
288
|
+
output.write(encryptor.update(random))
|
|
289
|
+
seal(:wipe_string!, random)
|
|
290
|
+
remaining -= take
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def copy_exact(input, output, length, chunk_size)
|
|
295
|
+
remaining = length
|
|
296
|
+
while remaining.positive?
|
|
297
|
+
chunk = input.read([remaining, chunk_size].min)
|
|
298
|
+
raise EOFError, "staging input is truncated" unless chunk && !chunk.empty?
|
|
299
|
+
output.write(chunk)
|
|
300
|
+
remaining -= chunk.bytesize
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def read_exact(io, length)
|
|
305
|
+
return "".b if length.zero?
|
|
306
|
+
buffer = +"".b
|
|
307
|
+
while buffer.bytesize < length
|
|
308
|
+
chunk = io.read(length - buffer.bytesize)
|
|
309
|
+
raise EOFError, "truncated envelope" unless chunk && !chunk.empty?
|
|
310
|
+
buffer << chunk.b
|
|
311
|
+
end
|
|
312
|
+
buffer
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def new_staging(prefix, staging_directory)
|
|
316
|
+
tempfile = Tempfile.new([prefix, ".bin"], staging_directory)
|
|
317
|
+
tempfile.binmode
|
|
318
|
+
tempfile.chmod(0o600)
|
|
319
|
+
unlink_open_tempfile(tempfile)
|
|
320
|
+
tempfile
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def unlink_open_tempfile(tempfile)
|
|
324
|
+
tempfile.unlink
|
|
325
|
+
rescue SystemCallError, IOError, NotImplementedError
|
|
326
|
+
nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def validate_chunk_size(value)
|
|
330
|
+
size = Integer(value)
|
|
331
|
+
raise ArgumentError, "chunk_size must be positive" unless size.positive?
|
|
332
|
+
size
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PQCrypto
|
|
4
|
+
module Seal
|
|
5
|
+
module Padding
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def padme_target(length)
|
|
9
|
+
n = Integer(length)
|
|
10
|
+
raise ArgumentError, "length must be non-negative" if n.negative?
|
|
11
|
+
return n if n <= 1
|
|
12
|
+
|
|
13
|
+
exponent = n.bit_length - 1
|
|
14
|
+
significant = exponent.bit_length
|
|
15
|
+
low_bits = exponent - significant
|
|
16
|
+
return n if low_bits <= 0
|
|
17
|
+
|
|
18
|
+
mask = (1 << low_bits) - 1
|
|
19
|
+
(n + mask) & ~mask
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def target(base_length, policy)
|
|
23
|
+
case policy
|
|
24
|
+
when nil, :none then base_length
|
|
25
|
+
when :padme then padme_target(base_length)
|
|
26
|
+
when Hash then fixed_or_buckets(base_length, policy)
|
|
27
|
+
else
|
|
28
|
+
raise InvalidConfigurationError, "unsupported padding policy: #{policy.inspect}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fixed_or_buckets(base_length, policy)
|
|
33
|
+
if policy.key?(:to)
|
|
34
|
+
target = Integer(policy.fetch(:to))
|
|
35
|
+
raise InvalidConfigurationError, "padding target is smaller than envelope" if target < base_length
|
|
36
|
+
target
|
|
37
|
+
elsif policy.key?(:buckets)
|
|
38
|
+
bucket = Array(policy.fetch(:buckets)).map { |n| Integer(n) }.sort.find { |n| n >= base_length }
|
|
39
|
+
raise InvalidConfigurationError, "no padding bucket can hold envelope" unless bucket
|
|
40
|
+
bucket
|
|
41
|
+
else
|
|
42
|
+
raise InvalidConfigurationError, "padding hash must contain :to or :buckets"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|