dstu_core 0.3.0-x86_64-linux → 0.4.0-x86_64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9438183bc3f9dcdd40546f565d6b5887006bc31b9206a6f1905a9d22b7a5782a
4
- data.tar.gz: 89e48f1fc8070cca09a1bccb8254eb6cc44c9d3298fe75362a6bed5c902a6f59
3
+ metadata.gz: 3324edace58d86e8f8ae5b4f139e7b9682b9a931b5fb775b0273791e4cc97980
4
+ data.tar.gz: 6961e94c1ec3c39977b3df52ec91e83f71a4e111489ff577606c62d5cb58fab2
5
5
  SHA512:
6
- metadata.gz: 47360c8056025ad64f518932ca73224e2296e8b3d95a2d0f90c34771a709b34904d1007c2f1be2e2a7d92778abb243f1a63cf884d9faf32f4129a6f77ad8d918
7
- data.tar.gz: 776223e7ade4d8776167cf721c0ef60b0378a53d9b5ad5ef10c7701bdb6e25fc61ddda651edd793eedf0021c21558f71db09921617bd38e3b97f7ace13883800
6
+ metadata.gz: dbb410375a407cd9954d796cd1d6f2eded7343b0e39dbabd564b3e875088efdb12c6ab65289e5348c143c04ff428d3d80718f93dc035b5876d2ddd095cf90db4
7
+ data.tar.gz: 4f1dffd6bad51d324401665fd91d3e8f3bfaea6ff64a3ad8409382a0c4f10fc1c15c019037a814c4d0de00adfbddaf06c2e3634e763c7a657c7816c4f6afa356
Binary file
Binary file
Binary file
Binary file
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # `crypto_secretstream` idiomatic wrapper (docs/DECISIONS.md D-118, docs/bindings-strategy.md
4
- # T-160 step 3) - hides chunk/tag/header bookkeeping behind `write`/`each`, built in pure Ruby on
5
- # top of the low-level SecretStreamPushState/SecretStreamPullState the compiled extension already
6
- # exposes (step 2), rather than new Rust glue. Modeled on stdlib's own
4
+ # T-160 step 3) - hides chunk/tag/header bookkeeping behind `write`/`each`. The framing itself
5
+ # (record layout, length rules, Final, trailing data) is dstu-core's stream-file codec, exposed as
6
+ # SecretStreamFileEncryptor/SecretStreamFileDecryptor (T-284); this file does only I/O. Modeled on
7
+ # stdlib's own
7
8
  # Zlib::GzipWriter/Zlib::GzipReader - the same "wraps an IO, transforms chunks transparently"
8
9
  # shape, the closest Ruby-native precedent for this problem (matching Python's file-like object,
9
10
  # Node's stream.Transform).
@@ -23,8 +24,8 @@
23
24
  # differing encodings and fails even when the bytes match (`"привіт".b == "привіт"` is `false` in
24
25
  # Ruby) - this is a caller-side encoding-compare gotcha, not a bug in this wrapper.
25
26
  module DstuCore
26
- # Matches crates/uacrypt/src/lib.rs's SECRETSTREAM_CHUNK_BYTES exactly - required for
27
- # wire-format interop with `uacrypt encrypt`/`decrypt`, not an independent choice.
27
+ # dstu-core's crypto_secretstream::CHUNK_BYTES and wire constants (D-208); the codec enforces
28
+ # them, these stay as public constants.
28
29
  SECRETSTREAM_CHUNK_BYTES = 8 * 1024
29
30
  SECRETSTREAM_AUTH_TAG_BYTES = 16
30
31
  # Matches dstu-core's crypto_secretstream::FORMAT_VERSION (D-208); the shared vectors pin it.
@@ -40,9 +41,8 @@ module DstuCore
40
41
  def initialize(key, out)
41
42
  @out = out
42
43
  @out.binmode if @out.respond_to?(:binmode)
43
- @push = SecretStreamPushState.new(key)
44
- @out.write([SECRETSTREAM_FORMAT_VERSION].pack("C"))
45
- @out.write(@push.header)
44
+ @enc = SecretStreamFileEncryptor.new(key)
45
+ @out.write(@enc.prefix)
46
46
  @buf = +""
47
47
  @closed = false
48
48
  end
@@ -65,18 +65,21 @@ module DstuCore
65
65
  @closed
66
66
  end
67
67
 
68
- # Buffers `data`, pushing any now-complete 8 KiB chunks immediately. The trailing partial (or
69
- # exactly-8-KiB) chunk is always held back until `close`, since only `close` knows no more
70
- # data is coming - the same one-chunk-ahead reasoning `uacrypt encrypt` itself uses to tag the
71
- # true last chunk `Final`, not an extra empty one after it.
68
+ # Buffers `data`, sealing every full 8 KiB chunk that is certain not to be the last one. The
69
+ # trailing partial (or exactly-8-KiB) chunk is held back until `close`, since only `close`
70
+ # knows no more data is coming - an exact multiple of 8 KiB must end in a full Final record
71
+ # (D-208). Full chunks are cut from `data` by offset, so one large write costs linear time
72
+ # (T-276).
72
73
  def write(data)
73
74
  raise IOError, "closed stream" if @closed
74
75
 
75
- @buf << data.b
76
- while @buf.bytesize > SECRETSTREAM_CHUNK_BYTES
77
- push_chunk(SECRETSTREAM_TAG_MESSAGE, @buf.byteslice(0, SECRETSTREAM_CHUNK_BYTES))
78
- @buf = @buf.byteslice(SECRETSTREAM_CHUNK_BYTES..) || +""
76
+ data = data.b
77
+ pos = top_up_buffer(data)
78
+ while data.bytesize - pos > SECRETSTREAM_CHUNK_BYTES
79
+ @out.write(@enc.seal_chunk(data.byteslice(pos, SECRETSTREAM_CHUNK_BYTES)))
80
+ pos += SECRETSTREAM_CHUNK_BYTES
79
81
  end
82
+ @buf << data.byteslice(pos..)
80
83
  data.bytesize
81
84
  end
82
85
 
@@ -87,19 +90,27 @@ module DstuCore
87
90
  def close
88
91
  return if @closed
89
92
 
90
- push_chunk(SECRETSTREAM_TAG_FINAL, @buf)
93
+ @out.write(@enc.seal_final(@buf))
91
94
  @buf = +""
92
95
  @closed = true
93
96
  end
94
97
 
95
98
  private
96
99
 
97
- def push_chunk(tag, data)
98
- ciphertext, auth_tag = @push.push(tag, data)
99
- @out.write([tag].pack("C"))
100
- @out.write([data.bytesize].pack("V"))
101
- @out.write(ciphertext)
102
- @out.write(auth_tag)
100
+ # Adds `data` to the held-back chunk if it all fits; otherwise tops that chunk up to a full one
101
+ # and seals it, since more data follows. Returns how many bytes of `data` it consumed.
102
+ def top_up_buffer(data)
103
+ return 0 if @buf.empty?
104
+
105
+ if @buf.bytesize + data.bytesize <= SECRETSTREAM_CHUNK_BYTES
106
+ @buf << data
107
+ return data.bytesize
108
+ end
109
+ taken = SECRETSTREAM_CHUNK_BYTES - @buf.bytesize
110
+ @buf << data.byteslice(0, taken)
111
+ @out.write(@enc.seal_chunk(@buf))
112
+ @buf = +""
113
+ taken
103
114
  end
104
115
  end
105
116
 
@@ -119,14 +130,10 @@ module DstuCore
119
130
  def initialize(key, inp)
120
131
  @inp = inp
121
132
  @inp.binmode if @inp.respond_to?(:binmode)
122
- version = read_exact(1, "format version").unpack1("C")
123
- if version != SECRETSTREAM_FORMAT_VERSION
124
- raise DstuCore::Error,
125
- "unsupported stream format version #{version} (this build reads #{SECRETSTREAM_FORMAT_VERSION})"
126
- end
127
- header = read_exact(32, "header")
128
- @pull = SecretStreamPullState.new(key, header)
133
+ @dec = SecretStreamFileDecryptor.new(key)
129
134
  @done = false
135
+ # The version byte and header, read now so a wrong version fails at construction.
136
+ 2.times { step }
130
137
  end
131
138
 
132
139
  def self.open(key, inp)
@@ -137,7 +144,10 @@ module DstuCore
137
144
  def each
138
145
  return enum_for(:each) unless block_given?
139
146
 
140
- yield next_chunk until @done
147
+ until @done
148
+ plaintext = step
149
+ yield plaintext unless plaintext.empty?
150
+ end
141
151
  end
142
152
 
143
153
  def read_all
@@ -149,38 +159,20 @@ module DstuCore
149
159
 
150
160
  private
151
161
 
152
- def next_chunk
153
- tag_byte = read_exact(1, "chunk tag").unpack1("C")
154
- chunk_len = read_exact(4, "chunk length").unpack1("V")
155
- # `chunk_len` is untrusted wire input, read before any tag verification - reject an
156
- # oversized declared length before acting on it (`read_exact` would otherwise try to
157
- # accumulate up to `chunk_len` bytes from `inp`, which for a socket/pipe could mean
158
- # gigabytes before ever failing). Matches `uacrypt decrypt`'s own
159
- # `CliError::SecretstreamChunkTooLarge` bound (`crates/uacrypt/src/lib.rs`).
160
- if chunk_len > SECRETSTREAM_CHUNK_BYTES
161
- raise DstuCore::Error,
162
- "secretstream chunk too large: declared #{chunk_len} bytes, max #{SECRETSTREAM_CHUNK_BYTES}"
163
- end
164
- if tag_byte != SECRETSTREAM_TAG_FINAL && chunk_len != SECRETSTREAM_CHUNK_BYTES
165
- raise DstuCore::Error,
166
- "non-final chunk of #{chunk_len} bytes, must be exactly #{SECRETSTREAM_CHUNK_BYTES} (D-208)"
167
- end
168
- ciphertext = read_exact(chunk_len, "chunk ciphertext")
169
- auth_tag = read_exact(SECRETSTREAM_AUTH_TAG_BYTES, "chunk auth tag")
170
- tag, plaintext = @pull.pull(tag_byte, ciphertext, auth_tag)
171
- if tag == SECRETSTREAM_TAG_FINAL
172
- # Matches `uacrypt decrypt`'s own `CliError::SecretstreamTrailingData` check - reject
173
- # bytes remaining after `Final` rather than silently ignoring them. Checked before
174
- # `plaintext` is returned, so a trailing-data stream never yields this last chunk either
175
- # via `each` or `read_all`.
176
- raise DstuCore::Error, "trailing data after the secretstream's Final chunk" if @inp.read(1)
177
-
162
+ # Feeds the codec one `next_len` read: the plaintext that read released (possibly empty), or
163
+ # at end of input the Final chunk's (after which the stream is done).
164
+ def step
165
+ want = @dec.next_len
166
+ data = read_up_to(want)
167
+ if data.bytesize < want
178
168
  @done = true
169
+ return @dec.finish
179
170
  end
180
- plaintext
171
+ @dec.feed(data)
181
172
  end
182
173
 
183
- def read_exact(size, what)
174
+ # Reads until `size` bytes arrived or `inp` ended; a short result means end of input.
175
+ def read_up_to(size)
184
176
  pieces = []
185
177
  remaining = size
186
178
  while remaining.positive?
@@ -190,12 +182,7 @@ module DstuCore
190
182
  pieces << piece
191
183
  remaining -= piece.bytesize
192
184
  end
193
- data = pieces.join
194
- if data.bytesize != size
195
- raise DstuCore::Error, "truncated secretstream: expected #{size} bytes for #{what}, got #{data.bytesize}"
196
- end
197
-
198
- data
185
+ pieces.join.b
199
186
  end
200
187
  end
201
188
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DstuCore
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dstu_core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - dstu-core contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-09 00:00:00.000000000 Z
11
+ date: 2026-10-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Ruby bindings for dstu-core (Ukrainian DSTU cryptographic standards)
14
14
  - not independently audited.