phylax 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/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +155 -0
- data/ext/phylax/extconf.rb +20 -0
- data/ext/phylax/phylax.c +1024 -0
- data/lib/phylax/version.rb +5 -0
- data/lib/phylax.rb +252 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e381c418f0f615ce325714db60e7859fb75be0584ed3cef79cdb39cf0a3ab5ce
|
|
4
|
+
data.tar.gz: 552fd46214a14a2faaa3038ec6f4bbd5a677367fb5e9740d894e8bf3605981bd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4c77e2bf51d2a617e664cfc40c40fd91e79700c13d4d1a6ebc954909a24251b8dcc3cb48d84d8d9e5ead11964e3a18e60eaf48ef3ae49d92ac439e6044fe4fa3
|
|
7
|
+
data.tar.gz: 4901fb8c6cee2d011097a46f500009882475220ce30980676bd6dcbbeeb788be1fc0e948c61d9160ef6cd6971b1549e168bebeb83f44fe38295219ba5d1ec018
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-06-01
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `Phylax::SecretBox` — authenticated AES-256-GCM encryption with automatic,
|
|
8
|
+
per-message random nonces framed as `nonce ‖ ciphertext ‖ tag`; nonce reuse is
|
|
9
|
+
impossible through the public API. `seal`/`open` with optional AAD,
|
|
10
|
+
`generate_key`, and `from_password` (PBKDF2-HMAC-SHA256).
|
|
11
|
+
- `Phylax.sha256/sha384/sha512` and streaming `Phylax::Digest` (non-destructive
|
|
12
|
+
`#digest` via `BCryptDuplicateHash`).
|
|
13
|
+
- `Phylax.hmac_sha256/sha384/sha512`, streaming `Phylax::HMAC`, and the
|
|
14
|
+
constant-time `Phylax::HMAC.verify`.
|
|
15
|
+
- `Phylax.pbkdf2` (HMAC-SHA-2 PRF; releases the GVL during derivation).
|
|
16
|
+
- `Phylax.random_bytes` (system-preferred CSPRNG).
|
|
17
|
+
- `Phylax.secure_compare` (constant-time for equal-length inputs).
|
|
18
|
+
- `Phylax.protect` / `Phylax.unprotect` — DPAPI secrets at rest, user or machine
|
|
19
|
+
scope, optional entropy.
|
|
20
|
+
- Error taxonomy: `Phylax::Error`, `Phylax::AuthenticationError`, `Phylax::OSError`.
|
|
21
|
+
|
|
22
|
+
Built on the Windows CNG provider (`bcrypt.dll`) and DPAPI (`crypt32.dll`); no
|
|
23
|
+
cryptography is implemented in the gem itself. Windows MSVC (mswin) Ruby only.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ned
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# phylax
|
|
2
|
+
|
|
3
|
+
**Misuse-resistant cryptography for Ruby, backed by the Windows CNG provider.**
|
|
4
|
+
|
|
5
|
+
`phylax` (Greek *φύλαξ*, "guardian") is a thin, **safe-by-default** binding to the
|
|
6
|
+
cryptography Windows already ships and validates: the **Cryptography API: Next
|
|
7
|
+
Generation** (CNG / `bcrypt.dll`) and **DPAPI** (`crypt32.dll`). It implements no
|
|
8
|
+
cryptography of its own — it exposes the operating system's own primitives through
|
|
9
|
+
an ergonomic, hard-to-misuse API.
|
|
10
|
+
|
|
11
|
+
The design goal is that the *easy* way is the *safe* way:
|
|
12
|
+
|
|
13
|
+
- **Authenticated encryption that can't be nonce-reused.** `SecretBox` (AES-256-GCM)
|
|
14
|
+
generates a fresh 96-bit nonce on every `seal` and frames it into the output, so
|
|
15
|
+
the catastrophic GCM failure mode — reusing a `(key, nonce)` pair — is impossible
|
|
16
|
+
through the public API. There is no nonce parameter to get wrong.
|
|
17
|
+
- **Decryption is all-or-nothing.** `open` verifies the authentication tag inside the
|
|
18
|
+
OS call and raises `Phylax::AuthenticationError` rather than ever returning
|
|
19
|
+
unauthenticated plaintext.
|
|
20
|
+
- **No silent weakening.** A key that isn't exactly 32 bytes is a loud error, not a
|
|
21
|
+
silently-truncated AES-128 key. Only SHA-2 is accepted — `:md5`/`:sha1` raise.
|
|
22
|
+
- **Constant-time comparison** is provided so you don't verify MACs with `==`.
|
|
23
|
+
|
|
24
|
+
| What | API |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Secure random | `Phylax.random_bytes(n)` |
|
|
27
|
+
| Hashing (one-shot) | `Phylax.sha256(data)`, `.sha384`, `.sha512` |
|
|
28
|
+
| Hashing (streaming) | `Phylax::Digest.new(:sha256)` |
|
|
29
|
+
| HMAC (one-shot) | `Phylax.hmac_sha256(key, data)`, `.hmac_sha384`, `.hmac_sha512` |
|
|
30
|
+
| HMAC (streaming) | `Phylax::HMAC.new(:sha256, key)`, `Phylax::HMAC.verify(...)` |
|
|
31
|
+
| Key derivation | `Phylax.pbkdf2(password:, salt:, iterations:, length:, hash:)` |
|
|
32
|
+
| Authenticated encryption | `Phylax::SecretBox` (AES-256-GCM) |
|
|
33
|
+
| Constant-time compare | `Phylax.secure_compare(a, b)` |
|
|
34
|
+
| Secrets at rest | `Phylax.protect(data, scope:, entropy:)` / `Phylax.unprotect` |
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- **Windows** with a native **MSVC (mswin)** Ruby. Not supported on MinGW/UCRT.
|
|
39
|
+
- Visual Studio 2017+ / Build Tools with the **Desktop development with C++** workload.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
gem install phylax
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Authenticated encryption — `SecretBox`
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require "phylax"
|
|
51
|
+
|
|
52
|
+
key = Phylax::SecretBox.generate_key # 32 cryptographically random bytes
|
|
53
|
+
box = Phylax::SecretBox.new(key)
|
|
54
|
+
|
|
55
|
+
sealed = box.seal("attack at dawn") # => nonce ‖ ciphertext ‖ tag (binary)
|
|
56
|
+
box.open(sealed) # => "attack at dawn"
|
|
57
|
+
|
|
58
|
+
# Additional authenticated data (authenticated, not encrypted) — must match.
|
|
59
|
+
sealed = box.seal(payload, aad: "v1:user-42")
|
|
60
|
+
box.open(sealed, aad: "v1:user-42") # ok
|
|
61
|
+
box.open(sealed, aad: "v1:user-99") # raises Phylax::AuthenticationError
|
|
62
|
+
|
|
63
|
+
# Any tampering with the nonce, ciphertext, tag, or aad fails closed:
|
|
64
|
+
box.open(sealed.tap { |s| s.setbyte(20, s.getbyte(20) ^ 1) })
|
|
65
|
+
# => raises Phylax::AuthenticationError (no plaintext is returned)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Derive the key from a password instead (PBKDF2-HMAC-SHA256):
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
salt = Phylax.random_bytes(16) # store this alongside the ciphertext
|
|
72
|
+
box = Phylax::SecretBox.from_password("correct horse battery staple",
|
|
73
|
+
salt: salt, iterations: 600_000)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Wire format.** `seal` returns `nonce (12 bytes) ‖ ciphertext (= plaintext length) ‖ tag (16 bytes)`,
|
|
77
|
+
so the overhead is a constant `Phylax::SecretBox::OVERHEAD` (28) bytes. This is
|
|
78
|
+
standard AES-256-GCM: a blob from `seal` can be decrypted by any GCM
|
|
79
|
+
implementation (e.g. OpenSSL) given the same key, and vice versa.
|
|
80
|
+
|
|
81
|
+
## Hashing and HMAC
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Phylax.sha256("data") # => 32 raw bytes (ASCII-8BIT)
|
|
85
|
+
Phylax.sha256("data").unpack1("H*") # => hex
|
|
86
|
+
|
|
87
|
+
# Streaming — non-destructive #digest, so you can keep updating:
|
|
88
|
+
d = Phylax::Digest.new(:sha256)
|
|
89
|
+
d << "chunk one" << "chunk two"
|
|
90
|
+
d.hexdigest
|
|
91
|
+
|
|
92
|
+
Phylax.hmac_sha256(key, "message") # keyed MAC, raw bytes
|
|
93
|
+
|
|
94
|
+
# Verify a MAC the safe way (constant-time, never raises on mismatch):
|
|
95
|
+
Phylax::HMAC.verify(:sha256, key, message, received_tag) # => true / false
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Key derivation, random, and comparison
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
Phylax.pbkdf2(password: pw, salt: salt, iterations: 600_000, length: 32)
|
|
102
|
+
Phylax.random_bytes(16) # system CSPRNG (CTR_DRBG)
|
|
103
|
+
Phylax.secure_compare(tag_a, tag_b) # constant-time for equal-length inputs
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Secrets at rest — DPAPI
|
|
107
|
+
|
|
108
|
+
`protect` encrypts data with a key the OS derives from the current user (default)
|
|
109
|
+
or the machine, so the blob can only be read back on the same machine (and, for
|
|
110
|
+
`:user` scope, by the same user). Great for storing a credential or a `SecretBox`
|
|
111
|
+
key on disk without a master password.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
blob = Phylax.protect("api-token-value") # bound to this Windows user
|
|
115
|
+
Phylax.unprotect(blob) # => "api-token-value"
|
|
116
|
+
|
|
117
|
+
# Optional second secret ("entropy") that must be supplied to unprotect:
|
|
118
|
+
blob = Phylax.protect(secret, entropy: app_salt)
|
|
119
|
+
Phylax.unprotect(blob, entropy: app_salt)
|
|
120
|
+
|
|
121
|
+
# Machine scope: any account on this machine can unprotect.
|
|
122
|
+
Phylax.protect(secret, scope: :machine)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Errors
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
StandardError
|
|
129
|
+
└─ Phylax::Error # base for everything phylax raises
|
|
130
|
+
├─ Phylax::AuthenticationError # GCM tag mismatch / tamper, or DPAPI integrity failure
|
|
131
|
+
└─ Phylax::OSError # other Windows API failure (#api, #code)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`ArgumentError`/`TypeError` are raised for bad sizes or types *before* any OS call
|
|
135
|
+
(e.g. a key that isn't 32 bytes, an unknown hash, a non-String input).
|
|
136
|
+
|
|
137
|
+
## Security notes
|
|
138
|
+
|
|
139
|
+
- **Algorithms.** AES-256-GCM (128-bit tag), SHA-256/384/512, HMAC-SHA-2,
|
|
140
|
+
PBKDF2-HMAC-SHA-2, `BCryptGenRandom` (SP 800-90A CTR_DRBG). All are the OS's
|
|
141
|
+
validated implementations; phylax only marshals bytes.
|
|
142
|
+
- **Random-nonce limit.** With random 96-bit nonces, keep a single `SecretBox` key
|
|
143
|
+
under ~2³² messages (the GCM birthday bound). Rotate keys for very high volume.
|
|
144
|
+
- **`secure_compare` leaks length.** It returns `false` immediately when lengths
|
|
145
|
+
differ and is constant-time only across equal-length inputs — fine for comparing
|
|
146
|
+
fixed-length MACs, which is its purpose.
|
|
147
|
+
- **DPAPI tamper detection is best-effort.** Windows may return an error *or*, rarely,
|
|
148
|
+
succeed with corrupted output on a mangled blob. If you need a hard integrity
|
|
149
|
+
guarantee, wrap the data in a `SecretBox` (or add your own HMAC) first.
|
|
150
|
+
- **Windows/MSVC only.** The extension links `bcrypt.lib` + `crypt32.lib` and is
|
|
151
|
+
built with `cl.exe` against a native-MSVC Ruby.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
[MIT](LICENSE.txt).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# extconf.rb for the phylax extension — ergonomic Ruby bindings to the Windows
|
|
4
|
+
# Cryptography API: Next Generation (CNG / bcrypt.dll) plus DPAPI (crypt32.dll).
|
|
5
|
+
|
|
6
|
+
require "mkmf"
|
|
7
|
+
|
|
8
|
+
unless RbConfig::CONFIG["target_os"] =~ /mswin/
|
|
9
|
+
abort <<~MSG
|
|
10
|
+
phylax requires a native Windows MSVC (mswin) Ruby — it binds the Windows
|
|
11
|
+
CNG primitive provider (bcrypt.dll) and DPAPI (crypt32.dll) and is built
|
|
12
|
+
with cl.exe. Your Ruby is "#{RbConfig::CONFIG['arch']}".
|
|
13
|
+
MSG
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# CNG primitives (BCrypt*) and DPAPI (CryptProtectData/CryptUnprotectData).
|
|
17
|
+
# Pure C — no -EHsc, so rb_raise/longjmp is the normal, safe error mechanism.
|
|
18
|
+
$libs = [$libs, "bcrypt.lib", "crypt32.lib"].join(" ")
|
|
19
|
+
|
|
20
|
+
create_makefile("phylax/phylax")
|