Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds crypto utils for AWS request signing #1046

Merged
merged 15 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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