Skip to content

Commit

Permalink
feat: adds crypto utils for AWS request signing (#1046)
Browse files Browse the repository at this point in the history
  • Loading branch information
bojeil-google authored Aug 24, 2020
1 parent bbcd03d commit 26ab5e3
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 8 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ const {GoogleAuth} = require('google-auth-library');
async function main() {
const url = 'https://cloud-run-1234-uc.a.run.app';
const auth = new GoogleAuth();
const client = auth.getIdTokenClient(url);
const client = await auth.getIdTokenClient(url);
const res = await client.request({url});
console.log(res.data);
}
Expand All @@ -375,7 +375,7 @@ async function main()
const targetAudience = 'iap-client-id';
const url = 'https://iap-url.com';
const auth = new GoogleAuth();
const client = auth.getIdTokenClient(targetAudience);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.log(res.data);
}
Expand Down
66 changes: 65 additions & 1 deletion browser-test/test.crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import * as base64js from 'base64-js';
import {assert} from 'chai';
import {createCrypto} from '../src/crypto/crypto';
import {createCrypto, fromArrayBufferToHex} from '../src/crypto/crypto';
import {BrowserCrypto} from '../src/crypto/browser/crypto';
import {privateKey, publicKey} from './fixtures/keys';
import {describe, it} from 'mocha';
Expand All @@ -24,6 +24,21 @@ import {describe, it} from 'mocha';
// text encoding natively.
require('fast-text-encoding');

/**
* Converts a string to an ArrayBuffer.
* https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
* @param str The string to convert to an ArrayBuffer.
* @return The ArrayBuffer representation of the string.
*/
function stringToArrayBuffer(str: string): ArrayBuffer {
const arrayBuffer = new ArrayBuffer(str.length * 2);
const arrayBufferView = new Uint16Array(arrayBuffer);
for (let i = 0; i < str.length; i++) {
arrayBufferView[i] = str.charCodeAt(i);
}
return arrayBuffer;
}

describe('Browser crypto tests', () => {
const crypto = createCrypto();

Expand Down Expand Up @@ -99,4 +114,53 @@ describe('Browser crypto tests', () => {
const encodedString = crypto.encodeBase64StringUtf8(originalString);
assert.strictEqual(encodedString, base64String);
});

it('should calculate SHA256 digest in hex encoding', async () => {
const input = 'I can calculate SHA256';
const expectedHexDigest =
'73d08486d8bfd4fb4bc12dd8903604ddbde5ad95b6efa567bd723ce81a881122';

const calculatedHexDigest = await crypto.sha256DigestHex(input);
assert.strictEqual(calculatedHexDigest, expectedHexDigest);
});

describe('should compute the HMAC-SHA256 hash of a message', () => {
it('using a string key', async () => {
const message = 'The quick brown fox jumps over the lazy dog';
const key = 'key';
const expectedHexHash =
'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8';
const extectedHash = new Uint8Array(
(expectedHexHash.match(/.{1,2}/g) as string[]).map(byte =>
parseInt(byte, 16)
)
);

const calculatedHash = await crypto.signWithHmacSha256(key, message);
assert.deepStrictEqual(calculatedHash, extectedHash.buffer);
});

it('using an ArrayBuffer key', async () => {
const message = 'The quick brown fox jumps over the lazy dog';
const key = stringToArrayBuffer('key');
const expectedHexHash =
'f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8';
const extectedHash = new Uint8Array(
(expectedHexHash.match(/.{1,2}/g) as string[]).map(byte =>
parseInt(byte, 16)
)
);

const calculatedHash = await crypto.signWithHmacSha256(key, message);
assert.deepStrictEqual(calculatedHash, extectedHash.buffer);
});
});

it('should expose a method to convert an ArrayBuffer to hex', () => {
const arrayBuffer = stringToArrayBuffer('Hello World!');
const expectedHexEncoding = '48656c6c6f20576f726c6421';

const calculatedHexEncoding = fromArrayBufferToHex(arrayBuffer);
assert.strictEqual(calculatedHexEncoding, expectedHexEncoding);
});
});
2 changes: 1 addition & 1 deletion src/auth/oauth2client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ export class OAuth2Client extends AuthClient {
}

/**
* Convenience method to automatically generate a code_verifier, and it's
* Convenience method to automatically generate a code_verifier, and its
* resulting SHA256. If used, this must be paired with a S256
* code_challenge_method.
*
Expand Down
61 changes: 60 additions & 1 deletion src/crypto/browser/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ if (typeof process === 'undefined' && typeof TextEncoder === 'undefined') {
require('fast-text-encoding');
}

import {Crypto, JwkCertificate} from '../crypto';
import {Crypto, JwkCertificate, fromArrayBufferToHex} from '../crypto';

export class BrowserCrypto implements Crypto {
constructor() {
Expand Down Expand Up @@ -140,4 +140,63 @@ export class BrowserCrypto implements Crypto {
const result = base64js.fromByteArray(uint8array);
return result;
}

/**
* Computes the SHA-256 hash of the provided string.
* @param str The plain text string to hash.
* @return A promise that resolves with the SHA-256 hash of the provided
* string in hexadecimal encoding.
*/
async sha256DigestHex(str: string): Promise<string> {
// SubtleCrypto digest() method is async, so we must make
// this method async as well.

// To calculate SHA256 digest using SubtleCrypto, we first
// need to convert an input string to an ArrayBuffer:
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const inputBuffer = new TextEncoder().encode(str);

// Result is ArrayBuffer as well.
const outputBuffer = await window.crypto.subtle.digest(
'SHA-256',
inputBuffer
);

return fromArrayBufferToHex(outputBuffer);
}

/**
* Computes the HMAC hash of a message using the provided crypto key and the
* SHA-256 algorithm.
* @param key The secret crypto key in utf-8 or ArrayBuffer format.
* @param msg The plain text message.
* @return A promise that resolves with the HMAC-SHA256 hash in ArrayBuffer
* format.
*/
async signWithHmacSha256(
key: string | ArrayBuffer,
msg: string
): Promise<ArrayBuffer> {
// Convert key, if provided in ArrayBuffer format, to string.
const rawKey =
typeof key === 'string'
? key
: String.fromCharCode(...new Uint16Array(key));

// eslint-disable-next-line node/no-unsupported-features/node-builtins
const enc = new TextEncoder();
const cryptoKey = await window.crypto.subtle.importKey(
'raw',
enc.encode(rawKey),
{
name: 'HMAC',
hash: {
name: 'SHA-256',
},
},
false,
['sign']
);
return window.crypto.subtle.sign('HMAC', cryptoKey, enc.encode(msg));
}
}
36 changes: 36 additions & 0 deletions src/crypto/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,26 @@ export interface Crypto {
): Promise<string>;
decodeBase64StringUtf8(base64: string): string;
encodeBase64StringUtf8(text: string): string;
/**
* Computes the SHA-256 hash of the provided string.
* @param str The plain text string to hash.
* @return A promise that resolves with the SHA-256 hash of the provided
* string in hexadecimal encoding.
*/
sha256DigestHex(str: string): Promise<string>;

/**
* Computes the HMAC hash of a message using the provided crypto key and the
* SHA-256 algorithm.
* @param key The secret crypto key in utf-8 or ArrayBuffer format.
* @param msg The plain text message.
* @return A promise that resolves with the HMAC-SHA256 hash in ArrayBuffer
* format.
*/
signWithHmacSha256(
key: string | ArrayBuffer,
msg: string
): Promise<ArrayBuffer>;
}

export function createCrypto(): Crypto {
Expand All @@ -67,3 +87,19 @@ export function hasBrowserCrypto() {
typeof window.crypto.subtle !== 'undefined'
);
}

/**
* Converts an ArrayBuffer to a hexadecimal string.
* @param arrayBuffer The ArrayBuffer to convert to hexadecimal string.
* @return The hexadecimal encoding of the ArrayBuffer.
*/
export function fromArrayBufferToHex(arrayBuffer: ArrayBuffer): string {
// Convert buffer to byte array.
const byteArray = Array.from(new Uint8Array(arrayBuffer));
// Convert bytes to hex string.
return byteArray
.map(byte => {
return byte === 0 ? '' : byte.toString(16).padStart(2, '0');
})
.join('');
}
57 changes: 57 additions & 0 deletions src/crypto/node/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,61 @@ export class NodeCrypto implements Crypto {
encodeBase64StringUtf8(text: string): string {
return Buffer.from(text, 'utf-8').toString('base64');
}

/**
* Computes the SHA-256 hash of the provided string.
* @param str The plain text string to hash.
* @return A promise that resolves with the SHA-256 hash of the provided
* string in hexadecimal encoding.
*/
async sha256DigestHex(str: string): Promise<string> {
return crypto.createHash('sha256').update(str).digest('hex');
}

/**
* Computes the HMAC hash of a message using the provided crypto key and the
* SHA-256 algorithm.
* @param key The secret crypto key in utf-8 or ArrayBuffer format.
* @param msg The plain text message.
* @return A promise that resolves with the HMAC-SHA256 hash in ArrayBuffer
* format.
*/
async signWithHmacSha256(
key: string | ArrayBuffer,
msg: string
): Promise<ArrayBuffer> {
const cryptoKey = typeof key === 'string' ? key : toBuffer(key);
return toArrayBuffer(
crypto.createHmac('sha256', cryptoKey).update(msg).digest()
);
}
}

/**
* Converts a Node.js Buffer to an ArrayBuffer.
* https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer
* @param buffer The Buffer input to covert.
* @return The ArrayBuffer representation of the input.
*/
function toArrayBuffer(buffer: Buffer): ArrayBuffer {
const arrayBuffer = new ArrayBuffer(buffer.length);
const arrayBufferView = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; i++) {
arrayBufferView[i] = buffer[i];
}
return arrayBuffer;
}

/**
* Converts an ArrayBuffer to a Node.js Buffer.
* @param arrayBuffer The ArrayBuffer input to covert.
* @return The Buffer representation of the input.
*/
function toBuffer(arrayBuffer: ArrayBuffer): Buffer {
const buf = Buffer.alloc(arrayBuffer.byteLength);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buf.length; ++i) {
buf[i] = view[i];
}
return buf;
}
55 changes: 53 additions & 2 deletions synth.metadata
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,66 @@
"git": {
"name": ".",
"remote": "https://github.com/googleapis/google-auth-library-nodejs.git",
"sha": "4830a5308d780822e884b0fb98fa605d3e7dc77b"
"sha": "0c8e086f3cad23efefb57418f1eeccba6674aaac"
}
},
{
"git": {
"name": "synthtool",
"remote": "https://github.com/googleapis/synthtool.git",
"sha": "5747555f7620113d9a2078a48f4c047a99d31b3e"
"sha": "05de3e1e14a0b07eab8b474e669164dbd31f81fb"
}
}
],
"generatedFiles": [
".eslintignore",
".eslintrc.json",
".gitattributes",
".github/ISSUE_TEMPLATE/bug_report.md",
".github/ISSUE_TEMPLATE/feature_request.md",
".github/ISSUE_TEMPLATE/support_request.md",
".github/PULL_REQUEST_TEMPLATE.md",
".github/publish.yml",
".github/release-please.yml",
".github/workflows/ci.yaml",
".kokoro/.gitattributes",
".kokoro/common.cfg",
".kokoro/continuous/node10/common.cfg",
".kokoro/continuous/node10/docs.cfg",
".kokoro/continuous/node10/test.cfg",
".kokoro/continuous/node12/common.cfg",
".kokoro/continuous/node12/lint.cfg",
".kokoro/continuous/node12/samples-test.cfg",
".kokoro/continuous/node12/system-test.cfg",
".kokoro/continuous/node12/test.cfg",
".kokoro/docs.sh",
".kokoro/lint.sh",
".kokoro/populate-secrets.sh",
".kokoro/presubmit/node10/common.cfg",
".kokoro/presubmit/node12/common.cfg",
".kokoro/presubmit/node12/samples-test.cfg",
".kokoro/presubmit/node12/system-test.cfg",
".kokoro/presubmit/node12/test.cfg",
".kokoro/publish.sh",
".kokoro/release/docs-devsite.cfg",
".kokoro/release/docs-devsite.sh",
".kokoro/release/docs.cfg",
".kokoro/release/docs.sh",
".kokoro/release/publish.cfg",
".kokoro/samples-test.sh",
".kokoro/system-test.sh",
".kokoro/test.bat",
".kokoro/test.sh",
".kokoro/trampoline.sh",
".mocharc.js",
".nycrc",
".prettierignore",
".prettierrc.js",
"CODE_OF_CONDUCT.md",
"CONTRIBUTING.md",
"LICENSE",
"api-extractor.json",
"renovate.json",
"samples/README.md"
]
}
Loading

0 comments on commit 26ab5e3

Please sign in to comment.