Merge branch '8784-dir-listings'
[arvados.git] / services / api / app / models / blob.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class Blob
6   extend DbCurrentTime
7
8   def initialize locator
9     @locator = locator
10   end
11
12   def empty?
13     !!@locator.match(/^d41d8cd98f00b204e9800998ecf8427e(\+.*)?$/)
14   end
15
16   # In order to get a Blob from Keep, you have to prove either
17   # [a] you have recently written it to Keep yourself, or
18   # [b] apiserver has recently decided that you should be able to read it
19   #
20   # To ensure that the requestor of a blob is authorized to read it,
21   # Keep requires clients to timestamp the blob locator with an expiry
22   # time, and to sign the timestamped locator with their API token.
23   #
24   # A signed blob locator has the form:
25   #     locator_hash +A blob_signature @ timestamp
26   # where the timestamp is a Unix time expressed as a hexadecimal value,
27   # and the blob_signature is the signed locator_hash + API token + timestamp.
28   # 
29   class InvalidSignatureError < StandardError
30   end
31
32   # Blob.sign_locator: return a signed and timestamped blob locator.
33   #
34   # The 'opts' argument should include:
35   #   [required] :api_token - API token (signatures only work for this token)
36   #   [optional] :key       - the Arvados server-side blobstore key
37   #   [optional] :ttl       - number of seconds before signature should expire
38   #   [optional] :expire    - unix timestamp when signature should expire
39   #
40   def self.sign_locator blob_locator, opts
41     # We only use the hash portion for signatures.
42     blob_hash = blob_locator.split('+').first
43
44     # Generate an expiry timestamp (seconds after epoch, base 16)
45     if opts[:expire]
46       if opts[:ttl]
47         raise "Cannot specify both :ttl and :expire options"
48       end
49       timestamp = opts[:expire]
50     else
51       timestamp = db_current_time.to_i +
52         (opts[:ttl] || Rails.configuration.blob_signature_ttl)
53     end
54     timestamp_hex = timestamp.to_s(16)
55     # => "53163cb4"
56     blob_signature_ttl = Rails.configuration.blob_signature_ttl.to_s(16)
57
58     # Generate a signature.
59     signature =
60       generate_signature((opts[:key] or Rails.configuration.blob_signing_key),
61                          blob_hash, opts[:api_token], timestamp_hex, blob_signature_ttl)
62
63     blob_locator + '+A' + signature + '@' + timestamp_hex
64   end
65
66   # Blob.verify_signature
67   #   Safely verify the signature on a blob locator.
68   #   Return value: true if the locator has a valid signature, false otherwise
69   #   Arguments: signed_blob_locator, opts
70   #
71   def self.verify_signature(*args)
72     begin
73       self.verify_signature!(*args)
74       true
75     rescue Blob::InvalidSignatureError
76       false
77     end
78   end
79
80   # Blob.verify_signature!
81   #   Verify the signature on a blob locator.
82   #   Return value: true if the locator has a valid signature
83   #   Arguments: signed_blob_locator, opts
84   #   Exceptions:
85   #     Blob::InvalidSignatureError if the blob locator does not include a
86   #     valid signature
87   #
88   def self.verify_signature! signed_blob_locator, opts
89     blob_hash = signed_blob_locator.split('+').first
90     given_signature, timestamp = signed_blob_locator.
91       split('+A').last.
92       split('+').first.
93       split('@')
94
95     if !timestamp
96       raise Blob::InvalidSignatureError.new 'No signature provided.'
97     end
98     unless timestamp =~ /^[\da-f]+$/
99       raise Blob::InvalidSignatureError.new 'Timestamp is not a base16 number.'
100     end
101     if timestamp.to_i(16) < (opts[:now] or db_current_time.to_i)
102       raise Blob::InvalidSignatureError.new 'Signature expiry time has passed.'
103     end
104     blob_signature_ttl = Rails.configuration.blob_signature_ttl.to_s(16)
105
106     my_signature =
107       generate_signature((opts[:key] or Rails.configuration.blob_signing_key),
108                          blob_hash, opts[:api_token], timestamp, blob_signature_ttl)
109
110     if my_signature != given_signature
111       raise Blob::InvalidSignatureError.new 'Signature is invalid.'
112     end
113
114     true
115   end
116
117   def self.generate_signature key, blob_hash, api_token, timestamp, blob_signature_ttl
118     OpenSSL::HMAC.hexdigest('sha1', key,
119                             [blob_hash,
120                              api_token,
121                              timestamp,
122                              blob_signature_ttl].join('@'))
123   end
124 end