Skip to content

Commit 13c6d27

Browse files
author
RTLcoil
authored
Add support of SHA-256 algorithm in auth signatures
1 parent 10d36f7 commit 13c6d27

File tree

3 files changed

+69
-25
lines changed

3 files changed

+69
-25
lines changed

lib/cloudinary/uploader.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,10 +341,11 @@ def self.call_api(action, options)
341341
non_signable ||= []
342342

343343
unless options[:unsigned]
344-
api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
345-
api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
346-
params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret)
347-
params[:api_key] = api_key
344+
api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
345+
api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
346+
signature_algorithm = options[:signature_algorithm]
347+
params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret, signature_algorithm)
348+
params[:api_key] = api_key
348349
end
349350
proxy = options[:api_proxy] || Cloudinary.config.api_proxy
350351
timeout = options.fetch(:timeout) { Cloudinary.config.to_h.fetch(:timeout, 60) }

lib/cloudinary/utils.rb

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ class Cloudinary::Utils
144144
LONG_URL_SIGNATURE_LENGTH = 32
145145
SHORT_URL_SIGNATURE_LENGTH = 8
146146

147+
ALGO_SHA1 = :sha1
148+
ALGO_SHA256 = :sha256
149+
150+
ALGORITHM_SIGNATURE = {
151+
ALGO_SHA1 => Digest::SHA1,
152+
ALGO_SHA256 => Digest::SHA256,
153+
}
154+
147155
def self.extract_config_params(options)
148156
options.select{|k,v| URL_KEYS.include?(k)}
149157
end
@@ -433,9 +441,9 @@ def self.api_string_to_sign(params_to_sign)
433441
params_to_sign.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.reject{|k,v| v.nil? || v == ""}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
434442
end
435443

436-
def self.api_sign_request(params_to_sign, api_secret)
444+
def self.api_sign_request(params_to_sign, api_secret, signature_algorithm = nil)
437445
to_sign = api_string_to_sign(params_to_sign)
438-
Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
446+
hash("#{to_sign}#{api_secret}", signature_algorithm, :hexdigest)
439447
end
440448

441449
# Returns a JSON array as String.
@@ -501,6 +509,7 @@ def self.unsigned_download_url(source, options = {})
501509
use_root_path = config_option_consume(options, :use_root_path)
502510
auth_token = config_option_consume(options, :auth_token)
503511
long_url_signature = config_option_consume(options, :long_url_signature)
512+
signature_algorithm = config_option_consume(options, :signature_algorithm)
504513
unless auth_token == false
505514
auth_token = Cloudinary::AuthToken.merge_auth_token(Cloudinary.config.auth_token, auth_token)
506515
end
@@ -545,7 +554,10 @@ def self.unsigned_download_url(source, options = {})
545554
raise(CloudinaryException, "Must supply api_secret") if (secret.nil? || secret.empty?)
546555
to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
547556
to_sign = fully_unescape(to_sign)
548-
signature = compute_signature(to_sign, secret, long_url_signature)
557+
signature_algorithm = long_url_signature ? ALGO_SHA256 : signature_algorithm
558+
hash = hash("#{to_sign}#{secret}", signature_algorithm)
559+
signature = Base64.urlsafe_encode64(hash)
560+
signature = "s--#{signature[0, long_url_signature ? LONG_URL_SIGNATURE_LENGTH : SHORT_URL_SIGNATURE_LENGTH ]}--"
549561
end
550562

551563
prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
@@ -671,8 +683,9 @@ def self.cloudinary_api_url(action = 'upload', options = {})
671683
def self.sign_request(params, options={})
672684
api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
673685
api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
686+
signature_algorithm = options[:signature_algorithm]
674687
params = params.reject{|k, v| self.safe_blank?(v)}
675-
params[:signature] = Cloudinary::Utils.api_sign_request(params, api_secret)
688+
params[:signature] = api_sign_request(params, api_secret, signature_algorithm)
676689
params[:api_key] = api_key
677690
params
678691
end
@@ -1163,23 +1176,18 @@ def self.to_usage_api_date_format(date)
11631176
end
11641177
end
11651178

1166-
# Computes a short or long signature based on a message and secret
1167-
# @param [String] message The string to sign
1168-
# @param [String] secret A secret that will be added to the message when signing
1169-
# @param [Boolean] long_signature Whether to create a short or long signature
1170-
# @return [String] Properly formatted signature
1171-
def self.compute_signature(message, secret, long_url_signature)
1172-
combined_message_secret = message + secret
1173-
1174-
algo, signature_length =
1175-
if long_url_signature
1176-
[Digest::SHA256, LONG_URL_SIGNATURE_LENGTH]
1177-
else
1178-
[Digest::SHA1, SHORT_URL_SIGNATURE_LENGTH]
1179-
end
1180-
1181-
"s--#{Base64.urlsafe_encode64(algo.digest(combined_message_secret))[0, signature_length]}--"
1179+
# Computes hash from input string using specified algorithm.
1180+
#
1181+
# @param [String] input String which to compute hash from
1182+
# @param [String|nil] signature_algorithm Algorithm to use for computing hash
1183+
# @param [Symbol] hash_method Hash method applied to a signature algorithm (:digest or :hexdigest)
1184+
#
1185+
# @return [String] Computed hash value
1186+
def self.hash(input, signature_algorithm = nil, hash_method = :digest)
1187+
signature_algorithm ||= Cloudinary.config.signature_algorithm || ALGO_SHA1
1188+
algorithm = ALGORITHM_SIGNATURE[signature_algorithm] || raise("Unsupported algorithm '#{signature_algorithm}'")
1189+
algorithm.public_send(hash_method, input)
11821190
end
11831191

1184-
private_class_method :compute_signature
1192+
private_class_method :hash
11851193
end

spec/utils_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,18 @@
154154
expect(expected).to eq("http://res.cloudinary.com/test123/image/upload/s--2hbrSMPOjj5BJ4xV7SgFbRDevFaQNUFf--/sample.jpg")
155155
end
156156

157+
it "should sign url with SHA256 algorithm set in configuration" do
158+
Cloudinary.config.signature_algorithm = Cloudinary::Utils::ALGO_SHA256
159+
160+
expected = Cloudinary::Utils.cloudinary_url "sample.jpg",
161+
:cloud_name => "test123",
162+
:api_key => "a",
163+
:api_secret => "b",
164+
:sign_url => true
165+
166+
expect(expected).to eq("http://res.cloudinary.com/test123/image/upload/s--2hbrSMPO--/sample.jpg")
167+
end
168+
157169
it "should not sign the url_suffix" do
158170
expected_signature = Cloudinary::Utils.cloudinary_url("test", :format => "jpg", :sign_url => true).match(/s--[0-9A-Za-z_-]{8}--/).to_s
159171
expect(["test", { :url_suffix => "hello", :private_cdn => true, :format => "jpg", :sign_url => true }])
@@ -925,6 +937,29 @@
925937
expect(Cloudinary::Utils.encode_double_array([[1, 2, 3, 4], [5, 6, 7, 8]])).to eq("1,2,3,4|5,6,7,8")
926938
end
927939

940+
it "should sign an API request using SHA1 by default" do
941+
signature = Cloudinary::Utils.api_sign_request({ :cloud_name => "dn6ot3ged", :timestamp => 1568810420, :username => "[email protected]" }, "hdcixPpR2iKERPwqvH6sHdK9cyac")
942+
expect(signature).to eq("14c00ba6d0dfdedbc86b316847d95b9e6cd46d94")
943+
end
944+
945+
it "should sign an API request using SHA256" do
946+
Cloudinary.config.signature_algorithm = Cloudinary::Utils::ALGO_SHA256
947+
signature = Cloudinary::Utils.api_sign_request({ :cloud_name => "dn6ot3ged", :timestamp => 1568810420, :username => "[email protected]" }, "hdcixPpR2iKERPwqvH6sHdK9cyac")
948+
expect(signature).to eq("45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd")
949+
end
950+
951+
it "should sign an API request using SHA256 via parameter" do
952+
signature = Cloudinary::Utils.api_sign_request({ :cloud_name => "dn6ot3ged", :timestamp => 1568810420, :username => "[email protected]" }, "hdcixPpR2iKERPwqvH6sHdK9cyac", :sha256)
953+
expect(signature).to eq("45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd")
954+
end
955+
956+
it "should raise when unsupported algorithm is passed" do
957+
signature_algorithm = "unsupported_algorithm"
958+
959+
expect{Cloudinary::Utils.api_sign_request({ :cloud_name => "dn6ot3ged", :timestamp => 1568810420, :username => "[email protected]" }, "hdcixPpR2iKERPwqvH6sHdK9cyac", signature_algorithm)}
960+
.to raise_error("Unsupported algorithm 'unsupported_algorithm'")
961+
end
962+
928963
describe ":if" do
929964
describe 'with literal condition string' do
930965
it "should include the if parameter as the first component in the transformation string" do

0 commit comments

Comments
 (0)