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.
@@ -0,0 +1,469 @@
1
+ require 'openssl'
2
+ require 'securerandom'
3
+ require 'stringio'
4
+ require 'uri'
5
+
6
+ module Konfidant
7
+ # KNF1 — Konfidant client-side encryption format.
8
+ #
9
+ # Content is encrypted on the sender's machine with a random 256-bit key that travels only in the share link's
10
+ # URL fragment, so Konfidant's servers store and deliver ciphertext they cannot decrypt.
11
+ #
12
+ # ciphertext = header (16 bytes) || sealed_chunk_0 || ... || sealed_chunk_n
13
+ # stream = uint32_be(len(meta)) || meta || content
14
+ # meta = kind (1) || uint16_be(len(name)) || name || uint16_be(len(mime)) || mime
15
+ # nonce_i = nonce_prefix (7) || uint32_be(i) || last_flag (1)
16
+ #
17
+ # Every chunk is sealed with AES-256-GCM (16-byte tag) and authenticates the header as AAD.
18
+ # All byte strings produced and consumed here are binary (ASCII-8BIT).
19
+ module Knf
20
+ MAGIC = 'KNF1'.b.freeze
21
+ HEADER_SIZE = 16
22
+ TAG_SIZE = 16
23
+ KEY_SIZE = 32
24
+ NONCE_PREFIX_SIZE = 7
25
+ DEFAULT_CHUNK_SIZE = 1024 * 1024
26
+ MIN_CHUNK_SIZE = 4096
27
+ MAX_CHUNK_SIZE = 16 * 1024 * 1024
28
+ MAX_NAME_BYTES = 1024
29
+ MAX_MIME_BYTES = 255
30
+ MAX_CHUNK_INDEX = 0xFFFFFFFF
31
+
32
+ KIND_TEXT = 1
33
+ KIND_FILE = 2
34
+ KINDS = { 'text' => KIND_TEXT, 'file' => KIND_FILE }.freeze
35
+ META_FIXED_SIZE = 5 # kind (1) + name length (2) + mime length (2)
36
+ META_LENGTH_PREFIX = 4
37
+ CIPHER = 'aes-256-gcm'.freeze
38
+ KEY_PATTERN = /\A[A-Za-z0-9_-]{43}\z/
39
+
40
+ # Result of a successful decryption. +data+ is a binary String; +kind+ is "text" or "file".
41
+ Decrypted = Data.define(:kind, :name, :mime, :data) do
42
+ # Content of a text share as a UTF-8 String.
43
+ def text
44
+ raise KnfError, 'Share is not a text share' unless kind == 'text'
45
+
46
+ Knf.send(:utf8_string, data, 'Text')
47
+ end
48
+ end
49
+
50
+ module_function
51
+
52
+ # 32 random bytes from the OS CSPRNG. Use a fresh key for every share.
53
+ def generate_key
54
+ SecureRandom.random_bytes(KEY_SIZE)
55
+ end
56
+
57
+ # Unpadded base64url (43 characters), as carried in the share link's +k+ fragment parameter.
58
+ def encode_key(key)
59
+ check_key!(key)
60
+ [key].pack('m0').tr('+/', '-_').delete('=')
61
+ end
62
+
63
+ def decode_key(encoded)
64
+ raise KnfError, 'Invalid key encoding' unless encoded.is_a?(String) && KEY_PATTERN.match?(encoded)
65
+
66
+ key = "#{encoded.tr('-_', '+/')}=".unpack1('m0')
67
+ raise KnfError, 'Invalid key length' unless key.bytesize == KEY_SIZE
68
+
69
+ key
70
+ rescue ArgumentError
71
+ raise KnfError, 'Invalid key encoding'
72
+ end
73
+
74
+ # Encodes the metadata block. +kind+ is "text" or "file"; texts carry no name or MIME type.
75
+ def encode_metadata(kind:, name: '', mime: '')
76
+ kind_byte = KINDS.fetch(kind.to_s) { raise KnfError, "Unknown content kind: #{kind.inspect}" }
77
+ name_bytes = utf8_bytes(name, 'File name')
78
+ mime_bytes = utf8_bytes(mime, 'MIME type')
79
+ if kind_byte == KIND_TEXT && !(name_bytes.empty? && mime_bytes.empty?)
80
+ raise KnfError, 'Text shares must not carry a name or MIME type'
81
+ end
82
+ raise KnfError, "File name exceeds #{MAX_NAME_BYTES} bytes" if name_bytes.bytesize > MAX_NAME_BYTES
83
+ raise KnfError, "MIME type exceeds #{MAX_MIME_BYTES} bytes" if mime_bytes.bytesize > MAX_MIME_BYTES
84
+
85
+ [kind_byte, name_bytes.bytesize].pack('Cn') + name_bytes + [mime_bytes.bytesize].pack('n') + mime_bytes
86
+ end
87
+
88
+ # Exact ciphertext size for a metadata block of +metadata_length+ bytes and +content_length+ content bytes.
89
+ def ciphertext_size(metadata_length:, content_length:, chunk_size: DEFAULT_CHUNK_SIZE)
90
+ stream_length = META_LENGTH_PREFIX + metadata_length + content_length
91
+ HEADER_SIZE + stream_length + (TAG_SIZE * ((stream_length + chunk_size - 1) / chunk_size))
92
+ end
93
+
94
+ # Largest ciphertext a file share of at most +max_content_bytes+ can produce (longest name and MIME type).
95
+ def max_file_ciphertext_size(max_content_bytes)
96
+ ciphertext_size(metadata_length: META_FIXED_SIZE + MAX_NAME_BYTES + MAX_MIME_BYTES,
97
+ content_length: max_content_bytes)
98
+ end
99
+
100
+ # Largest ciphertext a text share of at most +max_text_bytes+ UTF-8 bytes can produce.
101
+ def max_text_ciphertext_size(max_text_bytes)
102
+ ciphertext_size(metadata_length: META_FIXED_SIZE, content_length: max_text_bytes)
103
+ end
104
+
105
+ # Returns a streaming, IO-like Encryptor (responds to #read, #readpartial and #size) that yields the KNF1
106
+ # ciphertext chunk by chunk, so large inputs never have to be held in memory.
107
+ #
108
+ # +content+ is a String (any encoding; its bytes are used) or a readable IO. IOs that respond to #size and #pos
109
+ # (File, Tempfile, StringIO) are streamed from their current position; other IOs are read into memory first.
110
+ #
111
+ # +nonce_prefix+ exists for test vectors only. Never pass it in production.
112
+ def encryptor(key:, content:, kind:, name: '', mime: '', chunk_size: DEFAULT_CHUNK_SIZE, nonce_prefix: nil)
113
+ Encryptor.new(key: key, content: content, metadata: encode_metadata(kind: kind, name: name, mime: mime),
114
+ chunk_size: chunk_size, nonce_prefix: nonce_prefix)
115
+ end
116
+
117
+ # Encrypts +content+ and returns the complete KNF1 ciphertext as a binary String.
118
+ def encrypt(key:, content:, kind:, name: '', mime: '', chunk_size: DEFAULT_CHUNK_SIZE, nonce_prefix: nil)
119
+ encryptor(key: key, content: content, kind: kind, name: name, mime: mime,
120
+ chunk_size: chunk_size, nonce_prefix: nonce_prefix).read
121
+ end
122
+
123
+ def encrypt_text(key:, text:, chunk_size: DEFAULT_CHUNK_SIZE, nonce_prefix: nil)
124
+ encrypt(key: key, content: utf8_bytes(text, 'Text'), kind: 'text',
125
+ chunk_size: chunk_size, nonce_prefix: nonce_prefix)
126
+ end
127
+
128
+ # Decrypts a complete KNF1 ciphertext. Raises KnfError on any format or authentication failure.
129
+ def decrypt(key:, ciphertext:)
130
+ decryptor = Decryptor.new(key: key)
131
+ decryptor.push(ciphertext)
132
+ decryptor.finish
133
+ end
134
+
135
+ # True when +bytes+ starts with a well-formed KNF1 header followed by at least one tag's worth of data.
136
+ def valid_header?(bytes)
137
+ bytes = bytes.b
138
+ return false if bytes.bytesize < HEADER_SIZE + TAG_SIZE
139
+
140
+ parse_header(bytes.byteslice(0, HEADER_SIZE))
141
+ true
142
+ rescue KnfError
143
+ false
144
+ end
145
+
146
+ # Builds the share link from the server-issued download URL (already carrying "#t=<token>") and the key.
147
+ def build_share_url(download_url, key)
148
+ "#{download_url}&k=#{encode_key(key)}"
149
+ end
150
+
151
+ # Parses "#t=<token>&k=<key>" from a share link. Returns [origin, token, key] or raises ArgumentError.
152
+ def parse_share_url(share_url)
153
+ uri = URI.parse(share_url.to_s)
154
+ raise ArgumentError, 'Share URL must be an absolute http(s) URL' unless uri.is_a?(URI::HTTP) && uri.host
155
+
156
+ params = URI.decode_www_form(uri.fragment.to_s).to_h
157
+ token = params['t']
158
+ encoded_key = params['k']
159
+ raise ArgumentError, 'Share URL is missing the token (t) or key (k)' if blank?(token) || blank?(encoded_key)
160
+
161
+ origin = "#{uri.scheme}://#{uri.host}"
162
+ origin += ":#{uri.port}" unless uri.port == uri.default_port
163
+ [origin, token, decode_key(encoded_key)]
164
+ rescue URI::InvalidURIError
165
+ raise ArgumentError, 'Invalid share URL'
166
+ end
167
+
168
+ # --- internal helpers --------------------------------------------------------------------------------------
169
+
170
+ def check_key!(key)
171
+ raise KnfError, 'Key must be 32 bytes' unless key.is_a?(String) && key.bytesize == KEY_SIZE
172
+ end
173
+
174
+ def check_chunk_size!(chunk_size)
175
+ return if chunk_size.is_a?(Integer) && chunk_size.between?(MIN_CHUNK_SIZE, MAX_CHUNK_SIZE)
176
+
177
+ raise KnfError, 'Invalid chunk size'
178
+ end
179
+
180
+ def build_header(chunk_size, nonce_prefix)
181
+ MAGIC + [chunk_size].pack('N') + nonce_prefix + "\x00".b
182
+ end
183
+
184
+ # Validates a 16-byte header and returns [chunk_size, nonce_prefix].
185
+ def parse_header(header)
186
+ raise KnfError, 'Not a KNF1 payload' unless header.byteslice(0, 4) == MAGIC && header.getbyte(15).zero?
187
+
188
+ chunk_size = header.byteslice(4, 4).unpack1('N')
189
+ check_chunk_size!(chunk_size)
190
+ [chunk_size, header.byteslice(8, NONCE_PREFIX_SIZE)]
191
+ end
192
+
193
+ def chunk_nonce(nonce_prefix, index, last)
194
+ raise KnfError, 'Too many chunks' if index > MAX_CHUNK_INDEX
195
+
196
+ nonce_prefix + [index, last ? 1 : 0].pack('NC')
197
+ end
198
+
199
+ def seal(key, nonce, header, plaintext)
200
+ cipher = OpenSSL::Cipher.new(CIPHER).encrypt
201
+ cipher.key = key
202
+ cipher.iv = nonce
203
+ cipher.auth_data = header
204
+ sealed = cipher.update(plaintext) + cipher.final
205
+ sealed << cipher.auth_tag(TAG_SIZE)
206
+ sealed.b
207
+ end
208
+
209
+ def open_sealed(key, nonce, header, sealed)
210
+ cipher = OpenSSL::Cipher.new(CIPHER).decrypt
211
+ cipher.key = key
212
+ cipher.iv = nonce
213
+ cipher.auth_tag = sealed.byteslice(-TAG_SIZE, TAG_SIZE)
214
+ cipher.auth_data = header
215
+ (cipher.update(sealed.byteslice(0, sealed.bytesize - TAG_SIZE)) + cipher.final).b
216
+ rescue OpenSSL::Cipher::CipherError
217
+ raise KnfError, 'Decryption failed: wrong key or corrupted or truncated ciphertext'
218
+ end
219
+
220
+ def decode_stream(stream)
221
+ raise KnfError, 'Metadata truncated' if stream.bytesize < META_LENGTH_PREFIX
222
+
223
+ meta_length = stream.byteslice(0, META_LENGTH_PREFIX).unpack1('N')
224
+ raise KnfError, 'Metadata truncated' if META_LENGTH_PREFIX + meta_length > stream.bytesize
225
+
226
+ meta = stream.byteslice(META_LENGTH_PREFIX, meta_length)
227
+ kind, name, mime = decode_metadata(meta)
228
+ data = stream.byteslice((META_LENGTH_PREFIX + meta_length)..) || ''.b
229
+ Decrypted.new(kind: kind, name: name, mime: mime, data: data.b)
230
+ end
231
+
232
+ def decode_metadata(meta)
233
+ raise KnfError, 'Metadata truncated' if meta.bytesize < META_FIXED_SIZE
234
+
235
+ kind = KINDS.key(meta.getbyte(0))
236
+ raise KnfError, 'Unknown content kind' unless kind
237
+
238
+ name_length = meta.byteslice(1, 2).unpack1('n')
239
+ raise KnfError, 'Metadata truncated' if 3 + name_length + 2 > meta.bytesize
240
+
241
+ mime_length = meta.byteslice(3 + name_length, 2).unpack1('n')
242
+ raise KnfError, 'Metadata length mismatch' unless META_FIXED_SIZE + name_length + mime_length == meta.bytesize
243
+
244
+ [kind,
245
+ utf8_string(meta.byteslice(3, name_length), 'File name'),
246
+ utf8_string(meta.byteslice(5 + name_length, mime_length), 'MIME type')]
247
+ end
248
+
249
+ # Converts +value+ to its UTF-8 bytes (binary String). Binary strings are taken as UTF-8 bytes.
250
+ def utf8_bytes(value, label)
251
+ str = value.to_s
252
+ str = str.encoding == Encoding::BINARY ? str.dup.force_encoding(Encoding::UTF_8) : str.encode(Encoding::UTF_8)
253
+ raise KnfError, "#{label} must be valid UTF-8" unless str.valid_encoding?
254
+
255
+ str.b
256
+ rescue EncodingError
257
+ raise KnfError, "#{label} must be valid UTF-8"
258
+ end
259
+
260
+ def utf8_string(bytes, label)
261
+ str = bytes.dup.force_encoding(Encoding::UTF_8)
262
+ raise KnfError, "#{label} is not valid UTF-8" unless str.valid_encoding?
263
+
264
+ str
265
+ end
266
+
267
+ def blank?(value)
268
+ value.nil? || value.empty?
269
+ end
270
+
271
+ private_class_method :check_key!, :check_chunk_size!, :build_header, :parse_header, :chunk_nonce, :seal,
272
+ :open_sealed, :decode_stream, :decode_metadata, :utf8_bytes, :utf8_string, :blank?
273
+
274
+ # Streaming KNF1 encryptor with an IO-like read interface. #size is the exact ciphertext length, known up front
275
+ # so it can be declared to the server (ciphertext_size) and sent as Content-Length.
276
+ class Encryptor
277
+ attr_reader :size
278
+
279
+ def initialize(key:, content:, metadata:, chunk_size: DEFAULT_CHUNK_SIZE, nonce_prefix: nil)
280
+ Knf.send(:check_key!, key)
281
+ Knf.send(:check_chunk_size!, chunk_size)
282
+ nonce_prefix = nonce_prefix ? nonce_prefix.b : SecureRandom.random_bytes(NONCE_PREFIX_SIZE)
283
+ raise KnfError, 'Nonce prefix must be 7 bytes' unless nonce_prefix.bytesize == NONCE_PREFIX_SIZE
284
+
285
+ @key = key.b
286
+ @chunk_size = chunk_size
287
+ @nonce_prefix = nonce_prefix
288
+ @source, content_length = normalize_source(content)
289
+ @prefix = [metadata.bytesize].pack('N') + metadata
290
+ @remaining = @prefix.bytesize + content_length
291
+ @chunk_count = (@remaining + chunk_size - 1) / chunk_size
292
+ raise KnfError, 'Too many chunks' if @chunk_count - 1 > MAX_CHUNK_INDEX
293
+
294
+ @header = Knf.send(:build_header, chunk_size, nonce_prefix)
295
+ @size = Knf.ciphertext_size(metadata_length: metadata.bytesize, content_length: content_length,
296
+ chunk_size: chunk_size)
297
+ @index = 0
298
+ @buffer = @header.dup
299
+ end
300
+
301
+ # IO#read semantics: without +length+ returns everything left ("" at EOF); with +length+ returns up to
302
+ # +length+ bytes, or nil at EOF.
303
+ def read(length = nil, outbuf = nil)
304
+ if length.nil?
305
+ fill(Float::INFINITY)
306
+ data = take(@buffer.bytesize)
307
+ else
308
+ raise ArgumentError, "negative length #{length} given" if length.negative?
309
+
310
+ fill(length)
311
+ return replace(outbuf, ''.b) if length.zero?
312
+ return replace(outbuf, nil) if @buffer.empty?
313
+
314
+ data = take([length, @buffer.bytesize].min)
315
+ end
316
+ replace(outbuf, data)
317
+ end
318
+
319
+ def readpartial(length, outbuf = nil)
320
+ data = read(length, outbuf)
321
+ raise EOFError, 'end of file reached' if data.nil?
322
+
323
+ data
324
+ end
325
+
326
+ def eof?
327
+ @buffer.empty? && @index >= @chunk_count
328
+ end
329
+
330
+ private
331
+
332
+ def normalize_source(content)
333
+ case content
334
+ when String
335
+ bytes = content.b
336
+ [StringIO.new(bytes), bytes.bytesize]
337
+ else
338
+ raise ArgumentError, 'content must be a String or a readable IO' unless content.respond_to?(:read)
339
+
340
+ if content.respond_to?(:size) && content.respond_to?(:pos)
341
+ [content, content.size - content.pos]
342
+ else
343
+ bytes = (content.read || '').b
344
+ [StringIO.new(bytes), bytes.bytesize]
345
+ end
346
+ end
347
+ end
348
+
349
+ def fill(length)
350
+ @buffer << next_sealed_chunk while @buffer.bytesize < length && @index < @chunk_count
351
+ end
352
+
353
+ def take(length)
354
+ data = @buffer.byteslice(0, length)
355
+ @buffer = @buffer.byteslice(length..) || ''.b
356
+ data
357
+ end
358
+
359
+ def replace(outbuf, data)
360
+ return data if outbuf.nil?
361
+
362
+ outbuf.replace(data || ''.b)
363
+ data && outbuf
364
+ end
365
+
366
+ def next_sealed_chunk
367
+ length = [@chunk_size, @remaining].min
368
+ chunk = ''.b
369
+ unless @prefix.empty?
370
+ chunk << @prefix.byteslice(0, length)
371
+ @prefix = @prefix.byteslice(length..) || ''.b
372
+ end
373
+ chunk << read_source(length - chunk.bytesize)
374
+ @remaining -= length
375
+ last = @index == @chunk_count - 1
376
+ ensure_source_exhausted if last
377
+
378
+ nonce = Knf.send(:chunk_nonce, @nonce_prefix, @index, last)
379
+ @index += 1
380
+ Knf.send(:seal, @key, nonce, @header, chunk)
381
+ end
382
+
383
+ def read_source(length)
384
+ data = ''.b
385
+ while data.bytesize < length
386
+ part = @source.read(length - data.bytesize)
387
+ raise KnfError, 'Content is shorter than its declared size' if part.nil? || part.empty?
388
+
389
+ data << part.b
390
+ end
391
+ data
392
+ end
393
+
394
+ def ensure_source_exhausted
395
+ extra = @source.read(1)
396
+ raise KnfError, 'Content is longer than its declared size' unless extra.nil? || extra.empty?
397
+ end
398
+ end
399
+
400
+ # Incremental decryptor: #push bytes as they arrive, then #finish. A full-size chunk is only opened once more
401
+ # data has arrived, because the final chunk is authenticated with the "last" flag — this detects truncation.
402
+ class Decryptor
403
+ def initialize(key:)
404
+ Knf.send(:check_key!, key)
405
+ @key = key.b
406
+ @header = nil
407
+ @buffer = ''.b
408
+ @plaintext = ''.b
409
+ @index = 0
410
+ @finished = false
411
+ end
412
+
413
+ def push(bytes)
414
+ raise KnfError, 'Decryptor already finished' if @finished
415
+
416
+ @buffer << bytes.b
417
+ return self unless header_ready?
418
+
419
+ sealed_size = @chunk_size + TAG_SIZE
420
+ offset = 0
421
+ # Keep at least one byte beyond a full chunk before opening it as non-final.
422
+ while @buffer.bytesize - offset > sealed_size
423
+ open_chunk(@buffer.byteslice(offset, sealed_size), last: false)
424
+ offset += sealed_size
425
+ end
426
+ @buffer = @buffer.byteslice(offset..) if offset.positive?
427
+ self
428
+ end
429
+
430
+ # Opens the final chunk and returns Knf::Decrypted. Raises KnfError if the ciphertext is truncated.
431
+ def finish
432
+ raise KnfError, 'Decryptor already finished' if @finished
433
+ raise KnfError, 'Ciphertext truncated' if @header.nil? || @buffer.bytesize <= TAG_SIZE
434
+
435
+ open_chunk(@buffer, last: true)
436
+ @finished = true
437
+ @buffer = ''.b
438
+ stream = @plaintext
439
+ @plaintext = ''.b
440
+ Knf.send(:decode_stream, stream)
441
+ end
442
+
443
+ private
444
+
445
+ def header_ready?
446
+ return true if @header
447
+ return false if @buffer.bytesize < HEADER_SIZE
448
+
449
+ header = @buffer.byteslice(0, HEADER_SIZE)
450
+ @chunk_size, @nonce_prefix = Knf.send(:parse_header, header)
451
+ @header = header
452
+ @buffer = @buffer.byteslice(HEADER_SIZE..)
453
+ true
454
+ end
455
+
456
+ def open_chunk(sealed, last:)
457
+ nonce = Knf.send(:chunk_nonce, @nonce_prefix, @index, last)
458
+ @plaintext << Knf.send(:open_sealed, @key, nonce, @header, sealed)
459
+ @index += 1
460
+ rescue KnfError
461
+ # Any authentication failure is fatal: discard everything decrypted so far.
462
+ @plaintext = ''.b
463
+ @buffer = ''.b
464
+ @finished = true
465
+ raise
466
+ end
467
+ end
468
+ end
469
+ end
@@ -1,24 +1,22 @@
1
1
  module Konfidant
2
- FileMetadataHeaders = Data.define(:user_id, :ttl_hours, :organization_id)
2
+ # Result of Client#share_text. +share_url+ carries the decryption key in its fragment — treat it as a secret.
3
+ TextShare = Data.define(:share_url, :text_id, :expires_at)
3
4
 
4
- ShareTextResponse = Data.define(:text_id, :share_url, :expires_at, :verified_burn)
5
+ # Result of Client#share_file. +share_url+ carries the decryption key in its fragment — treat it as a secret.
6
+ FileShare = Data.define(:share_url, :file_id, :expires_at, :verified_burn)
5
7
 
6
- ShareFileResponse = Data.define(:upload_url, :file_key, :metadata_headers, :poll_url)
8
+ # Result of Client#create_file_upload. +ciphertext_size+ is the size declared to the server (echoed client-side).
9
+ FileUpload = Data.define(:upload_url, :file_key, :upload_headers, :upload_expires_in, :ciphertext_size)
7
10
 
8
- FileStatusResponse = Data.define(
9
- :status, :message, :file_id, :file_name, :share_url, :expires_at, :verified_burn
10
- ) do
11
- def initialize(status:, message: nil, file_id: nil, file_name: nil,
12
- share_url: nil, expires_at: nil, verified_burn: nil)
13
- super
14
- end
15
- end
11
+ # Result of Client#complete_file_upload. +download_url+ holds only the token (#t=…); append the key with
12
+ # Konfidant::Knf.build_share_url to obtain a usable share link.
13
+ CompletedUpload = Data.define(:download_url, :file_id, :expires_at, :verified_burn)
16
14
 
17
- Share = Data.define(
18
- :type, :file_name, :file_size_bytes, :created_at, :expires_at, :accessed_at, :created_by
19
- ) do
20
- def initialize(type:, file_name:, file_size_bytes:, created_at:,
21
- expires_at:, created_by:, accessed_at: nil)
15
+ # Result of Client#open_share. +kind+ is "text" or "file"; +data+ is binary; +text+ is set for text shares only.
16
+ OpenedShare = Data.define(:kind, :name, :mime, :data, :text)
17
+
18
+ Share = Data.define(:type, :file_size_bytes, :created_at, :expires_at, :accessed_at, :created_by) do
19
+ def initialize(type:, created_at:, expires_at:, file_size_bytes: nil, accessed_at: nil, created_by: nil)
22
20
  super
23
21
  end
24
22
  end
@@ -26,6 +24,4 @@ module Konfidant
26
24
  Pagination = Data.define(:total, :limit, :offset, :has_more)
27
25
 
28
26
  ListSharesResponse = Data.define(:shares, :pagination)
29
-
30
- ShareResult = Data.define(:share_url, :file_id, :expires_at, :verified_burn)
31
27
  end
@@ -1,3 +1,3 @@
1
1
  module Konfidant
2
- VERSION = '0.7.0'
2
+ VERSION = '1.3.0'
3
3
  end
data/lib/konfidant.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'konfidant/version'
2
2
  require_relative 'konfidant/errors'
3
3
  require_relative 'konfidant/types'
4
+ require_relative 'konfidant/knf'
4
5
  require_relative 'konfidant/client'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: konfidant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konfidant
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '0.22'
60
+ version: '1.0'
61
61
  type: :development
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0.22'
67
+ version: '1.0'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: simplecov-lcov
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -89,6 +89,7 @@ files:
89
89
  - lib/konfidant.rb
90
90
  - lib/konfidant/client.rb
91
91
  - lib/konfidant/errors.rb
92
+ - lib/konfidant/knf.rb
92
93
  - lib/konfidant/types.rb
93
94
  - lib/konfidant/version.rb
94
95
  homepage: https://github.com/konfidant/sdk-ruby
@@ -109,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
110
  - !ruby/object:Gem::Version
110
111
  version: '0'
111
112
  requirements: []
112
- rubygems_version: 4.0.10
113
+ rubygems_version: 4.0.20
113
114
  specification_version: 4
114
- summary: Official Ruby SDK for the Konfidant API
115
+ summary: Official Ruby SDK for the Konfidant API (client-side, zero-knowledge encryption)
115
116
  test_files: []