hide-protocol 0.5.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/LICENSE +17 -0
- data/README.md +177 -0
- data/lib/hide_protocol/binding.rb +173 -0
- data/lib/hide_protocol.rb +543 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c2a953028799de42c33b2d4500308cdc35dd51f0ab3a421135869f9b7f22659f
|
|
4
|
+
data.tar.gz: b666943d4bed7075eb7899e3ac5a5b9c7475db0cd6cca222e7ed29cca8b158ee
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b4e375e687c8c6dbdfec9bc487fbf0b8aedc32cf1e72f7139d018e5922ed9bf11f4951dd9cef88893b9cdd4642f46ef82e4c796a4f365135ed354228d25665aa
|
|
7
|
+
data.tar.gz: 99059cec81d10c21228681a9ea36a2985f944e3e34379e03921cc35a3a3a6106733ed26899d7ec701280e48ab897356b2a0a9acf040b299cf1f6d307400a2a25
|
data/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
|
|
17
|
+
The full license text is available at the URL above.
|
data/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# hide-protocol (Ruby)
|
|
2
|
+
|
|
3
|
+
Ruby bindings to the HIDE core: hybrid post-quantum encryption for files and
|
|
4
|
+
messages (X25519 + ML-KEM-768).
|
|
5
|
+
|
|
6
|
+
## Read this before using it
|
|
7
|
+
|
|
8
|
+
**This is experimental and unaudited.** Do not use it to protect data you
|
|
9
|
+
cannot afford to lose or expose. The format may change.
|
|
10
|
+
|
|
11
|
+
**A successful decryption proves the data was not altered. It does *not* prove
|
|
12
|
+
who sent it.** Anyone holding your public key can produce a container that
|
|
13
|
+
decrypts for you. If you need to know the sender, sign the message with
|
|
14
|
+
`Hide::SigningIdentity` and verify it separately.
|
|
15
|
+
|
|
16
|
+
A decrypted filename is attacker-controlled. Never use it to choose an output
|
|
17
|
+
path.
|
|
18
|
+
|
|
19
|
+
## How it binds
|
|
20
|
+
|
|
21
|
+
There is no Ruby cryptography here, and no compiled extension. The gem calls
|
|
22
|
+
the same Rust core as the CLI through its C ABI, using stdlib `fiddle` — so
|
|
23
|
+
installing it needs no compiler and adds no runtime gem dependency.
|
|
24
|
+
|
|
25
|
+
The native library is found in this order:
|
|
26
|
+
|
|
27
|
+
1. `HIDE_LIBRARY`, if set — the full path to the shared library.
|
|
28
|
+
2. Beside the gem, in `lib/hide_protocol/`, where the release workflow puts it.
|
|
29
|
+
3. The system loader.
|
|
30
|
+
|
|
31
|
+
Platform names are `hide_ffi.dll` on Windows, `libhide_ffi.dylib` on macOS and
|
|
32
|
+
`libhide_ffi.so` elsewhere.
|
|
33
|
+
|
|
34
|
+
### Running against a local build
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
cargo build -p hide-ffi
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then point `HIDE_LIBRARY` at the result:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
# Linux
|
|
44
|
+
export HIDE_LIBRARY="$PWD/target/debug/libhide_ffi.so"
|
|
45
|
+
# macOS
|
|
46
|
+
export HIDE_LIBRARY="$PWD/target/debug/libhide_ffi.dylib"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
# Windows
|
|
51
|
+
$env:HIDE_LIBRARY = "$PWD\target\debug\hide_ffi.dll"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Use
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
require "hide_protocol"
|
|
58
|
+
|
|
59
|
+
Hide::SecretKey.generate do |secret|
|
|
60
|
+
box = Hide.encrypt(
|
|
61
|
+
File.binread("salarii.csv"),
|
|
62
|
+
recipients: [secret.public_key],
|
|
63
|
+
filename: "salarii.csv",
|
|
64
|
+
media_type: "text/csv"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
opened = Hide.decrypt(box, secret)
|
|
68
|
+
opened.plaintext # => the original bytes
|
|
69
|
+
opened.filename # => "salarii.csv"
|
|
70
|
+
opened.media_type # => "text/csv"
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The block form always closes the key, including when the block raises. Without
|
|
75
|
+
a block, call `#close` yourself.
|
|
76
|
+
|
|
77
|
+
### Keys
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
secret = Hide::SecretKey.generate
|
|
81
|
+
public_key = secret.public_key # 1216 bytes, binary
|
|
82
|
+
|
|
83
|
+
sealed = secret.protect("a long passphrase") # for writing to disk
|
|
84
|
+
Hide.inspect_key(sealed) # => "protected"
|
|
85
|
+
|
|
86
|
+
reopened = Hide::SecretKey.open(sealed, "a long passphrase")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A passphrase must be at least `Hide::MIN_PASSPHRASE_LEN` (8) characters. A
|
|
90
|
+
forgotten passphrase cannot be recovered: there is no escrow.
|
|
91
|
+
|
|
92
|
+
Secret key material never crosses into Ruby. `inspect` and `to_s` report only
|
|
93
|
+
whether the key is open or closed, and a closed key raises on any use.
|
|
94
|
+
|
|
95
|
+
### Sharing a public key
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
text = Hide.armor_public_key(public_key) # "hide-public-key:..."
|
|
99
|
+
Hide.dearmor_public_key(text) # back to bytes
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Signing
|
|
103
|
+
|
|
104
|
+
Encryption and signing share one seed, so there is a single thing to back up.
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
sealed = Hide::SigningIdentity.generate("a long passphrase") # write this to disk
|
|
108
|
+
|
|
109
|
+
Hide::SigningIdentity.open(sealed, "a long passphrase") do |signer|
|
|
110
|
+
public_key = signer.public_key # 1984 bytes, shareable
|
|
111
|
+
signature = signer.sign("myapp/v1 release", bytes) # 3373 bytes
|
|
112
|
+
Hide.verify(public_key, "myapp/v1 release", bytes, signature)
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`verify` returns `nil` and raises on failure, rather than returning a boolean a
|
|
117
|
+
caller could forget to test. The context string separates uses of one identity:
|
|
118
|
+
never let a remote party choose it.
|
|
119
|
+
|
|
120
|
+
### Proving possession live
|
|
121
|
+
|
|
122
|
+
A detached signature proves possession at some point, to nobody in particular,
|
|
123
|
+
and can be replayed. A challenge binds a nonce, an audience and an expiry.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
challenge = Hide.new_challenge("ssh://host.example", Time.now.to_i, 60)
|
|
127
|
+
answer = signer.answer(challenge)
|
|
128
|
+
|
|
129
|
+
spent = Hide::SpentNonces.new # must outlive the request
|
|
130
|
+
spent.accept(challenge, answer, public_key, Time.now.to_i)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The second acceptance of the same answer raises `ChallengeReplayedError`, and
|
|
134
|
+
one presented after the window raises `ChallengeExpiredError`.
|
|
135
|
+
|
|
136
|
+
## API
|
|
137
|
+
|
|
138
|
+
| | |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `Hide.version` | version of the native core |
|
|
141
|
+
| `Hide.encrypt(plaintext, recipients:, filename: nil, media_type: nil)` | binary String |
|
|
142
|
+
| `Hide.decrypt(container, secret)` | `Decrypted` with `plaintext`, `filename`, `media_type` |
|
|
143
|
+
| `Hide.armor_public_key` / `Hide.dearmor_public_key` | text form of a public key |
|
|
144
|
+
| `Hide.inspect_key(bytes)` | `"raw"` or `"protected"` |
|
|
145
|
+
| `Hide::SecretKey.generate` / `.open(bytes, passphrase = nil)` | keys |
|
|
146
|
+
| `Hide::SigningIdentity.generate(passphrase)` | sealed key file bytes |
|
|
147
|
+
| `Hide::SigningIdentity.open(bytes, passphrase = nil)` | `#public_key`, `#sign`, `#answer`, `#close` |
|
|
148
|
+
| `Hide.verify(public_key, context, message, signature)` | raises unless it verifies |
|
|
149
|
+
| `Hide.new_challenge(audience, now, valid_for)` | challenge bytes |
|
|
150
|
+
| `Hide::SpentNonces#accept(challenge, signature, public_key, now)` | accepts once |
|
|
151
|
+
| `Hide::PUBLIC_KEY_LEN` (1216), `Hide::MIN_PASSPHRASE_LEN` (8) | constants |
|
|
152
|
+
| `Hide::SIGNATURE_LEN` (3373), `Hide::VERIFYING_KEY_LEN` (1984), `Hide::NONCE_LEN` (32) | constants |
|
|
153
|
+
|
|
154
|
+
Encryption takes between 1 and 64 recipients, each public key exactly
|
|
155
|
+
`Hide::PUBLIC_KEY_LEN` bytes.
|
|
156
|
+
|
|
157
|
+
All binary values in and out are ASCII-8BIT (binary) Strings. Metadata comes
|
|
158
|
+
back as UTF-8.
|
|
159
|
+
|
|
160
|
+
### Errors
|
|
161
|
+
|
|
162
|
+
Everything raises a subclass of `Hide::Error`:
|
|
163
|
+
|
|
164
|
+
`InvalidArgumentError`, `AuthenticationError` (altered data, or not a
|
|
165
|
+
container), `WrongPassphraseError`, `NoMatchingRecipientError` (this key was
|
|
166
|
+
not a recipient), `NotAKeyError`, `TooLargeError`, `ClosedKeyError`,
|
|
167
|
+
`ChallengeExpiredError`, `ChallengeReplayedError`.
|
|
168
|
+
|
|
169
|
+
## Tests
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
HIDE_LIBRARY=/path/to/libhide_ffi.so ruby -Ilib -Itest test/test_hide.rb
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Licence
|
|
176
|
+
|
|
177
|
+
Apache-2.0.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
|
|
6
|
+
module Hide
|
|
7
|
+
# Raw access to the HIDE C core.
|
|
8
|
+
#
|
|
9
|
+
# fiddle rather than the ffi gem: fiddle ships with Ruby, so installing this
|
|
10
|
+
# gem needs no compiler and adds no runtime dependency, and the ABI is
|
|
11
|
+
# described in exactly one place.
|
|
12
|
+
module Binding
|
|
13
|
+
OK = 0
|
|
14
|
+
ERR_INVALID_ARGUMENT = 1
|
|
15
|
+
ERR_WRONG_PASSPHRASE = 2
|
|
16
|
+
ERR_NOT_A_KEY = 3
|
|
17
|
+
ERR_AUTHENTICATION = 4
|
|
18
|
+
ERR_NO_MATCHING_RECIPIENT = 5
|
|
19
|
+
ERR_TOO_LARGE = 7
|
|
20
|
+
ERR_MALFORMED = 6
|
|
21
|
+
ERR_CHALLENGE_EXPIRED = 8
|
|
22
|
+
ERR_CHALLENGE_REPLAYED = 9
|
|
23
|
+
ERR_PANIC = 98
|
|
24
|
+
ERR_INTERNAL = 99
|
|
25
|
+
|
|
26
|
+
KEY_RAW = 0
|
|
27
|
+
KEY_PROTECTED = 1
|
|
28
|
+
|
|
29
|
+
PUBLIC_KEY_LEN = 1216
|
|
30
|
+
SIGNATURE_LEN = 3373
|
|
31
|
+
VERIFYING_KEY_LEN = 1984
|
|
32
|
+
NONCE_LEN = 32
|
|
33
|
+
MIN_PASSPHRASE_LEN = 8
|
|
34
|
+
|
|
35
|
+
WORD = Fiddle::SIZEOF_VOIDP
|
|
36
|
+
# HideBuffer is { uint8_t *data; size_t len; size_t capacity; }.
|
|
37
|
+
BUFFER_SIZE = WORD * 3
|
|
38
|
+
# size_t and a pointer are the same width on every platform Ruby builds on.
|
|
39
|
+
WORD_PACK = WORD == 8 ? "J" : "L"
|
|
40
|
+
|
|
41
|
+
class LibraryNotFound < StandardError; end
|
|
42
|
+
|
|
43
|
+
def self.library_names
|
|
44
|
+
case RbConfig::CONFIG["host_os"]
|
|
45
|
+
when /mswin|mingw|cygwin/ then ["hide_ffi.dll"]
|
|
46
|
+
when /darwin/ then ["libhide_ffi.dylib"]
|
|
47
|
+
else ["libhide_ffi.so"]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Mirrors the Python binding: an explicit override, then the copy shipped
|
|
52
|
+
# beside the gem, then whatever the system loader can find.
|
|
53
|
+
def self.load_library
|
|
54
|
+
here = File.dirname(__FILE__)
|
|
55
|
+
candidates = library_names.map { |name| File.join(here, name) }
|
|
56
|
+
|
|
57
|
+
override = ENV["HIDE_LIBRARY"]
|
|
58
|
+
candidates.unshift(override) if override && !override.empty?
|
|
59
|
+
|
|
60
|
+
candidates.each do |candidate|
|
|
61
|
+
return Fiddle.dlopen(candidate) if File.exist?(candidate)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
Fiddle.dlopen(library_names.first)
|
|
66
|
+
rescue Fiddle::DLError
|
|
67
|
+
raise LibraryNotFound,
|
|
68
|
+
"the HIDE native library was not found. Install a gem that " \
|
|
69
|
+
"bundles it, or set HIDE_LIBRARY to the path of " \
|
|
70
|
+
"#{library_names.first} built by `cargo build -p hide-ffi`."
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
LIB = load_library
|
|
75
|
+
|
|
76
|
+
VOIDP = Fiddle::TYPE_VOIDP
|
|
77
|
+
INT32 = Fiddle::TYPE_INT
|
|
78
|
+
SIZE_T = Fiddle::TYPE_SIZE_T
|
|
79
|
+
VOID = Fiddle::TYPE_VOID
|
|
80
|
+
UINT64 = Fiddle::TYPE_LONG_LONG
|
|
81
|
+
|
|
82
|
+
SIGNATURES = {
|
|
83
|
+
hide_error_message: [[INT32], VOIDP],
|
|
84
|
+
hide_version: [[], VOIDP],
|
|
85
|
+
# A 24-byte struct return is passed through a hidden out-pointer by both
|
|
86
|
+
# the SysV and the Windows x64 ABI, so binding it this way is the
|
|
87
|
+
# portable spelling; see empty_buffer for the fallback.
|
|
88
|
+
hide_buffer_empty: [[VOIDP], VOID],
|
|
89
|
+
hide_buffer_free: [[VOIDP], VOID],
|
|
90
|
+
hide_keypair_generate: [[VOIDP, VOIDP], INT32],
|
|
91
|
+
hide_inspect_key: [[VOIDP, SIZE_T, VOIDP], INT32],
|
|
92
|
+
hide_secret_key_open: [[VOIDP, SIZE_T, VOIDP, VOIDP], INT32],
|
|
93
|
+
hide_secret_key_protect: [[VOIDP, VOIDP, VOIDP], INT32],
|
|
94
|
+
hide_secret_key_public: [[VOIDP, VOIDP], INT32],
|
|
95
|
+
hide_secret_key_free: [[VOIDP], VOID],
|
|
96
|
+
hide_public_key_armor: [[VOIDP, SIZE_T, VOIDP], INT32],
|
|
97
|
+
hide_public_key_dearmor: [[VOIDP, VOIDP], INT32],
|
|
98
|
+
hide_encrypt: [[VOIDP, SIZE_T, VOIDP, SIZE_T, VOIDP, VOIDP, VOIDP], INT32],
|
|
99
|
+
hide_decrypt: [[VOIDP, SIZE_T, VOIDP, VOIDP, VOIDP, VOIDP], INT32],
|
|
100
|
+
hide_identity_generate: [[VOIDP, VOIDP], INT32],
|
|
101
|
+
hide_signing_identity_open: [[VOIDP, SIZE_T, VOIDP, VOIDP], INT32],
|
|
102
|
+
hide_signing_identity_public: [[VOIDP, VOIDP], INT32],
|
|
103
|
+
hide_signing_identity_free: [[VOIDP], VOID],
|
|
104
|
+
hide_sign_message: [[VOIDP, VOIDP, SIZE_T, VOIDP, SIZE_T, VOIDP], INT32],
|
|
105
|
+
hide_verify_message: [[VOIDP, SIZE_T, VOIDP, SIZE_T, VOIDP, SIZE_T, VOIDP, SIZE_T], INT32],
|
|
106
|
+
hide_challenge_new: [[VOIDP, UINT64, UINT64, VOIDP], INT32],
|
|
107
|
+
hide_challenge_answer: [[VOIDP, VOIDP, SIZE_T, VOIDP], INT32],
|
|
108
|
+
hide_spent_nonces_new: [[], VOIDP],
|
|
109
|
+
hide_spent_nonces_free: [[VOIDP], VOID],
|
|
110
|
+
hide_challenge_accept: [[VOIDP, VOIDP, SIZE_T, VOIDP, SIZE_T, VOIDP, SIZE_T, UINT64], INT32]
|
|
111
|
+
}.freeze
|
|
112
|
+
|
|
113
|
+
FUNCTIONS = SIGNATURES.each_with_object({}) do |(name, (args, ret)), acc|
|
|
114
|
+
acc[name] = Fiddle::Function.new(LIB[name.to_s], args, ret, name: name.to_s)
|
|
115
|
+
end.freeze
|
|
116
|
+
|
|
117
|
+
def self.call(name, *args)
|
|
118
|
+
FUNCTIONS.fetch(name).call(*args)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A static C string owned by the library; never freed by us.
|
|
122
|
+
def self.static_string(pointer)
|
|
123
|
+
return "" if pointer.null?
|
|
124
|
+
|
|
125
|
+
Fiddle::Pointer.new(pointer.to_i).to_s.force_encoding(Encoding::UTF_8)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Scratch space for one HideBuffer, zeroed the way hide_buffer_empty zeroes it.
|
|
129
|
+
def self.empty_buffer
|
|
130
|
+
slot = Fiddle::Pointer.malloc(BUFFER_SIZE, Fiddle::RUBY_FREE)
|
|
131
|
+
slot[0, BUFFER_SIZE] = "\x00" * BUFFER_SIZE
|
|
132
|
+
call(:hide_buffer_empty, slot)
|
|
133
|
+
slot
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.read_word(slot, index)
|
|
137
|
+
slot[index * WORD, WORD].unpack1(WORD_PACK)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Copies a native buffer out and frees the original. The length field is
|
|
141
|
+
# the only thing consulted: text from this library is length-prefixed, not
|
|
142
|
+
# NUL-terminated, because metadata is attacker-controlled and a NUL in it
|
|
143
|
+
# would silently truncate a C-string read.
|
|
144
|
+
def self.take(slot)
|
|
145
|
+
data = read_word(slot, 0)
|
|
146
|
+
len = read_word(slot, 1)
|
|
147
|
+
bytes =
|
|
148
|
+
if data.zero? || len.zero?
|
|
149
|
+
+""
|
|
150
|
+
else
|
|
151
|
+
Fiddle::Pointer.new(data, len)[0, len]
|
|
152
|
+
end
|
|
153
|
+
bytes.force_encoding(Encoding::BINARY)
|
|
154
|
+
ensure
|
|
155
|
+
call(:hide_buffer_free, slot)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Metadata arrives as length-prefixed UTF-8; empty means absent.
|
|
159
|
+
def self.take_text(slot)
|
|
160
|
+
raw = take(slot)
|
|
161
|
+
return nil if raw.empty?
|
|
162
|
+
|
|
163
|
+
raw.dup.force_encoding(Encoding::UTF_8).scrub
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# A slot holding one pointer-sized out-parameter.
|
|
167
|
+
def self.pointer_slot
|
|
168
|
+
slot = Fiddle::Pointer.malloc(WORD, Fiddle::RUBY_FREE)
|
|
169
|
+
slot[0, WORD] = "\x00" * WORD
|
|
170
|
+
slot
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hide_protocol/binding"
|
|
4
|
+
|
|
5
|
+
# HIDE — encrypt to a person, not to a key.
|
|
6
|
+
#
|
|
7
|
+
# EXPERIMENTAL AND UNAUDITED. Do not protect data you cannot afford to lose or
|
|
8
|
+
# expose. A successful decryption proves the data was not altered; it does
|
|
9
|
+
# *not* prove who created it.
|
|
10
|
+
#
|
|
11
|
+
# secret = Hide::SecretKey.generate
|
|
12
|
+
# box = Hide.encrypt("hello", recipients: [secret.public_key])
|
|
13
|
+
# Hide.decrypt(box, secret).plaintext # => "hello"
|
|
14
|
+
module Hide
|
|
15
|
+
PUBLIC_KEY_LEN = Binding::PUBLIC_KEY_LEN
|
|
16
|
+
SIGNATURE_LEN = Binding::SIGNATURE_LEN
|
|
17
|
+
VERIFYING_KEY_LEN = Binding::VERIFYING_KEY_LEN
|
|
18
|
+
NONCE_LEN = Binding::NONCE_LEN
|
|
19
|
+
MIN_PASSPHRASE_LEN = Binding::MIN_PASSPHRASE_LEN
|
|
20
|
+
|
|
21
|
+
MAX_RECIPIENTS = 64
|
|
22
|
+
|
|
23
|
+
# Base class for every failure this library reports.
|
|
24
|
+
class Error < StandardError; end
|
|
25
|
+
|
|
26
|
+
# An argument this library refused before it reached the core.
|
|
27
|
+
class InvalidArgumentError < Error; end
|
|
28
|
+
|
|
29
|
+
# The data was altered, or is not a HIDE container.
|
|
30
|
+
class AuthenticationError < Error; end
|
|
31
|
+
|
|
32
|
+
# The passphrase is wrong, or the key file was modified.
|
|
33
|
+
class WrongPassphraseError < Error; end
|
|
34
|
+
|
|
35
|
+
# This key was not one of the recipients.
|
|
36
|
+
class NoMatchingRecipientError < Error; end
|
|
37
|
+
|
|
38
|
+
# The bytes are not a HIDE key.
|
|
39
|
+
class NotAKeyError < Error; end
|
|
40
|
+
|
|
41
|
+
# More data than the format admits.
|
|
42
|
+
class TooLargeError < Error; end
|
|
43
|
+
|
|
44
|
+
# The key has been closed and its material released.
|
|
45
|
+
class ClosedKeyError < Error; end
|
|
46
|
+
|
|
47
|
+
# The challenge expired before it was answered.
|
|
48
|
+
class ChallengeExpiredError < Error; end
|
|
49
|
+
|
|
50
|
+
# This challenge was already answered. Almost certainly a replay.
|
|
51
|
+
class ChallengeReplayedError < Error; end
|
|
52
|
+
|
|
53
|
+
ERRORS = {
|
|
54
|
+
Binding::ERR_INVALID_ARGUMENT => InvalidArgumentError,
|
|
55
|
+
Binding::ERR_WRONG_PASSPHRASE => WrongPassphraseError,
|
|
56
|
+
Binding::ERR_NOT_A_KEY => NotAKeyError,
|
|
57
|
+
Binding::ERR_AUTHENTICATION => AuthenticationError,
|
|
58
|
+
Binding::ERR_NO_MATCHING_RECIPIENT => NoMatchingRecipientError,
|
|
59
|
+
Binding::ERR_MALFORMED => AuthenticationError,
|
|
60
|
+
Binding::ERR_TOO_LARGE => TooLargeError,
|
|
61
|
+
Binding::ERR_CHALLENGE_EXPIRED => ChallengeExpiredError,
|
|
62
|
+
Binding::ERR_CHALLENGE_REPLAYED => ChallengeReplayedError
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
def version
|
|
67
|
+
Binding.static_string(Binding.call(:hide_version))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Encrypts for 1..64 recipient public keys. Returns binary bytes.
|
|
71
|
+
def encrypt(plaintext, recipients:, filename: nil, media_type: nil)
|
|
72
|
+
joined = join_recipients(recipients)
|
|
73
|
+
plain = binary(plaintext, "plaintext")
|
|
74
|
+
|
|
75
|
+
out = Binding.empty_buffer
|
|
76
|
+
check(Binding.call(
|
|
77
|
+
:hide_encrypt,
|
|
78
|
+
buffer_arg(plain), plain.bytesize,
|
|
79
|
+
joined, recipients.length,
|
|
80
|
+
cstring(filename), cstring(media_type),
|
|
81
|
+
out
|
|
82
|
+
))
|
|
83
|
+
Binding.take(out)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Decrypts and verifies. Nothing is returned unless the whole payload
|
|
87
|
+
# authenticates, so a caller cannot act on unverified data.
|
|
88
|
+
def decrypt(container, secret)
|
|
89
|
+
raise TypeError, "secret must be a Hide::SecretKey" unless secret.is_a?(SecretKey)
|
|
90
|
+
|
|
91
|
+
bytes = binary(container, "container")
|
|
92
|
+
out = Binding.empty_buffer
|
|
93
|
+
filename = Binding.empty_buffer
|
|
94
|
+
media_type = Binding.empty_buffer
|
|
95
|
+
begin
|
|
96
|
+
check(Binding.call(
|
|
97
|
+
:hide_decrypt,
|
|
98
|
+
buffer_arg(bytes), bytes.bytesize,
|
|
99
|
+
secret.__handle,
|
|
100
|
+
out, filename, media_type
|
|
101
|
+
))
|
|
102
|
+
rescue StandardError
|
|
103
|
+
# On failure the core leaves the out-parameters untouched, so these are
|
|
104
|
+
# still the empty buffers we created and freeing them is what releases
|
|
105
|
+
# the scratch space.
|
|
106
|
+
Binding.take(out)
|
|
107
|
+
Binding.take(filename)
|
|
108
|
+
Binding.take(media_type)
|
|
109
|
+
raise
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Decrypted.new(
|
|
113
|
+
Binding.take(out),
|
|
114
|
+
Binding.take_text(filename),
|
|
115
|
+
Binding.take_text(media_type)
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Renders a public key as pasteable text.
|
|
120
|
+
def armor_public_key(public_key)
|
|
121
|
+
bytes = binary(public_key, "public key")
|
|
122
|
+
out = Binding.empty_buffer
|
|
123
|
+
check(Binding.call(:hide_public_key_armor, buffer_arg(bytes), bytes.bytesize, out))
|
|
124
|
+
Binding.take_text(out) || ""
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def dearmor_public_key(text)
|
|
128
|
+
out = Binding.empty_buffer
|
|
129
|
+
check(Binding.call(:hide_public_key_dearmor, cstring(text.to_s), out))
|
|
130
|
+
Binding.take(out)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns "raw" or "protected" without needing the passphrase.
|
|
134
|
+
def inspect_key(data)
|
|
135
|
+
bytes = binary(data, "key")
|
|
136
|
+
slot = Binding.pointer_slot
|
|
137
|
+
check(Binding.call(:hide_inspect_key, buffer_arg(bytes), bytes.bytesize, slot))
|
|
138
|
+
kind = slot[0, Binding::WORD].unpack1(Binding::WORD_PACK) & 0xFFFFFFFF
|
|
139
|
+
kind == Binding::KEY_PROTECTED ? "protected" : "raw"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def sign(identity, context, message)
|
|
143
|
+
identity.sign(context, message)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Raises unless both the Ed25519 and the ML-DSA half verify. Nothing is
|
|
147
|
+
# returned: a caller who forgot to test a boolean would read every failure
|
|
148
|
+
# as a pass.
|
|
149
|
+
def verify(public_key, context, message, signature)
|
|
150
|
+
key = binary(public_key, "public key")
|
|
151
|
+
ctx = binary(context, "context")
|
|
152
|
+
msg = binary(message, "message")
|
|
153
|
+
sig = binary(signature, "signature")
|
|
154
|
+
check(Binding.call(
|
|
155
|
+
:hide_verify_message,
|
|
156
|
+
buffer_arg(key), key.bytesize,
|
|
157
|
+
buffer_arg(ctx), ctx.bytesize,
|
|
158
|
+
buffer_arg(msg), msg.bytesize,
|
|
159
|
+
buffer_arg(sig), sig.bytesize
|
|
160
|
+
))
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# A detached signature proves possession at some point, to nobody in
|
|
165
|
+
# particular, and can be replayed. A challenge binds a random nonce, an
|
|
166
|
+
# audience and an expiry, so an answer is good once, here, now.
|
|
167
|
+
def new_challenge(audience, now, valid_for)
|
|
168
|
+
out = Binding.empty_buffer
|
|
169
|
+
check(Binding.call(
|
|
170
|
+
:hide_challenge_new,
|
|
171
|
+
cstring(audience), Integer(now), Integer(valid_for), out
|
|
172
|
+
))
|
|
173
|
+
Binding.take(out)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def check(code)
|
|
177
|
+
return if code == Binding::OK
|
|
178
|
+
|
|
179
|
+
message = Binding.static_string(Binding.call(:hide_error_message, code))
|
|
180
|
+
raise ERRORS.fetch(code, Error), message
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def join_recipients(recipients)
|
|
186
|
+
unless recipients.is_a?(Array)
|
|
187
|
+
raise InvalidArgumentError, "recipients must be an array of public keys"
|
|
188
|
+
end
|
|
189
|
+
unless recipients.length.between?(1, MAX_RECIPIENTS)
|
|
190
|
+
raise InvalidArgumentError, "there must be between 1 and #{MAX_RECIPIENTS} recipients"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
recipients.each_with_object(+"") do |key, acc|
|
|
194
|
+
bytes = binary(key, "public key")
|
|
195
|
+
if bytes.bytesize != PUBLIC_KEY_LEN
|
|
196
|
+
raise InvalidArgumentError,
|
|
197
|
+
"a public key is #{PUBLIC_KEY_LEN} bytes, got #{bytes.bytesize}"
|
|
198
|
+
end
|
|
199
|
+
acc << bytes
|
|
200
|
+
end.force_encoding(Encoding::BINARY)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def binary(value, what)
|
|
204
|
+
raise InvalidArgumentError, "#{what} must be a String" unless value.is_a?(String)
|
|
205
|
+
|
|
206
|
+
value.dup.force_encoding(Encoding::BINARY)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Fiddle passes a String as a pointer to its bytes, but a zero-length
|
|
210
|
+
# String has no address the callee may read, so give it one it can ignore.
|
|
211
|
+
def buffer_arg(bytes)
|
|
212
|
+
bytes.empty? ? Fiddle::Pointer.malloc(1, Fiddle::RUBY_FREE) : bytes
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# NULL for absent, UTF-8 plus a terminator otherwise.
|
|
216
|
+
def cstring(value)
|
|
217
|
+
return nil if value.nil?
|
|
218
|
+
|
|
219
|
+
text = value.to_s
|
|
220
|
+
if text.include?("\x00")
|
|
221
|
+
raise InvalidArgumentError, "text passed to the core must not contain NUL"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
"#{text.encode(Encoding::UTF_8)}\x00".force_encoding(Encoding::BINARY)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# A verified payload. Holding one of these means it authenticated.
|
|
229
|
+
#
|
|
230
|
+
# The filename is attacker-controlled: never use it to choose an output path.
|
|
231
|
+
class Decrypted
|
|
232
|
+
attr_reader :plaintext, :filename, :media_type
|
|
233
|
+
|
|
234
|
+
def initialize(plaintext, filename, media_type)
|
|
235
|
+
@plaintext = plaintext
|
|
236
|
+
@filename = filename
|
|
237
|
+
@media_type = media_type
|
|
238
|
+
freeze
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
alias data plaintext
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Argument coercion shared by the classes that pass byte strings to the core.
|
|
245
|
+
module Bytes
|
|
246
|
+
private
|
|
247
|
+
|
|
248
|
+
def binary(value, what)
|
|
249
|
+
raise InvalidArgumentError, "#{what} must be a String" unless value.is_a?(String)
|
|
250
|
+
|
|
251
|
+
value.dup.force_encoding(Encoding::BINARY)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Fiddle passes a String as a pointer to its bytes, but a zero-length
|
|
255
|
+
# String has no address the callee may read, so give it one it can ignore.
|
|
256
|
+
def arg(bytes)
|
|
257
|
+
bytes.empty? ? Fiddle::Pointer.malloc(1, Fiddle::RUBY_FREE) : bytes
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# A signing key. The seed stays inside the native library and is never
|
|
262
|
+
# exposed to Ruby; there is deliberately no accessor for it.
|
|
263
|
+
class SigningIdentity
|
|
264
|
+
include Bytes
|
|
265
|
+
|
|
266
|
+
class << self
|
|
267
|
+
# Creates an identity and returns the sealed key file to store. One seed
|
|
268
|
+
# backs both encryption and signing, so there is one thing to back up.
|
|
269
|
+
def generate(passphrase)
|
|
270
|
+
text = passphrase.to_s
|
|
271
|
+
if text.length < MIN_PASSPHRASE_LEN
|
|
272
|
+
raise InvalidArgumentError,
|
|
273
|
+
"the passphrase must be at least #{MIN_PASSPHRASE_LEN} characters"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
out = Binding.empty_buffer
|
|
277
|
+
Hide.check(Binding.call(:hide_identity_generate, "#{text}\x00".b, out))
|
|
278
|
+
Binding.take(out)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Loads a signing identity. A key file written before signatures existed
|
|
282
|
+
# carries no signing seed and fails rather than being downgraded.
|
|
283
|
+
def open(data, passphrase = nil, &block)
|
|
284
|
+
raise InvalidArgumentError, "key data must be a String" unless data.is_a?(String)
|
|
285
|
+
|
|
286
|
+
bytes = data.dup.force_encoding(Encoding::BINARY)
|
|
287
|
+
handle = Binding.pointer_slot
|
|
288
|
+
Hide.check(Binding.call(
|
|
289
|
+
:hide_signing_identity_open,
|
|
290
|
+
bytes.empty? ? Fiddle::Pointer.malloc(1, Fiddle::RUBY_FREE) : bytes,
|
|
291
|
+
bytes.bytesize,
|
|
292
|
+
passphrase.nil? ? nil : "#{passphrase}\x00".b,
|
|
293
|
+
handle
|
|
294
|
+
))
|
|
295
|
+
identity = new(handle[0, Binding::WORD].unpack1(Binding::WORD_PACK))
|
|
296
|
+
return identity unless block
|
|
297
|
+
|
|
298
|
+
begin
|
|
299
|
+
block.call(identity)
|
|
300
|
+
ensure
|
|
301
|
+
identity.close
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
alias load open
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def initialize(address)
|
|
309
|
+
@address = address
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# The shareable verifying key, for others to check signatures with.
|
|
313
|
+
def public_key
|
|
314
|
+
alive!
|
|
315
|
+
out = Binding.empty_buffer
|
|
316
|
+
Hide.check(Binding.call(:hide_signing_identity_public, handle, out))
|
|
317
|
+
Binding.take(out)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# context separates uses of one identity, so a signature made for one
|
|
321
|
+
# purpose cannot be replayed as another. Never let a remote party choose it.
|
|
322
|
+
def sign(context, message)
|
|
323
|
+
alive!
|
|
324
|
+
ctx = binary(context, "context")
|
|
325
|
+
msg = binary(message, "message")
|
|
326
|
+
out = Binding.empty_buffer
|
|
327
|
+
Hide.check(Binding.call(
|
|
328
|
+
:hide_sign_message,
|
|
329
|
+
handle,
|
|
330
|
+
arg(ctx), ctx.bytesize,
|
|
331
|
+
arg(msg), msg.bytesize,
|
|
332
|
+
out
|
|
333
|
+
))
|
|
334
|
+
Binding.take(out)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Answers a challenge, proving possession to whoever issued it.
|
|
338
|
+
def answer(challenge)
|
|
339
|
+
alive!
|
|
340
|
+
bytes = binary(challenge, "challenge")
|
|
341
|
+
out = Binding.empty_buffer
|
|
342
|
+
Hide.check(Binding.call(:hide_challenge_answer, handle, arg(bytes), bytes.bytesize, out))
|
|
343
|
+
Binding.take(out)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def close
|
|
347
|
+
return if @address.nil? || @address.zero?
|
|
348
|
+
|
|
349
|
+
Binding.call(:hide_signing_identity_free, Fiddle::Pointer.new(@address))
|
|
350
|
+
@address = nil
|
|
351
|
+
nil
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def closed?
|
|
355
|
+
@address.nil? || @address.zero?
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Never render key material, not even a fingerprint of it.
|
|
359
|
+
def inspect
|
|
360
|
+
"#<Hide::SigningIdentity #{closed? ? "closed" : "open"}>"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
alias to_s inspect
|
|
364
|
+
|
|
365
|
+
private
|
|
366
|
+
|
|
367
|
+
def handle
|
|
368
|
+
Fiddle::Pointer.new(@address)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def alive!
|
|
372
|
+
raise ClosedKeyError, "this identity has been closed" if closed?
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# The verifier's record of answered challenges.
|
|
377
|
+
#
|
|
378
|
+
# Replay can only be detected by the verifier: a replayed answer is a genuine
|
|
379
|
+
# signature and nothing about it is invalid on its own. This must therefore
|
|
380
|
+
# outlive a single request.
|
|
381
|
+
class SpentNonces
|
|
382
|
+
include Bytes
|
|
383
|
+
|
|
384
|
+
def self.open
|
|
385
|
+
record = new
|
|
386
|
+
return record unless block_given?
|
|
387
|
+
|
|
388
|
+
begin
|
|
389
|
+
yield record
|
|
390
|
+
ensure
|
|
391
|
+
record.close
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def initialize
|
|
396
|
+
pointer = Binding.call(:hide_spent_nonces_new)
|
|
397
|
+
raise Error, "could not allocate the nonce record" if pointer.null?
|
|
398
|
+
|
|
399
|
+
@address = pointer.to_i
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Accepts an answer exactly once: raises ChallengeReplayedError the second
|
|
403
|
+
# time, ChallengeExpiredError after the window, AuthenticationError if it
|
|
404
|
+
# does not verify.
|
|
405
|
+
def accept(challenge, signature, public_key, now)
|
|
406
|
+
raise ClosedKeyError, "this record has been closed" if closed?
|
|
407
|
+
|
|
408
|
+
chal = binary(challenge, "challenge")
|
|
409
|
+
sig = binary(signature, "signature")
|
|
410
|
+
key = binary(public_key, "public key")
|
|
411
|
+
Hide.check(Binding.call(
|
|
412
|
+
:hide_challenge_accept,
|
|
413
|
+
Fiddle::Pointer.new(@address),
|
|
414
|
+
arg(chal), chal.bytesize,
|
|
415
|
+
arg(sig), sig.bytesize,
|
|
416
|
+
arg(key), key.bytesize,
|
|
417
|
+
Integer(now)
|
|
418
|
+
))
|
|
419
|
+
nil
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def close
|
|
423
|
+
return if closed?
|
|
424
|
+
|
|
425
|
+
Binding.call(:hide_spent_nonces_free, Fiddle::Pointer.new(@address))
|
|
426
|
+
@address = nil
|
|
427
|
+
nil
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def closed?
|
|
431
|
+
@address.nil? || @address.zero?
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# A secret key. The bytes stay inside the native library and are never
|
|
436
|
+
# exposed to Ruby; there is deliberately no accessor for them.
|
|
437
|
+
class SecretKey
|
|
438
|
+
class << self
|
|
439
|
+
def generate(&block)
|
|
440
|
+
handle = Binding.pointer_slot
|
|
441
|
+
public_key = Binding.empty_buffer
|
|
442
|
+
Hide.check(Binding.call(:hide_keypair_generate, handle, public_key))
|
|
443
|
+
Binding.take(public_key)
|
|
444
|
+
wrap(handle, &block)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Loads a key file. A protected key without its passphrase fails.
|
|
448
|
+
def open(data, passphrase = nil, &block)
|
|
449
|
+
unless data.is_a?(String)
|
|
450
|
+
raise InvalidArgumentError, "key data must be a String"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
bytes = data.dup.force_encoding(Encoding::BINARY)
|
|
454
|
+
handle = Binding.pointer_slot
|
|
455
|
+
Hide.check(Binding.call(
|
|
456
|
+
:hide_secret_key_open,
|
|
457
|
+
bytes.empty? ? Fiddle::Pointer.malloc(1, Fiddle::RUBY_FREE) : bytes,
|
|
458
|
+
bytes.bytesize,
|
|
459
|
+
passphrase.nil? ? nil : "#{passphrase}\x00".b,
|
|
460
|
+
handle
|
|
461
|
+
))
|
|
462
|
+
wrap(handle, &block)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
alias load open
|
|
466
|
+
|
|
467
|
+
private
|
|
468
|
+
|
|
469
|
+
# With a block the key is always closed, even if the block raises.
|
|
470
|
+
def wrap(handle, &block)
|
|
471
|
+
key = new(handle[0, Binding::WORD].unpack1(Binding::WORD_PACK))
|
|
472
|
+
return key unless block
|
|
473
|
+
|
|
474
|
+
begin
|
|
475
|
+
block.call(key)
|
|
476
|
+
ensure
|
|
477
|
+
key.close
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def initialize(address)
|
|
483
|
+
@address = address
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def public_key
|
|
487
|
+
alive!
|
|
488
|
+
out = Binding.empty_buffer
|
|
489
|
+
Hide.check(Binding.call(:hide_secret_key_public, handle, out))
|
|
490
|
+
Binding.take(out)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Seals this key with a passphrase, for writing to disk. A forgotten
|
|
494
|
+
# passphrase cannot be recovered: there is no escrow.
|
|
495
|
+
def protect(passphrase)
|
|
496
|
+
alive!
|
|
497
|
+
text = passphrase.to_s
|
|
498
|
+
if text.length < MIN_PASSPHRASE_LEN
|
|
499
|
+
raise InvalidArgumentError,
|
|
500
|
+
"the passphrase must be at least #{MIN_PASSPHRASE_LEN} characters"
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
out = Binding.empty_buffer
|
|
504
|
+
Hide.check(Binding.call(:hide_secret_key_protect, handle, "#{text}\x00".b, out))
|
|
505
|
+
Binding.take(out)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def close
|
|
509
|
+
return if @address.nil? || @address.zero?
|
|
510
|
+
|
|
511
|
+
Binding.call(:hide_secret_key_free, Fiddle::Pointer.new(@address))
|
|
512
|
+
@address = nil
|
|
513
|
+
nil
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def closed?
|
|
517
|
+
@address.nil? || @address.zero?
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Never render key material, not even a fingerprint of it.
|
|
521
|
+
def inspect
|
|
522
|
+
"#<Hide::SecretKey #{closed? ? "closed" : "open"}>"
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
alias to_s inspect
|
|
526
|
+
|
|
527
|
+
# Internal: the raw handle, for Hide.decrypt.
|
|
528
|
+
def __handle
|
|
529
|
+
alive!
|
|
530
|
+
handle
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
private
|
|
534
|
+
|
|
535
|
+
def handle
|
|
536
|
+
Fiddle::Pointer.new(@address)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def alive!
|
|
540
|
+
raise ClosedKeyError, "this key has been closed" if closed?
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hide-protocol
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- HIDE contributors
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-09-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.0'
|
|
27
|
+
description: 'Ruby bindings to the HIDE core (X25519 + ML-KEM-768). Experimental and
|
|
28
|
+
unaudited: a successful decryption proves the data was not altered, not who created
|
|
29
|
+
it.'
|
|
30
|
+
email:
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- lib/hide_protocol.rb
|
|
38
|
+
- lib/hide_protocol/binding.rb
|
|
39
|
+
homepage: https://github.com/hide-protocol/hide
|
|
40
|
+
licenses:
|
|
41
|
+
- Apache-2.0
|
|
42
|
+
metadata:
|
|
43
|
+
homepage_uri: https://github.com/hide-protocol/hide
|
|
44
|
+
source_code_uri: https://github.com/hide-protocol/hide
|
|
45
|
+
bug_tracker_uri: https://github.com/hide-protocol/hide/issues
|
|
46
|
+
rubygems_mfa_required: 'true'
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '3.0'
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.5.22
|
|
63
|
+
signing_key:
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Experimental hybrid post-quantum file and message encryption. Unaudited.
|
|
66
|
+
test_files: []
|