dstu_core 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d2334ab3aa0124cebb58c7581ae4aeb17da063c86cc204c81c4520d2828e31f5
4
+ data.tar.gz: ed3fd69afa6c610c3ab0400b57df533d1884a1b7890b87f3cba4dbf3578a4d1f
5
+ SHA512:
6
+ metadata.gz: 8e1f3bd238c2610b0d49f299d5491da7edcb8c44ad64ec2a1dae830909e5c731e6be51133373179da81ca370e0adbfd7296e23cb0feb4548fe6c15fbcb58886e
7
+ data.tar.gz: 8239f6561bb4ab5882b40d75c415cbb45dc6c607d46bb9737fa32dbfc7121a219a6f30d5497d1b3f26eae767cdbd1901f7a77d9b3eb1b1cc48fbdc1d9fac62be
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # dstu-core (Ruby bindings)
2
+
3
+ **Provisional — not published to RubyGems, not independently audited.** See the root project's
4
+ `docs/SECURITY.md` and `docs/DECISIONS.md` for the full threat model and per-construction status.
5
+ This binding wraps the full `dstu_core::crypto_*` surface (`docs/bindings-strategy.md` T-160) —
6
+ install from source as shown below.
7
+
8
+ ## Installing (from source)
9
+
10
+ ```sh
11
+ gem install bundler
12
+ bundle install
13
+ bundle exec rake compile
14
+ ruby -Ilib -e 'require "dstu_core"; DstuCore.self_test'
15
+ ```
16
+
17
+ `magnus`/`rb_sys` need a matching Ruby + C toolchain to build the native extension — a real
18
+ `gcc`/`clang` install, not just Ruby itself. On Windows specifically, install the **DevKit**
19
+ variant of Ruby (bundles a matching MSYS2/mingw-w64-ucrt toolchain — a plain Ruby install has no
20
+ compiler wired up at all):
21
+
22
+ ```
23
+ winget install --id RubyInstallerTeam.RubyWithDevKit.3.3 --exact
24
+ ```
25
+
26
+ `rb-sys`'s `bindgen` step also needs a `libclang` that matches Ruby's own mingw-ucrt target — a
27
+ generic pre-existing Windows LLVM install parses Ruby's headers incorrectly. Install the matching
28
+ MSYS2 package and point `LIBCLANG_PATH` at it:
29
+
30
+ ```
31
+ pacman -S --needed mingw-w64-ucrt-x86_64-clang # via `ridk` or an MSYS2 shell
32
+ export LIBCLANG_PATH="/path/to/msys64/ucrt64/bin"
33
+ ```
34
+
35
+ `bundle exec rake compile` builds the Rust extension and copies it into `lib/dstu_core/`.
36
+ `DstuCore.self_test` re-runs `dstu_core::selftest::run()` (`docs/TASKS.md` T-161) against the
37
+ exact compiled build and raises `DstuCore::Error` if anything official-vector-level is wrong — the
38
+ first thing to run after any build to confirm it actually works, not just compiled.
39
+
40
+ This crate is its own Cargo workspace, split across `Cargo.toml` (the workspace root) and
41
+ `ext/dstu_core_rb/Cargo.toml` (the actual crate) — separate from the repo root
42
+ (`docs/DECISIONS.md` D-119) — build/test it from inside this directory, not from the repo root.
43
+ A **source** install (the steps above) needs this repo's own `crates/dstu-core` alongside it, since
44
+ `ext/dstu_core_rb/Cargo.toml` depends on it by relative path — it cannot install standalone outside
45
+ this repo. `rake native gem` instead builds a precompiled, platform-tagged gem that ships the
46
+ compiled extension directly and installs anywhere (see `docs/DECISIONS.md` D-136).
47
+
48
+ ## Usage
49
+
50
+ Every method/class below lives directly on the `DstuCore` module (no further nesting). Every
51
+ `String` this binding touches is binary (`ASCII-8BIT`) — `.b`/`force_encoding("BINARY")` a UTF-8
52
+ string before comparing it against a decrypted result. See `examples/` for complete, runnable
53
+ scripts, and `spec/` for the full correctness/rejection/misuse suite each one is verified against
54
+ (D-64/D-65).
55
+
56
+ ```ruby
57
+ require "dstu_core"
58
+
59
+ key = DstuCore.secretbox_keygen
60
+ sealed = DstuCore.secretbox_seal(key, "a message worth protecting")
61
+ raise unless DstuCore.secretbox_open(key, sealed) == "a message worth protecting"
62
+ ```
63
+
64
+ | Module | Methods/classes | Notes |
65
+ |---|---|---|
66
+ | `crypto_secretbox` | `secretbox_keygen`, `secretbox_seal`, `secretbox_open` | Single-message authenticated encryption. `examples/secretbox.rb`. |
67
+ | `crypto_box` | `box_keygen`, `box_public_key`, `box_seal`, `box_open` | Public-key encryption (hybrid via KDF over `hazmat::dstu9041`, `l(p)=256`, D-169). `box_seal`/`box_open` are not memory-bounded — the whole message is held in memory. `examples/box.rb`. |
68
+ | `crypto_box512` | `box512_keygen`, `box512_public_key`, `box512_seal`, `box512_open` | `l(p)=512`/E512/1 sibling of `crypto_box` (T-193/T-204). `examples/box512.rb`. |
69
+ | `crypto_secretstream` | `secretstream_keygen`, `SecretStreamPushState`, `SecretStreamPullState`, `SecretStreamWriter`, `SecretStreamReader` | Chunked streaming AEAD. `SecretStreamWriter`/`Reader` (modeled on stdlib's own `Zlib::GzipWriter`/`GzipReader`) wire format matches `uacrypt encrypt`/`decrypt` exactly (D-118). `examples/secretstream_file.rb`. |
70
+ | `crypto_sign` | `sign_keygen`, `sign_verifying_key`, `sign_message`, `sign_verify` | DSTU 4145 `m=163` digital signatures, deterministic nonce (no RNG dependency). `examples/sign.rb`. |
71
+ | `crypto_sign257` | `sign257_keygen`, `sign257_verifying_key`, `sign257_message`, `sign257_verify` | `m=257` sibling of `crypto_sign` (T-199/T-204) — the curve real Diia-issued qualified signatures use. `examples/sign257.rb`. |
72
+ | `crypto_pwhash` | `pwhash_hash_password`, `pwhash_verify_password`, `PWHASH_INTERACTIVE`/`PWHASH_MODERATE`/`PWHASH_SENSITIVE` | Argon2id (the one deliberately non-DSTU component, D-49/D-50). `examples/password_hashing.rb`. |
73
+ | `crypto_auth` | `auth_keygen`, `auth`, `auth_verify` | Keyed message authentication (Kupyna-KMAC). `examples/misc.rb`. |
74
+ | `crypto_kdf` | `kdf_keygen`, `kdf_derive_subkey` | Deterministic subkey derivation. `examples/misc.rb`. |
75
+ | `crypto_generichash` | `kupyna256`, `kupyna512`, `Kupyna256Hasher`, `Kupyna512Hasher` | One-shot and streaming Kupyna hashing. `examples/misc.rb`. |
76
+ | `crypto_stream` | `stream_keygen`, `stream_encrypt`, `stream_decrypt` | Strumok-256 keystream — **unauthenticated**, `stream_decrypt` never fails on tampered input. `examples/misc.rb`. |
77
+ | `randombytes` | `randombytes_buf` | CSPRNG-backed random bytes. `examples/misc.rb`. |
78
+ | — | `self_test`, `DstuCore::Error` | Runtime KAT self-check (T-161); the one exception class every crypto-operation failure raises (`ArgumentError` covers caller-input mistakes like a wrong-length key instead). |
79
+
80
+ ## Testing
81
+
82
+ ```sh
83
+ bundle exec rubocop
84
+ bundle exec rspec
85
+ ```
86
+
87
+ `cargo build -p uacrypt --release` (from the repo root) first if you want
88
+ `spec/secretstream_spec.rb`'s live `uacrypt` CLI interop test to actually run instead of being
89
+ `skip`ped. `cargo xtask ruby` (from the repo root) runs this whole sequence, including that build
90
+ step and `rake compile`, in one command.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rb_sys/extensiontask"
5
+ require "rspec/core/rake_task"
6
+
7
+ GEMSPEC = Gem::Specification.load("dstu_core.gemspec")
8
+
9
+ RbSys::ExtensionTask.new("dstu_core_rb", GEMSPEC) do |ext|
10
+ ext.lib_dir = "lib/dstu_core"
11
+ end
12
+
13
+ RSpec::Core::RakeTask.new(:spec)
14
+
15
+ task build: :compile
16
+ task default: %i[compile spec]
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
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
7
+ # Zlib::GzipWriter/Zlib::GzipReader - the same "wraps an IO, transforms chunks transparently"
8
+ # shape, the closest Ruby-native precedent for this problem (matching Python's file-like object,
9
+ # Node's stream.Transform).
10
+ #
11
+ # **Wire format matches `uacrypt encrypt`/`decrypt` exactly**
12
+ # (crates/uacrypt/src/lib.rs's `run_secretstream_encrypt`/`run_secretstream_decrypt`, D-68):
13
+ # `header (32 bytes)` followed by one record per chunk, `tag_byte (1) || chunk_len_u32_le (4) ||
14
+ # ciphertext (chunk_len) || auth_tag (16)`, chunks capped at 8 KiB (matching
15
+ # `SECRETSTREAM_CHUNK_BYTES`, not an independent choice) - a file `SecretStreamWriter` writes is
16
+ # decryptable by `uacrypt decrypt` and vice versa.
17
+ #
18
+ # **Every `String` this wrapper touches is binary (`ASCII-8BIT`/`BINARY`)**, both what it writes
19
+ # to `out`/`inp` and what `#read_all`/`#each` yield - matching every underlying `crypto_*` wrapper
20
+ # function (`RString::to_bytes`, D-134). A caller passing UTF-8 text must `.b`/
21
+ # `force_encoding("BINARY")` before comparing against the decrypted result, or the comparison sees
22
+ # differing encodings and fails even when the bytes match (`"привіт".b == "привіт"` is `false` in
23
+ # Ruby) - this is a caller-side encoding-compare gotcha, not a bug in this wrapper.
24
+ module DstuCore
25
+ # Matches crates/uacrypt/src/lib.rs's SECRETSTREAM_CHUNK_BYTES exactly - required for
26
+ # wire-format interop with `uacrypt encrypt`/`decrypt`, not an independent choice.
27
+ SECRETSTREAM_CHUNK_BYTES = 8 * 1024
28
+ SECRETSTREAM_AUTH_TAG_BYTES = 16
29
+
30
+ # Write-only wrapper: buffers input and pushes each full 8 KiB chunk to `out` as it fills, hiding
31
+ # the header/tag/framing bookkeeping entirely.
32
+ #
33
+ # File.open("out.bin", "wb") do |f|
34
+ # DstuCore::SecretStreamWriter.open(key, f) { |w| w.write("a whole file, incrementally") }
35
+ # end
36
+ class SecretStreamWriter
37
+ def initialize(key, out)
38
+ @out = out
39
+ @out.binmode if @out.respond_to?(:binmode)
40
+ @push = SecretStreamPushState.new(key)
41
+ @out.write(@push.header)
42
+ @buf = +""
43
+ @closed = false
44
+ end
45
+
46
+ # Block form: yields a writer, then closes it - **only on the success path**. The D-118
47
+ # pitfall this deliberately avoids: Ruby's own "always runs, even on error" cleanup idiom
48
+ # (`ensure`, the shape `File.open`/`Zlib::GzipWriter.wrap` both use) would finalize (emit the
49
+ # `Final` chunk) even when the block raised partway through, producing a stream that looks
50
+ # complete but silently drops data - violates D-65's "no partial output treated as valid on
51
+ # failure." If a truncated-but-decryptable prefix is genuinely wanted, call `close` explicitly
52
+ # inside a `rescue` clause instead.
53
+ def self.open(key, out)
54
+ writer = new(key, out)
55
+ yield writer
56
+ writer.close
57
+ writer
58
+ end
59
+
60
+ def closed?
61
+ @closed
62
+ end
63
+
64
+ # Buffers `data`, pushing any now-complete 8 KiB chunks immediately. The trailing partial (or
65
+ # exactly-8-KiB) chunk is always held back until `close`, since only `close` knows no more
66
+ # data is coming - the same one-chunk-ahead reasoning `uacrypt encrypt` itself uses to tag the
67
+ # true last chunk `Final`, not an extra empty one after it.
68
+ def write(data)
69
+ raise IOError, "closed stream" if @closed
70
+
71
+ @buf << data.b
72
+ while @buf.bytesize > SECRETSTREAM_CHUNK_BYTES
73
+ push_chunk(SECRETSTREAM_TAG_MESSAGE, @buf.byteslice(0, SECRETSTREAM_CHUNK_BYTES))
74
+ @buf = @buf.byteslice(SECRETSTREAM_CHUNK_BYTES..) || +""
75
+ end
76
+ data.bytesize
77
+ end
78
+
79
+ alias << write
80
+
81
+ # Flushes any buffered bytes as the stream's Final chunk. Idempotent - safe to call more than
82
+ # once, matching normal Ruby `IO#close` semantics.
83
+ def close
84
+ return if @closed
85
+
86
+ push_chunk(SECRETSTREAM_TAG_FINAL, @buf)
87
+ @buf = +""
88
+ @closed = true
89
+ end
90
+
91
+ private
92
+
93
+ def push_chunk(tag, data)
94
+ ciphertext, auth_tag = @push.push(tag, data)
95
+ @out.write([tag].pack("C"))
96
+ @out.write([data.bytesize].pack("V"))
97
+ @out.write(ciphertext)
98
+ @out.write(auth_tag)
99
+ end
100
+ end
101
+
102
+ # Read-only, chunk-iterating wrapper: reads and decrypts one chunk from `inp` at a time.
103
+ # `include Enumerable` gives `to_a`/`map`/etc. for free; `read_all` joins every plaintext chunk
104
+ # into one `String`, bounded only by available memory (the same caveat `crypto_secretbox`
105
+ # already carries). Raises `DstuCore::Error` on authentication failure or truncation - a
106
+ # dropped/tampered/reordered chunk, or a stream that ends before a Final chunk, both fail closed
107
+ # rather than yielding wrong plaintext.
108
+ #
109
+ # File.open("out.bin", "rb") do |f|
110
+ # plaintext = DstuCore::SecretStreamReader.open(key, f, &:read_all)
111
+ # end
112
+ class SecretStreamReader
113
+ include Enumerable
114
+
115
+ def initialize(key, inp)
116
+ @inp = inp
117
+ @inp.binmode if @inp.respond_to?(:binmode)
118
+ header = read_exact(32, "header")
119
+ @pull = SecretStreamPullState.new(key, header)
120
+ @done = false
121
+ end
122
+
123
+ def self.open(key, inp)
124
+ reader = new(key, inp)
125
+ yield reader
126
+ end
127
+
128
+ def each
129
+ return enum_for(:each) unless block_given?
130
+
131
+ yield next_chunk until @done
132
+ end
133
+
134
+ def read_all
135
+ each.to_a.join
136
+ end
137
+
138
+ # No-op - present for symmetry with SecretStreamWriter and Ruby's own IO-like `close` idiom.
139
+ def close; end
140
+
141
+ private
142
+
143
+ def next_chunk
144
+ tag_byte = read_exact(1, "chunk tag").unpack1("C")
145
+ chunk_len = read_exact(4, "chunk length").unpack1("V")
146
+ # `chunk_len` is untrusted wire input, read before any tag verification - reject an
147
+ # oversized declared length before acting on it (`read_exact` would otherwise try to
148
+ # accumulate up to `chunk_len` bytes from `inp`, which for a socket/pipe could mean
149
+ # gigabytes before ever failing). Matches `uacrypt decrypt`'s own
150
+ # `CliError::SecretstreamChunkTooLarge` bound (`crates/uacrypt/src/lib.rs`).
151
+ if chunk_len > SECRETSTREAM_CHUNK_BYTES
152
+ raise DstuCore::Error,
153
+ "secretstream chunk too large: declared #{chunk_len} bytes, max #{SECRETSTREAM_CHUNK_BYTES}"
154
+ end
155
+ ciphertext = read_exact(chunk_len, "chunk ciphertext")
156
+ auth_tag = read_exact(SECRETSTREAM_AUTH_TAG_BYTES, "chunk auth tag")
157
+ tag, plaintext = @pull.pull(tag_byte, ciphertext, auth_tag)
158
+ if tag == SECRETSTREAM_TAG_FINAL
159
+ # Matches `uacrypt decrypt`'s own `CliError::SecretstreamTrailingData` check - reject
160
+ # bytes remaining after `Final` rather than silently ignoring them. Checked before
161
+ # `plaintext` is returned, so a trailing-data stream never yields this last chunk either
162
+ # via `each` or `read_all`.
163
+ raise DstuCore::Error, "trailing data after the secretstream's Final chunk" if @inp.read(1)
164
+
165
+ @done = true
166
+ end
167
+ plaintext
168
+ end
169
+
170
+ def read_exact(size, what)
171
+ pieces = []
172
+ remaining = size
173
+ while remaining.positive?
174
+ piece = @inp.read(remaining)
175
+ break unless piece
176
+
177
+ pieces << piece
178
+ remaining -= piece.bytesize
179
+ end
180
+ data = pieces.join
181
+ if data.bytesize != size
182
+ raise DstuCore::Error, "truncated secretstream: expected #{size} bytes for #{what}, got #{data.bytesize}"
183
+ end
184
+
185
+ data
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DstuCore
4
+ VERSION = "0.1.0"
5
+ end
data/lib/dstu_core.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dstu_core/version"
4
+ require "dstu_core/dstu_core_rb"
5
+ require_relative "dstu_core/secretstream"
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dstu_core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: x86_64-linux
6
+ authors:
7
+ - dstu-core contributors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby bindings for dstu-core (Ukrainian DSTU cryptographic standards)
14
+ - provisional, not yet published to RubyGems.
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - Rakefile
22
+ - lib/dstu_core.rb
23
+ - lib/dstu_core/3.1/dstu_core_rb.so
24
+ - lib/dstu_core/3.2/dstu_core_rb.so
25
+ - lib/dstu_core/3.3/dstu_core_rb.so
26
+ - lib/dstu_core/3.4/dstu_core_rb.so
27
+ - lib/dstu_core/secretstream.rb
28
+ - lib/dstu_core/version.rb
29
+ homepage: https://github.com/user137/uacrypt
30
+ licenses:
31
+ - MIT
32
+ - Apache-2.0
33
+ metadata:
34
+ rubygems_mfa_required: 'true'
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '3.1'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: 3.5.dev
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.5.23
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Ruby bindings for dstu-core (Ukrainian DSTU cryptographic standards)
57
+ test_files: []