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 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")