konfidant 0.7.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d76590436720d9998f06a04fffa4a5a06b3aff597c675e6b4a1eafe770cd383f
4
- data.tar.gz: 2e09ccdb885e047fdefe69faeba3289a919052534d07ef0bc5040a0e91256b14
3
+ metadata.gz: b9defdc63a2d22d69dabe29e0c3639d225348ba9aef1b975a8153e9b7e762179
4
+ data.tar.gz: 59d2d610234de9ccd59fd02bd3a06f3343375ae6ccc25baab71ad0aab4f98fc9
5
5
  SHA512:
6
- metadata.gz: 6e858d0172471e5e88261696bc2201307e582f4003eb588ee6308fb0c1e2d280c72e6673b5329c00b2efc64b204a4abb9adacd848700d6a2daaeb04fe491a5c1
7
- data.tar.gz: 1ecd149e3f5efed0fb906b75499789610b5305dbb816b95dbad72074a022937a1f42126556faedd248d9a96e71a5c8e40d87fbb06e4cb2f4f1736cf443b9a691
6
+ metadata.gz: 81a88c2361ec384f99e7282516a40dd8286fdceed5bce6834a2e219cd4fc5aa8fa5c8cc24b5f70cf794dd25b672a0e35f6fc84e6bdeffcfd59cc20d02cad116a
7
+ data.tar.gz: 3c4177715e1bcc654e8d90772b4d97abb501449b6f741077c2fc854ef21a927142f1044bbdee8826558f7e61a976a0e9b7c73594f3130b91ecb9fad04615711c
data/README.md CHANGED
@@ -6,29 +6,34 @@
6
6
 
7
7
  Official Ruby SDK for the [Konfidant](https://www.konfidant.app) API.
8
8
 
9
- Konfidant lets you share secrets — encrypted text and files — that self-destruct after being read.
9
+ Konfidant lets you share secrets — text and files — that self-destruct after being read.
10
+
11
+ ## Zero-knowledge model
12
+
13
+ All content is **encrypted on your machine** before anything is sent:
14
+
15
+ - Every share gets a fresh random 256-bit key. Text, file bytes, file name and MIME type are encrypted together
16
+ with AES-256-GCM in the chunked KNF1
17
+ format (stdlib `OpenSSL` only).
18
+ - Konfidant receives only ciphertext and its size. It never sees the key, the content or the file name.
19
+ - The key lives **only in the share link's URL fragment**: `https://download.konfidant.app/#t=<token>&k=<key>`.
20
+ Browsers never send the fragment to a server. The `t` token is single-use, so the ciphertext can be fetched once.
21
+ - Anyone holding the full `share_url` can read the secret once — treat it like the secret itself. If you lose it,
22
+ the content cannot be recovered by anyone, including Konfidant.
10
23
 
11
24
  ---
12
25
 
13
26
  ## Installation
14
27
 
15
- Add to your `Gemfile`:
16
-
17
28
  ```ruby
18
29
  gem 'konfidant'
19
30
  ```
20
31
 
21
- Then run:
22
-
23
32
  ```bash
24
- bundle install
33
+ bundle install # or: gem install konfidant
25
34
  ```
26
35
 
27
- Or install directly:
28
-
29
- ```bash
30
- gem install konfidant
31
- ```
36
+ Requires Ruby >= 3.2. No runtime dependencies (stdlib `net/http`, `openssl`, `json`).
32
37
 
33
38
  ---
34
39
 
@@ -37,307 +42,166 @@ gem install konfidant
37
42
  ```ruby
38
43
  require 'konfidant'
39
44
 
40
- client = Konfidant::Client.new(api_key: 'your-api-key')
41
-
42
- result = client.share_text(text: 'super-secret-password', ttl_hours: 24)
43
-
44
- puts "Share this link: #{result.share_url}"
45
- ```
46
-
47
- ---
48
-
49
- ## Authentication
45
+ client = Konfidant::Client.new(api_key: ENV['KONFIDANT_API_KEY'])
50
46
 
51
- All requests require a Bearer API key. Generate one from the [Konfidant dashboard](https://www.konfidant.app).
47
+ text = client.share_text(text: 'db-password: hunter2', ttl_hours: 24)
48
+ puts text.share_url
52
49
 
53
- ```ruby
54
- client = Konfidant::Client.new(api_key: ENV['KONFIDANT_API_KEY'])
50
+ file = File.open('contract.pdf', 'rb') do |f|
51
+ client.share_file(content: f, filename: 'contract.pdf', content_type: 'application/pdf', ttl_hours: 48)
52
+ end
53
+ puts file.share_url
55
54
  ```
56
55
 
57
56
  ---
58
57
 
59
- ## API Reference
58
+ ## API reference
60
59
 
61
- ### `Konfidant::Client.new`
60
+ ### `Konfidant::Client.new(api_key:, base_url: nil, http_timeout: 120)`
62
61
 
63
62
  | Option | Type | Required | Description |
64
63
  |----------------|-----------|----------|----------------------------------------------------------------------|
65
- | `api_key` | `String` | Yes | Your Konfidant API key |
66
- | `base_url` | `String` | No | Override the base URL (default: `https://www.konfidant.app`) |
64
+ | `api_key` | `String` | Yes | Your Konfidant API key (sent as `Authorization: Bearer …`) |
65
+ | `base_url` | `String` | No | Override the API base URL (default: `https://www.konfidant.app`) |
67
66
  | `http_timeout` | `Integer` | No | Per-request HTTP timeout in seconds (default: `120`; `nil` disables) |
68
67
 
69
68
  Raises `ArgumentError` if `api_key` is nil or empty.
70
69
 
71
- ---
72
-
73
- ### `client.share_text(text:, ttl_hours:)`
74
-
75
- Encrypt and share a text message.
76
-
77
- | Argument | Type | Description |
78
- |-------------|-----------|--------------------------|
79
- | `text` | `String` | The secret text to share |
80
- | `ttl_hours` | `Integer` | Time-to-live in hours |
81
-
82
- Returns a `Konfidant::ShareTextResponse`:
83
-
84
- | Field | Type | Description |
85
- |----------------|-----------|---------------------------------------------|
86
- | `text_id` | `String` | Unique ID of the shared text |
87
- | `share_url` | `String` | One-time download link to send to recipient |
88
- | `expires_at` | `String` | Expiry datetime |
89
- | `verified_burn`| `Boolean` | Whether burn-on-read is verified |
90
-
91
- #### Example: share text
92
-
93
- ```ruby
94
- result = client.share_text(text: 'db-password: hunter2', ttl_hours: 48)
95
-
96
- puts result.share_url # send to recipient
97
- puts result.expires_at
98
- puts result.verified_burn
99
- ```
100
-
101
- ---
102
-
103
- ### `client.share_file(filename:, file_size:, ttl_hours:)`
104
-
105
- Request a presigned upload URL for a file. Use the returned response with `upload_file` to complete
106
- the upload, then poll `get_file_status` for the share link.
70
+ ### `client.share_text(text:, ttl_hours: nil)` → `Konfidant::TextShare`
107
71
 
108
- > For a one-call convenience wrapper, see [`share_and_upload_file`](#clientshare_and_upload_file).
72
+ Encrypts `text` (UTF-8, no SDK-side size limit; your plan's limit applies server-side) and uploads the ciphertext.
73
+ `ttl_hours` is omitted from the request when `nil` (server default applies).
109
74
 
110
- | Argument | Type | Description |
111
- |-------------|-----------|----------------------------------|
112
- | `filename` | `String` | Original filename with extension |
113
- | `file_size` | `Integer` | File size in bytes |
114
- | `ttl_hours` | `Integer` | Time-to-live in hours |
75
+ | Field | Type | Description |
76
+ |--------------|---------------|------------------------------------------------|
77
+ | `share_url` | `String` | One-time link **including the key** (`&k=…`) |
78
+ | `text_id` | `String, nil` | Text ID (`nil` when verified burn is disabled) |
79
+ | `expires_at` | `String` | ISO 8601 expiry |
115
80
 
116
- Returns a `Konfidant::ShareFileResponse`:
81
+ ### `client.share_file(content:, filename:, content_type: '', ttl_hours: nil)` → `Konfidant::FileShare`
117
82
 
118
- | Field | Type | Description |
119
- |--------------------|-------------------------------|-------------------------------------------------|
120
- | `upload_url` | `String` | Short-lived presigned S3 PUT URL |
121
- | `file_key` | `String` | Use with `get_file_status` and `upload_file` |
122
- | `poll_url` | `String` | Convenience URL for status polling |
123
- | `metadata_headers` | `Konfidant::FileMetadataHeaders` | Required S3 headers — passed by `upload_file` |
83
+ Encrypts and shares a file in one call: encrypt → `create_file_upload` → `upload_ciphertext` → `complete_file_upload`.
124
84
 
125
- ---
126
-
127
- ### `client.upload_file(io:, size:, content_type:, presigned:)`
128
-
129
- Upload file bytes to the presigned S3 URL from `share_file`. Automatically attaches the required
130
- S3 metadata headers. Does **not** send the Konfidant `Authorization` header to S3.
85
+ | Argument | Type | Description |
86
+ |----------------|--------------|-------------------------------------------------------------------------------------|
87
+ | `content` | `String, IO` | File bytes, or a readable IO. `File`/`StringIO` are streamed chunk by chunk; other IOs are read into memory |
88
+ | `filename` | `String` | Original file name, at most 1 024 UTF-8 bytes (encrypted) |
89
+ | `content_type` | `String` | MIME type, at most 255 bytes, may be empty (encrypted) |
90
+ | `ttl_hours` | `Integer` | Time-to-live in hours (optional) |
131
91
 
132
- | Argument | Type | Description |
133
- |----------------|-----------------------------|---------------------------------------|
134
- | `io` | `IO` | Readable IO object (File, StringIO) |
135
- | `size` | `Integer` | Content-Length in bytes |
136
- | `content_type` | `String` | MIME type (e.g. `application/pdf`) |
137
- | `presigned` | `Konfidant::ShareFileResponse` | Full response from `share_file` |
92
+ | Field | Type | Description |
93
+ |-----------------|---------------|----------------------------------------------|
94
+ | `share_url` | `String` | One-time link **including the key** (`&k=…`) |
95
+ | `file_id` | `String, nil` | File ID (`nil` when verified burn is off) |
96
+ | `expires_at` | `String` | ISO 8601 expiry |
97
+ | `verified_burn` | `Boolean` | Whether verified burn is enabled |
138
98
 
139
- Returns `nil` on success. Raises `Konfidant::ApiError` on S3 error.
99
+ ### Low-level file flow
140
100
 
141
- #### Example: manual three-step flow
101
+ Use these when you need control over each step (e.g. your own retry logic).
142
102
 
143
103
  ```ruby
144
- file = File.open('report.pdf', 'rb')
145
- size = File.size('report.pdf')
146
-
147
- # Step 1 – get presigned URL
148
- presigned = client.share_file(
149
- filename: 'report.pdf',
150
- file_size: size,
151
- ttl_hours: 72
152
- )
153
-
154
- # Step 2 – upload to S3
155
- client.upload_file(
156
- io: file,
157
- size: size,
158
- content_type: 'application/pdf',
159
- presigned: presigned
160
- )
161
-
162
- # Step 3 – poll for share link
163
- loop do
164
- status = client.get_file_status(presigned.file_key)
165
- if status.status == 'complete'
166
- puts "Share URL: #{status.share_url}"
167
- break
168
- end
169
- sleep 2
170
- end
171
- ```
172
-
173
- ---
174
-
175
- ### `client.get_file_status(file_key)`
176
-
177
- Poll the encryption status of an uploaded file.
178
-
179
- | Argument | Type | Description |
180
- |------------|----------|-----------------------------------------------|
181
- | `file_key` | `String` | The `file_key` from the `share_file` response |
182
-
183
- Returns a `Konfidant::FileStatusResponse`:
184
-
185
- | Field | Type | When set |
186
- |----------------|-----------|-----------------------------------------|
187
- | `status` | `String` | Always (`"processing"` or `"complete"`) |
188
- | `message` | `String` | When `processing` |
189
- | `file_id` | `String` | When `complete` |
190
- | `file_name` | `String` | When `complete` |
191
- | `share_url` | `String` | When `complete` |
192
- | `expires_at` | `String` | When `complete` |
193
- | `verified_burn`| `Boolean` | When `complete` |
194
-
195
- ---
196
-
197
- ### `client.list_shares(type: nil, status: nil, limit: nil, offset: nil)`
198
-
199
- List all shares for the authenticated organization. All parameters are optional.
200
-
201
- | Argument | Type | Description |
202
- |----------|----------|--------------------------------|
203
- | `type` | `String` | `"file"` or `"text"` |
204
- | `status` | `String` | `"active"` or `"accessed"` |
205
- | `limit` | `Integer`| Page size (default 50) |
206
- | `offset` | `Integer`| Pagination offset |
104
+ key = Konfidant::Knf.generate_key
105
+ encryptor = Konfidant::Knf.encryptor(key: key, content: File.open('a.zip', 'rb'), kind: 'file',
106
+ name: 'a.zip', mime: 'application/zip')
207
107
 
208
- Returns a `Konfidant::ListSharesResponse`:
108
+ upload = client.create_file_upload(ciphertext_size: encryptor.size, ttl_hours: 24)
109
+ client.upload_ciphertext(upload: upload, ciphertext: encryptor) # String or IO-like
110
+ done = client.complete_file_upload(file_key: upload.file_key)
209
111
 
210
- | Field | Type | Description |
211
- |--------------|----------------------------|---------------------|
212
- | `shares` | `Array<Konfidant::Share>` | Share entries |
213
- | `pagination` | `Konfidant::Pagination` | Page metadata |
112
+ share_url = Konfidant::Knf.build_share_url(done.download_url, key)
113
+ ```
214
114
 
215
- Each `Konfidant::Share`:
115
+ | Method | Request | Returns |
116
+ |-----------------------------------------------------|--------------------------------------------------|-----------------------------------------------------------------------------------------|
117
+ | `create_file_upload(ciphertext_size:, ttl_hours:)` | `POST /api/v1/files` | `FileUpload(upload_url, file_key, upload_headers, upload_expires_in, ciphertext_size)` |
118
+ | `upload_ciphertext(upload:, ciphertext:)` | `PUT upload_url` with exactly `upload_headers` | `nil`; never sends the API key |
119
+ | `complete_file_upload(file_key:)` | `POST /api/v1/files/{file_key}/complete` | `CompletedUpload(download_url, file_id, expires_at, verified_burn)` |
216
120
 
217
- | Field | Type | Description |
218
- |------------------|---------------|-------------------------|
219
- | `type` | `String` | `"file"` or `"text"` |
220
- | `file_name` | `String` | Filename or text label |
221
- | `file_size_bytes`| `Integer` | Size in bytes |
222
- | `created_at` | `String` | Creation datetime |
223
- | `expires_at` | `String` | Expiry datetime |
224
- | `accessed_at` | `String, nil` | Access datetime, or nil |
225
- | `created_by` | `String` | Email of creator |
121
+ `complete_file_upload` raises `ApiError` with status `409` and message `upload_incomplete` if the PUT did not land.
122
+ `download_url` contains only the token — append the key with `Knf.build_share_url` before sending it to anyone.
226
123
 
227
- `Konfidant::Pagination`:
124
+ ### `client.open_share(share_url)` → `Konfidant::OpenedShare`
228
125
 
229
- | Field | Type | Description |
230
- |------------|-----------|--------------------------|
231
- | `total` | `Integer` | Total number of shares |
232
- | `limit` | `Integer` | Page size used |
233
- | `offset` | `Integer` | Offset used |
234
- | `has_more` | `Boolean` | Whether more pages exist |
126
+ Recipient side. Sends only the token (`POST https://<link host>/api/download`, no API key, no decryption key),
127
+ receives the ciphertext and decrypts it locally. Opening a share **consumes it**.
235
128
 
236
- #### Example: list shares
129
+ | Field | Type | Description |
130
+ |--------|---------------|---------------------------------------------|
131
+ | `kind` | `String` | `"text"` or `"file"` |
132
+ | `name` | `String` | File name (`""` for text) |
133
+ | `mime` | `String` | MIME type (`""` for text) |
134
+ | `data` | `String` | Decrypted bytes (binary / ASCII-8BIT) |
135
+ | `text` | `String, nil` | UTF-8 text for text shares, otherwise `nil` |
237
136
 
238
- ```ruby
239
- result = client.list_shares(type: 'file', limit: 10)
137
+ Raises `ApiError` (`410`) if the share was already opened or has expired, `KnfError` if the ciphertext does not
138
+ authenticate (wrong key, modified or truncated data).
240
139
 
241
- result.shares.each do |share|
242
- puts "#{share.file_name} — expires #{share.expires_at}"
243
- end
140
+ ### `client.list_shares(type: nil, status: nil, limit: nil, offset: nil)` → `Konfidant::ListSharesResponse`
244
141
 
245
- puts "Total: #{result.pagination.total}"
246
- puts "More? #{result.pagination.has_more}"
247
- ```
142
+ | Argument | Type | Description |
143
+ |----------|-----------|----------------------------|
144
+ | `type` | `String` | `"file"` or `"text"` |
145
+ | `status` | `String` | `"active"` or `"accessed"` |
146
+ | `limit` | `Integer` | Page size (default 50) |
147
+ | `offset` | `Integer` | Pagination offset |
248
148
 
249
- ---
149
+ Returns `shares` (`Array<Konfidant::Share>` with `type`, `file_size_bytes`, `created_at`, `expires_at`,
150
+ `accessed_at`, `created_by`) and `pagination` (`total`, `limit`, `offset`, `has_more`). File names are not
151
+ available: the server never sees them.
250
152
 
251
- ### `client.share_and_upload_file`
252
-
253
- Convenience wrapper that calls `share_file` → `upload_file` → polls `get_file_status` until complete.
254
-
255
- ```ruby
256
- client.share_and_upload_file(
257
- io: io,
258
- size: size,
259
- filename: filename,
260
- content_type: content_type,
261
- ttl_hours: ttl_hours,
262
- poll_interval: 2, # seconds between status checks (default: 2)
263
- timeout: 60 # max wait for encryption in seconds (default: 60)
264
- )
265
- ```
153
+ ### `Konfidant::Knf`
266
154
 
267
- | Argument | Type | Default | Description |
268
- |-----------------|-----------|---------|-------------------------------------|
269
- | `io` | `IO` | — | Readable IO object (File, StringIO) |
270
- | `size` | `Integer` | — | File size in bytes |
271
- | `filename` | `String` | — | Filename with extension |
272
- | `content_type` | `String` | — | MIME type |
273
- | `ttl_hours` | `Integer` | — | Time-to-live in hours |
274
- | `poll_interval` | `Numeric` | `2` | Seconds between status checks |
275
- | `timeout` | `Numeric` | `60` | Max seconds to wait for encryption |
155
+ The KNF1 implementation is public for advanced use and interoperability:
276
156
 
277
- Returns a `Konfidant::ShareResult`:
157
+ | Method | Description |
158
+ |------------------------------------------------------------------------|-----------------------------------------------------|
159
+ | `generate_key` / `encode_key(key)` / `decode_key(str)` | 32-byte key; unpadded base64url (43 chars) |
160
+ | `encrypt(key:, content:, kind:, name: '', mime: '')` | Full ciphertext as a binary String |
161
+ | `encryptor(...)` | Streaming IO-like encryptor (`#read`, `#size`) |
162
+ | `encrypt_text(key:, text:)` | Text share ciphertext |
163
+ | `decrypt(key:, ciphertext:)` / `Decryptor.new(key:)#push`/`#finish` | Verify and decrypt (one-shot or streaming) |
164
+ | `ciphertext_size(metadata_length:, content_length:)` | Exact ciphertext length |
165
+ | `build_share_url(download_url, key)` / `parse_share_url(url)` | Share link helpers |
278
166
 
279
- | Field | Type | Description |
280
- |----------------|-----------|--------------------------------|
281
- | `share_url` | `String` | One-time download link |
282
- | `file_id` | `String` | Unique file ID |
283
- | `expires_at` | `String` | Expiry datetime |
284
- | `verified_burn`| `Boolean` | Whether burn-on-read is active |
285
-
286
- Raises `RuntimeError` with `"konfidant: encryption timed out after Ns"` if encryption does not complete
287
- within `timeout` seconds.
288
-
289
- #### Example: share and upload file
290
-
291
- ```ruby
292
- file = File.open('confidential.zip', 'rb')
293
- size = File.size('confidential.zip')
294
-
295
- result = client.share_and_upload_file(
296
- io: file,
297
- size: size,
298
- filename: 'confidential.zip',
299
- content_type: 'application/zip',
300
- ttl_hours: 48
301
- )
302
-
303
- puts "Ready to share: #{result.share_url}"
304
- ```
167
+ `encrypt` also accepts `chunk_size:` and a `nonce_prefix:` option that exists **only** for reproducing test
168
+ vectors — never pass `nonce_prefix` in production.
305
169
 
306
170
  ---
307
171
 
308
172
  ## Error handling
309
173
 
310
- All API and S3 errors raise `Konfidant::ApiError`.
174
+ All SDK errors inherit from `Konfidant::Error`.
311
175
 
312
176
  ```ruby
313
177
  begin
314
178
  client.share_text(text: 'secret', ttl_hours: 1)
315
179
  rescue Konfidant::ApiError => e
316
- puts e.message # e.g. "Missing or invalid Authorization header."
317
- puts e.status_code # e.g. 401
318
- puts e.body.inspect # parsed response body (Hash or String)
180
+ e.message # the "error" field of the JSON body, e.g. "upload_incomplete", or "HTTP 500"
181
+ e.status_code # e.g. 401
182
+ e.body # parsed body (Hash) or raw String
183
+ rescue Konfidant::KnfError => e
184
+ e.message # encryption/decryption failure
319
185
  end
320
186
  ```
321
187
 
322
- ### Common error codes
323
-
324
- | Status | Meaning |
325
- |--------|----------------------------|
326
- | `400` | Bad request / invalid body |
327
- | `401` | Missing or invalid API key |
328
- | `403` | Insufficient API key scope |
329
- | `404` | Resource not found |
188
+ | Status | Meaning |
189
+ |--------|-------------------------------------------|
190
+ | `400` | Bad request / invalid body |
191
+ | `401` | Missing or invalid API key |
192
+ | `403` | Insufficient API key scope |
193
+ | `404` | Resource not found |
194
+ | `409` | `upload_incomplete` on complete |
195
+ | `410` | Share already opened or expired |
330
196
 
331
197
  ---
332
198
 
333
199
  ## Development
334
200
 
335
201
  ```bash
336
- bundle install # install dependencies
337
- bundle exec rspec # run tests
202
+ bundle install
203
+ bundle exec rspec
338
204
  ```
339
205
 
340
- ### Requirements
341
-
342
- - Ruby >= 3.2.0
343
- - No runtime dependencies (uses stdlib `net/http`, `uri`, `json`)
206
+ `spec/fixtures/knf1-test-vectors.json` holds the cross-SDK KNF1 test vectors; the specs reproduce every ciphertext
207
+ byte for byte.