Skip to content

Instantly share code, notes, and snippets.

@lukepolo
Last active June 17, 2020 16:20
Show Gist options
  • Select an option

  • Save lukepolo/1bc4ee9e8133ab33484a8d8ec8ef9e17 to your computer and use it in GitHub Desktop.

Select an option

Save lukepolo/1bc4ee9e8133ab33484a8d8ec8ef9e17 to your computer and use it in GitHub Desktop.
google-storage-url-signer.sql
create or replace function qx.create_google_storage_url(
file_path text,
bucket text default 'qx-media-unstable',
request_location text default 'auto',
request_service text default 'storage',
request_type text default 'goog4_request'
)
returns text
volatile
language plpgsql
as $$
declare
host text;
access_key text;
hash_algorithm text;
credential_scope text;
string_to_sign text;
request_date text;
request_date_time text;
path_to_resource text;
canonical_request text;
canonical_query_string text;
begin
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#required-headers
host = 'storage.googleapis.com';
-- https://cloud.google.com/storage/docs/authentication/signatures#credential-scope
request_date = to_char(now(), 'YYYYMMDD');
-- https://cloud.google.com/storage/docs/authentication/signatures#active_datetime
request_date_time = to_char(now(), 'YYYYMMDD"T"HH24MISS"Z"');
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#about-resource-path
path_to_resource = concat('/', bucket, '/', file_path);
-- https://cloud.google.com/storage/docs/authentication/signatures#credential-scope
credential_scope = concat(request_date, '/', request_location, '/', request_service, '/', request_type);
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#required-query-parameters
hash_algorithm = 'GOOG4-HMAC-SHA256';
-- todo : we need to not store this in plain text
access_key = qx.current_setting('qx.google_storage_access_key');
canonical_query_string = concat(
concat('X-Goog-Algorithm', '=', hash_algorithm),
concat('&', 'X-Goog-Credential', '=', replace(concat(access_key, '/', credential_scope), '/', '%2F')),
concat('&', 'X-Goog-Date', '=', request_date_time),
concat('&', 'X-Goog-Expires', '=', 60 * 60),
concat('&', 'X-Goog-SignedHeaders', '=', 'host')
);
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#request-structure
canonical_request = concat(
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#verbs
'GET',
e'\n',
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#about-resource-path
qx.google_file_path_encode(path_to_resource),
e'\n',
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#about-query-strings
canonical_query_string,
e'\n',
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#about-headers
concat('host:', host),
e'\n',
e'\n',
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#signed_headers
'host',
e'\n',
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#payload
'UNSIGNED-PAYLOAD'
);
-- https://cloud.google.com/storage/docs/authentication/signatures#string-to-sign
string_to_sign = concat(
-- https://cloud.google.com/storage/docs/authentication/signatures#signing_algorithm
hash_algorithm,
e'\n',
-- https://cloud.google.com/storage/docs/authentication/signatures#active_datetime
request_date_time,
e'\n',
-- https://cloud.google.com/storage/docs/authentication/signatures#credential_scope
credential_scope,
e'\n',
-- https://cloud.google.com/storage/docs/authentication/signatures#hash_of_the_canonical_request
encode(digest(canonical_request, 'sha256'), 'hex')
);
-- https://cloud.google.com/storage/docs/access-control/signing-urls-manually#algorithm
return concat('https://', host, path_to_resource, '?', canonical_query_string, '&', 'X-Goog-Signature', '=', qx.create_google_storage_signature(string_to_sign, request_date, request_location, request_service, request_type));
end
$$;
create or replace function qx.create_google_storage_signature(
string_to_sign text,
request_date text,
request_location text,
request_service text,
request_type text
)
returns text
volatile
language plpgsql
as $$
declare
hmac_secret text;
message_digest text;
begin
-- todo : we need to not store this in plain text
hmac_secret = qx.current_setting('qx.google_storage_hmac_secret');
-- https://cloud.google.com/storage/docs/authentication/signatures#derive-key
message_digest = hmac(string_to_sign::text::bytea, hmac(request_type::text::bytea, hmac(request_service::text::bytea, hmac(request_location::text::bytea, hmac(request_date, concat('GOOG4', hmac_secret), 'sha256'), 'sha256'), 'sha256'), 'sha256'), 'sha256');
-- https://cloud.google.com/storage/docs/authentication/signatures#after_signing
return encode(message_digest::text::bytea, 'hex');
end
$$;
-- todo - im sure there is a better way of handling this
-- https://cloud.google.com/storage/docs/authentication/canonical-requests#about-resource-path
create or replace function qx.google_file_path_encode(
plaintext text
)
returns text
stable
language plpgsql
as $$
declare
begin
plaintext = replace(plaintext, '?', '%3F');
plaintext = replace(plaintext, '=', '%3D');
plaintext = replace(plaintext, '!', '%21');
plaintext = replace(plaintext, '#', '%23');
plaintext = replace(plaintext, '$', '%24');
plaintext = replace(plaintext, '&', '%26');
plaintext = replace(plaintext, '''', '%27');
plaintext = replace(plaintext, '(', '%28');
plaintext = replace(plaintext, ')', '%29');
plaintext = replace(plaintext, '*', '%2A');
plaintext = replace(plaintext, '+', '%2B');
plaintext = replace(plaintext, ',', '%2C');
plaintext = replace(plaintext, ';', '%3B');
plaintext = replace(plaintext, ';', '%40');
plaintext = replace(plaintext, '[', '%5B');
plaintext = replace(plaintext, ']', '%5C');
-- plaintext = replace(plaintext, '.', '%2E'); -- their docs say to do this, but leaving it works
plaintext = replace(plaintext, '"', '%22');
return plaintext;
end
$$;
@lukepolo
Copy link
Copy Markdown
Author

Updated to correct the chaining of the hmac

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment