diff --git a/lib-es5/utils/consts.js b/lib-es5/utils/consts.js index 167c205e..a5364e90 100644 --- a/lib-es5/utils/consts.js +++ b/lib-es5/utils/consts.js @@ -70,6 +70,9 @@ var LAYER_KEYWORD_PARAMS = { var UPLOAD_PREFIX = "https://api.cloudinary.com"; +var SUPPORTED_SIGNATURE_ALGORITHMS = ["sha1", "sha256"]; +var DEFAULT_SIGNATURE_ALGORITHM = "sha1"; + module.exports = { DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION, DEFAULT_POSTER_OPTIONS, @@ -79,5 +82,7 @@ module.exports = { LAYER_KEYWORD_PARAMS, TRANSFORMATION_PARAMS, SIMPLE_PARAMS, - UPLOAD_PREFIX + UPLOAD_PREFIX, + SUPPORTED_SIGNATURE_ALGORITHMS, + DEFAULT_SIGNATURE_ALGORITHM }; \ No newline at end of file diff --git a/lib-es5/utils/index.js b/lib-es5/utils/index.js index 79c83afd..7d9b72d1 100644 --- a/lib-es5/utils/index.js +++ b/lib-es5/utils/index.js @@ -99,7 +99,9 @@ var _require2 = require('./consts'), LAYER_KEYWORD_PARAMS = _require2.LAYER_KEYWORD_PARAMS, TRANSFORMATION_PARAMS = _require2.TRANSFORMATION_PARAMS, SIMPLE_PARAMS = _require2.SIMPLE_PARAMS, - UPLOAD_PREFIX = _require2.UPLOAD_PREFIX; + UPLOAD_PREFIX = _require2.UPLOAD_PREFIX, + SUPPORTED_SIGNATURE_ALGORITHMS = _require2.SUPPORTED_SIGNATURE_ALGORITHMS, + DEFAULT_SIGNATURE_ALGORITHM = _require2.DEFAULT_SIGNATURE_ALGORITHM; function textStyle(layer) { var keywords = []; @@ -757,6 +759,10 @@ function url(public_id) { var api_secret = consumeOption(options, "api_secret", config().api_secret); var url_suffix = consumeOption(options, "url_suffix"); var use_root_path = consumeOption(options, "use_root_path", config().use_root_path); + var signature_algorithm = consumeOption(options, "signature_algorithm", config().signature_algorithm || DEFAULT_SIGNATURE_ALGORITHM); + if (long_url_signature) { + signature_algorithm = 'sha256'; + } var auth_token = consumeOption(options, "auth_token"); if (auth_token !== false) { auth_token = exports.merge(config().auth_token, auth_token); @@ -812,9 +818,8 @@ function url(public_id) { } // eslint-disable-next-line no-empty } catch (error) {} - var shasum = crypto.createHash(long_url_signature ? 'sha256' : 'sha1'); - shasum.update(utf8_encode(to_sign + api_secret), 'binary'); - signature = shasum.digest('base64').replace(/\//g, '_').replace(/\+/g, '-').substring(0, long_url_signature ? 32 : 8); + var hash = computeHash(to_sign + api_secret, signature_algorithm, 'base64'); + signature = hash.replace(/\//g, '_').replace(/\+/g, '-').substring(0, long_url_signature ? 32 : 8); signature = `s--${signature}--`; } var prefix = unsigned_url_prefix(public_id, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution); @@ -1009,9 +1014,24 @@ function api_sign_request(params_to_sign, api_secret) { return `${k}=${toArray(v).join(",")}`; }).sort().join("&"); - var shasum = crypto.createHash('sha1'); - shasum.update(utf8_encode(to_sign + api_secret), 'binary'); - return shasum.digest('hex'); + return computeHash(to_sign + api_secret, config().signature_algorithm || DEFAULT_SIGNATURE_ALGORITHM, 'hex'); +} + +/** + * Computes hash from input string using specified algorithm. + * @private + * @param {string} input string which to compute hash from + * @param {string} signature_algorithm algorithm to use for computing hash + * @param {string} encoding type of encoding + * @return {string} computed hash value + */ +function computeHash(input, signature_algorithm, encoding) { + if (!SUPPORTED_SIGNATURE_ALGORITHMS.includes(signature_algorithm)) { + throw new Error(`Signature algorithm ${signature_algorithm} is not supported. Supported algorithms: ${SUPPORTED_SIGNATURE_ALGORITHMS.join(', ')}`); + } + var hash = crypto.createHash(signature_algorithm); + hash.update(utf8_encode(input), 'binary'); + return hash.digest(encoding); } function clear_blank(hash) { @@ -1053,9 +1073,8 @@ function webhook_signature(data, timestamp) { ensurePresenceOf({ data, timestamp }); var api_secret = ensureOption(options, 'api_secret'); - var shasum = crypto.createHash('sha1'); - shasum.update(data + timestamp + api_secret, 'binary'); - return shasum.digest('hex'); + var signature_algorithm = ensureOption(options, 'signature_algorithm', DEFAULT_SIGNATURE_ALGORITHM); + return computeHash(data + timestamp + api_secret, signature_algorithm, 'hex'); } /** @@ -1075,7 +1094,10 @@ function verifyNotificationSignature(body, timestamp, signature) { if (timestamp < Date.now() - valid_for) { return false; } - var payload_hash = utils.webhook_signature(body, timestamp, { api_secret: config().api_secret }); + var payload_hash = utils.webhook_signature(body, timestamp, { + api_secret: config().api_secret, + signature_algorithm: config().signature_algorithm + }); return signature === payload_hash; } diff --git a/lib/utils/consts.js b/lib/utils/consts.js index d2eb686f..90f74494 100644 --- a/lib/utils/consts.js +++ b/lib/utils/consts.js @@ -131,6 +131,9 @@ const LAYER_KEYWORD_PARAMS = { const UPLOAD_PREFIX = "https://api.cloudinary.com"; +const SUPPORTED_SIGNATURE_ALGORITHMS = ["sha1", "sha256"]; +const DEFAULT_SIGNATURE_ALGORITHM = "sha1"; + module.exports = { DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION, DEFAULT_POSTER_OPTIONS, @@ -140,5 +143,7 @@ module.exports = { LAYER_KEYWORD_PARAMS, TRANSFORMATION_PARAMS, SIMPLE_PARAMS, - UPLOAD_PREFIX + UPLOAD_PREFIX, + SUPPORTED_SIGNATURE_ALGORITHMS, + DEFAULT_SIGNATURE_ALGORITHM }; diff --git a/lib/utils/index.js b/lib/utils/index.js index 8719daad..13867857 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -85,7 +85,9 @@ const { LAYER_KEYWORD_PARAMS, TRANSFORMATION_PARAMS, SIMPLE_PARAMS, - UPLOAD_PREFIX + UPLOAD_PREFIX, + SUPPORTED_SIGNATURE_ALGORITHMS, + DEFAULT_SIGNATURE_ALGORITHM } = require('./consts'); function textStyle(layer) { @@ -692,6 +694,10 @@ function url(public_id, options = {}) { let api_secret = consumeOption(options, "api_secret", config().api_secret); let url_suffix = consumeOption(options, "url_suffix"); let use_root_path = consumeOption(options, "use_root_path", config().use_root_path); + let signature_algorithm = consumeOption(options, "signature_algorithm", config().signature_algorithm || DEFAULT_SIGNATURE_ALGORITHM); + if (long_url_signature) { + signature_algorithm = 'sha256'; + } let auth_token = consumeOption(options, "auth_token"); if (auth_token !== false) { auth_token = exports.merge(config().auth_token, auth_token); @@ -735,9 +741,8 @@ function url(public_id, options = {}) { // eslint-disable-next-line no-empty } catch (error) { } - let shasum = crypto.createHash(long_url_signature ? 'sha256' : 'sha1'); - shasum.update(utf8_encode(to_sign + api_secret), 'binary'); - signature = shasum.digest('base64').replace(/\//g, '_').replace(/\+/g, '-').substring(0, long_url_signature ? 32 : 8); + let hash = computeHash(to_sign + api_secret, signature_algorithm, 'base64'); + signature = hash.replace(/\//g, '_').replace(/\+/g, '-').substring(0, long_url_signature ? 32 : 8); signature = `s--${signature}--`; } let prefix = unsigned_url_prefix( @@ -934,9 +939,24 @@ function api_sign_request(params_to_sign, api_secret) { ).map( ([k, v]) => `${k}=${toArray(v).join(",")}` ).sort().join("&"); - let shasum = crypto.createHash('sha1'); - shasum.update(utf8_encode(to_sign + api_secret), 'binary'); - return shasum.digest('hex'); + return computeHash(to_sign + api_secret, config().signature_algorithm || DEFAULT_SIGNATURE_ALGORITHM, 'hex'); +} + +/** + * Computes hash from input string using specified algorithm. + * @private + * @param {string} input string which to compute hash from + * @param {string} signature_algorithm algorithm to use for computing hash + * @param {string} encoding type of encoding + * @return {string} computed hash value + */ +function computeHash(input, signature_algorithm, encoding) { + if (!SUPPORTED_SIGNATURE_ALGORITHMS.includes(signature_algorithm)) { + throw new Error(`Signature algorithm ${signature_algorithm} is not supported. Supported algorithms: ${SUPPORTED_SIGNATURE_ALGORITHMS.join(', ')}`); + } + let hash = crypto.createHash(signature_algorithm); + hash.update(utf8_encode(input), 'binary'); + return hash.digest(encoding); } function clear_blank(hash) { @@ -966,9 +986,8 @@ function webhook_signature(data, timestamp, options = {}) { ensurePresenceOf({ data, timestamp }); let api_secret = ensureOption(options, 'api_secret'); - let shasum = crypto.createHash('sha1'); - shasum.update(data + timestamp + api_secret, 'binary'); - return shasum.digest('hex'); + let signature_algorithm = ensureOption(options, 'signature_algorithm', DEFAULT_SIGNATURE_ALGORITHM); + return computeHash(data + timestamp + api_secret, signature_algorithm, 'hex'); } /** @@ -986,7 +1005,10 @@ function verifyNotificationSignature(body, timestamp, signature, valid_for = 720 if (timestamp < Date.now() - valid_for) { return false; } - const payload_hash = utils.webhook_signature(body, timestamp, { api_secret: config().api_secret }); + const payload_hash = utils.webhook_signature(body, timestamp, { + api_secret: config().api_secret, + signature_algorithm: config().signature_algorithm + }); return signature === payload_hash; } diff --git a/test/unit/cloudinary_spec.js b/test/unit/cloudinary_spec.js index d920e295..27ed5703 100644 --- a/test/unit/cloudinary_spec.js +++ b/test/unit/cloudinary_spec.js @@ -7,7 +7,8 @@ describe("cloudinary", function () { cloud_name: "test123", api_key: 'a', api_secret: 'b', - responsive_width_transformation: null + responsive_width_transformation: null, + signature_algorithm: 'sha1' })); }); it("should use cloud_name from config", function () { @@ -471,6 +472,30 @@ describe("cloudinary", function () { undef: void 0 }, "1234")).to.eql("f05cfe85cee78e7e997b3c7da47ba212dcbf1ea5"); }); + it("should correctly sign api requests with signature algorithm SHA1", function () { + cloudinary.config({ signature_algorithm: 'sha1' }); + expect(cloudinary.utils.api_sign_request({ + username: "user@cloudinary.com", + timestamp: 1568810420, + cloud_name: "dn6ot3ged" + }, "hdcixPpR2iKERPwqvH6sHdK9cyac")).to.eql("14c00ba6d0dfdedbc86b316847d95b9e6cd46d94"); + }); + it("should correctly sign api requests with signature algorithm SHA1 as default", function () { + cloudinary.config({ signature_algorithm: null }); + expect(cloudinary.utils.api_sign_request({ + username: "user@cloudinary.com", + timestamp: 1568810420, + cloud_name: "dn6ot3ged" + }, "hdcixPpR2iKERPwqvH6sHdK9cyac")).to.eql("14c00ba6d0dfdedbc86b316847d95b9e6cd46d94"); + }); + it("should correctly sign api requests with signature algorithm SHA256", function () { + cloudinary.config({ signature_algorithm: 'sha256' }); + expect(cloudinary.utils.api_sign_request({ + username: "user@cloudinary.com", + timestamp: 1568810420, + cloud_name: "dn6ot3ged" + }, "hdcixPpR2iKERPwqvH6sHdK9cyac")).to.eql("45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd"); + }); it("should correctly build signed preloaded image", function () { expect(cloudinary.utils.signed_preloaded_image({ resource_type: "image", @@ -827,4 +852,13 @@ describe("cloudinary", function () { result = cloudinary.utils.url("sample.jpg", options); expect(result).to.eql('http://res.cloudinary.com/test123/image/upload/s--v2fTPYTu--/sample.jpg'); }); + it("should generate urls with signature algorithm SHA256 when sign_url is true", function () { + var options, result; + options = { + sign_url: true, + signature_algorithm: 'sha256' + }; + result = cloudinary.utils.url("sample.jpg", options); + expect(result).to.eql('http://res.cloudinary.com/test123/image/upload/s--2hbrSMPO--/sample.jpg'); + }); }); diff --git a/test/utils/utils_spec.js b/test/utils/utils_spec.js index 36806165..d3810d04 100644 --- a/test/utils/utils_spec.js +++ b/test/utils/utils_spec.js @@ -34,7 +34,8 @@ describe("utils", function () { private_cdn: false, secure: false, cname: null, - cdn_subdomain: false + cdn_subdomain: false, + signature_algorithm: undefined })); this.orig = clone(this.cfg); cloud_name = cloudinary.config("cloud_name"); @@ -1438,6 +1439,20 @@ describe("utils", function () { ) ).to.eql(true); }); + it("should return true when signature with algorithm SHA256 is valid", function () { + cloudinary.config({ + api_secret: 'hardcoded', + signature_algorithm: 'sha256' + }); + const distant_future_timestamp = 7952342400000; // 2222-01-01T00:00:00Z + expect( + utils.verifyNotificationSignature( + response_json, + distant_future_timestamp, + "6c5a29fd8815772fbac2f10ae741e093d0859313947ef8fadeb29126ded6649c" + ) + ).to.eql(true); + }); it("should return false when signature is not valid", function () { response_signature = utils.webhook_signature(response_json, valid_response_timestamp, { api_secret: cloudinary.config().api_secret @@ -1485,18 +1500,13 @@ describe("utils", function () { }); }); context("sign URLs", function () { - var configBck = void 0; - before(function () { - configBck = cloudinary.config(); + beforeEach(function () { cloudinary.config({ cloud_name: 'test123', api_key: "1234", api_secret: "b" }); }); - after(function () { - cloudinary.config(configBck); - }); it("should correctly sign URLs", function () { test_cloudinary_url("image.jpg", { version: 1234,