Skip to content

Commit

Permalink
Moves closure sha384 into a new extension amp-crypto-polyfill for laz…
Browse files Browse the repository at this point in the history
…y load (ampproject#7006)

* Moves closure sha384 into a new extension amp-crypto-polyfill for lazy load.

* fix

* Update tests. Remove base64 polyfill from lib.

* Fix presubmit

* Address comments

* Fix test.
  • Loading branch information
lannka authored and mrjoro committed Apr 28, 2017
1 parent 4f5dd5c commit 6eea581
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 379 deletions.
2 changes: 1 addition & 1 deletion build-system/dep-check-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ exports.rules = [
filesMatching: '**/*.js',
mustNotDependOn: 'third_party/**/*.js',
whitelist: [
'extensions/amp-analytics/**/*.js->' +
'extensions/amp-crypto-polyfill/**/*.js->' +
'third_party/closure-library/sha384-generated.js',
'extensions/amp-mustache/0.1/amp-mustache.js->' +
'third_party/mustache/mustache.js',
Expand Down
1 change: 1 addition & 0 deletions build-system/tasks/presubmit-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ var forbiddenTermsSrcInclusive = {
'src/service/lightbox-manager-discovery.js',
'src/shadow-embed.js',
'extensions/amp-ad/0.1/amp-ad.js',
'extensions/amp-analytics/0.1/crypto-impl.js',
'extensions/amp-a4a/0.1/amp-a4a.js',
],
},
Expand Down
86 changes: 59 additions & 27 deletions extensions/amp-analytics/0.1/crypto-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,73 +14,91 @@
* limitations under the License.
*/

import * as lib from '../../../third_party/closure-library/sha384-generated';
import {fromClass} from '../../../src/service';
import {dev} from '../../../src/log';
import {getExistingServiceForWindow} from '../../../src/service';
import {extensionsFor} from '../../../src/extensions';
import {stringToBytes} from '../../../src/utils/bytes';
import {base64UrlEncodeFromBytes} from '../../../src/utils/base64';

/** @const {string} */
const TAG = 'Crypto';
const FALLBACK_MSG = 'SubtleCrypto failed, fallback to closure lib.';

export class Crypto {

/**
* @param {!Window} win
*/
constructor(win) {
/** @private {!Window} */
this.win_ = win;

/** @private @const {?webCrypto.SubtleCrypto} */
this.subtle_ = getSubtle(win);

/** @private {?Promise<{sha384: function((string|Uint8Array))}>} */
this.polyfillPromise_ = null;

if (!this.subtle_) {
this.loadPolyfill_();
}
}

/**
* Returns the SHA-384 hash of the input string in a number array.
* Input string cannot contain chars out of range [0,255].
* @param {string|!Uint8Array} input
* @returns {!Promise<!Array<number>>}
* @return {!Promise<!Uint8Array>}
* @throws {!Error} when input string contains chars out of range [0,255]
*/
sha384(input) {
if (this.subtle_) {
try {
return this.subtle_.digest({name: 'SHA-384'},
input instanceof Uint8Array ? input : stringToBytes(input))
// [].slice.call(Unit8Array) is a shim for Array.from(Unit8Array)
/** @param {?} buffer */
.then(buffer => [].slice.call(new Uint8Array(buffer)),
e => {
// Chrome doesn't allow the usage of Crypto API under
// non-secure origin: https://www.chromium.org/Home/chromium-security/prefer-secure-origins-for-powerful-new-features
if (e.message && e.message.indexOf('secure origin') < 0) {
// Log unexpected fallback.
dev().error(TAG, FALLBACK_MSG, e);
}
return lib.sha384(input);
});
} catch (e) {
dev().error(TAG, FALLBACK_MSG, e);
}
if (typeof input === 'string') {
input = stringToBytes(input);
}

// polyfill is (being) loaded,
// means native Crypto API is not available or failed before.
if (this.polyfillPromise_) {
return this.polyfillPromise_.then(polyfill => polyfill.sha384(input));
}
try {
return this.subtle_.digest({name: 'SHA-384'}, input)
/** @param {?} buffer */
.then(buffer => new Uint8Array(buffer),
e => {
// Chrome doesn't allow the usage of Crypto API under
// non-secure origin: https://www.chromium.org/Home/chromium-security/prefer-secure-origins-for-powerful-new-features
if (e.message && e.message.indexOf('secure origin') < 0) {
// Log unexpected fallback.
dev().error(TAG, FALLBACK_MSG, e);
}
return this.loadPolyfill_().then(() => this.sha384(input));
});
} catch (e) {
dev().error(TAG, FALLBACK_MSG, e);
return this.loadPolyfill_().then(() => this.sha384(input));
}
return Promise.resolve(lib.sha384(input));
}

/**
* Returns the SHA-384 hash of the input string in the format of web safe
* base64 (using -_. instead of +/=).
* Input string cannot contain chars out of range [0,255].
* @param {string|!Uint8Array} input
* @returns {!Promise<string>}
* @return {!Promise<string>}
* @throws {!Error} when input string contains chars out of range [0,255]
*/
sha384Base64(input) {
return this.sha384(input).then(buffer => {
return lib.base64(buffer);
});
return this.sha384(input).then(buffer => base64UrlEncodeFromBytes(buffer));
}

/**
* Returns a uniform hash of the input string as a float number in the range
* of [0, 1).
* Input string cannot contain chars out of range [0,255].
* @param {string|!Uint8Array} input
* @returns {!Promise<number>}
* @return {!Promise<number>}
*/
uniform(input) {
return this.sha384(input).then(buffer => {
Expand All @@ -93,6 +111,20 @@ export class Crypto {
return result;
});
}

/**
* Loads Crypto polyfill library.
* @return {!Promise<{sha384: function((string|Uint8Array))}>}
* @private
*/
loadPolyfill_() {
if (this.polyfillPromise_) {
return this.polyfillPromise_;
}
return this.polyfillPromise_ = extensionsFor(this.win_)
.loadExtension('amp-crypto-polyfill')
.then(() => getExistingServiceForWindow(this.win_, 'crypto-polyfill'));
}
}

function getSubtle(win) {
Expand Down
81 changes: 45 additions & 36 deletions extensions/amp-analytics/0.1/test/test-crypto-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@

import {Crypto} from '../crypto-impl';
import {Platform} from '../../../../src/service/platform-impl';
import * as lib from '../../../../third_party/closure-library/sha384-generated';
import * as sinon from 'sinon';
import {
installCryptoPolyfill,
} from '../../../../extensions/amp-crypto-polyfill/0.1/amp-crypto-polyfill';
import {
installExtensionsService,
} from '../../../../src/service/extensions-impl';

describe('crypto-impl', () => {
describes.realWin('crypto-impl', {}, env => {

let sandbox;
let win;
let crypto;

function uint8Array(array) {
const uint8Array = new Uint8Array(array.length);
Expand All @@ -31,8 +36,12 @@ describe('crypto-impl', () => {
return uint8Array;
}

function testSuite(descption, crypto) {
describe(descption, () => {
function testSuite(description, win) {
describe(description, () => {
beforeEach(() => {
crypto = createCrypto(win || env.win);
});

it('should hash "abc" in sha384', () => {
return crypto.sha384('abc').then(buffer => {
expect(buffer.length).to.equal(48);
Expand All @@ -43,7 +52,7 @@ describe('crypto-impl', () => {
});

it('should hash [1,2,3] in sha384', () => {
return crypto.sha384(uint8Array([1,2,3])).then(buffer => {
return crypto.sha384(uint8Array([1, 2, 3])).then(buffer => {
expect(buffer.length).to.equal(48);
expect(buffer[0]).to.equal(134);
expect(buffer[1]).to.equal(34);
Expand All @@ -62,8 +71,10 @@ describe('crypto-impl', () => {
});

it('should hash [1,2,3] in sha384', () => {
return expect(crypto.sha384Base64([1,2,3])).to.eventually.equal(
'hiKdxtL_vqxzgHRBVKpwApHAZDUqDb3He57T8sjh2sTcMlhn053f8dJim3o5PUf2');
return expect(crypto.sha384Base64(uint8Array([1, 2, 3])))
.to.eventually.equal(
'hiKdxtL_vqxzgHRBVKpwApHAZDUqDb3H' +
'e57T8sjh2sTcMlhn053f8dJim3o5PUf2');
});

it('should throw when input contains chars out of range [0,255]', () => {
Expand All @@ -80,41 +91,46 @@ describe('crypto-impl', () => {
});
}

function createCrypto(win) {
const extensions = installExtensionsService(win);
sandbox.stub(extensions, 'loadExtension', extensionId => {
expect(extensionId).to.equal('amp-crypto-polyfill');
installCryptoPolyfill(win);
return Promise.resolve();
});

return new Crypto(win);
}

function isModernChrome() {
const platform = new Platform(window);
return platform.isChrome() && platform.getMajorVersion() >= 37;
}

beforeEach(() => {
sandbox = sinon.sandbox.create();
});

afterEach(() => {
sandbox.restore();
win = env.win;
});

testSuite('with native crypto API', new Crypto(window));
testSuite('with crypto lib', new Crypto({}));
testSuite('with native crypto API rejects', new Crypto({
testSuite('with native crypto API');
testSuite('with crypto lib', {});
testSuite('with native crypto API rejects', {
crypto: {
subtle: {
digest: () => Promise.reject('Operation not supported'),
},
},
}));
testSuite('with native crypto API throws', new Crypto({
});
testSuite('with native crypto API throws', {
crypto: {
subtle: {
digest: () => {
throw new Error();
},
digest: () => {throw new Error();},
},
},
}));
});

it('native API result should exactly equal to crypto lib result', () => {
return Promise
.all([new Crypto(window).sha384('abc'), new Crypto({}).sha384('abc')])
.all([createCrypto(win).sha384('abc'), createCrypto({}).sha384('abc')])
.then(results => {
expect(results[0]).to.jsonEqual(results[1]);
});
Expand All @@ -123,28 +139,21 @@ describe('crypto-impl', () => {
// Run tests below only on browsers that we're confident about the existence
// of native Crypto API.
if (isModernChrome()) {
it('should not call closure lib when native API is available ' +
it('should not load closure lib when native API is available ' +
'(string input)', () => {
const nativeApiSpy = sandbox.spy(window.crypto.subtle, 'digest');
const libSpy = sandbox.spy(lib, 'sha384');
return new Crypto(window).sha384Base64('abc').then(hash => {
return new Crypto(win).sha384Base64('abc').then(hash => {
expect(hash).to.equal(
'ywB1P0WjXou1oD1pmsZQBycsMqsO3tFjGotgWkP_W-2AhgcroefMI1i67KE0yCWn');
expect(nativeApiSpy).to.have.been.calledOnce;
expect(libSpy).to.not.have.been.called;
});
});

it('should not call closure lib when native API is available ' +
it('should not load closure lib when native API is available ' +
'(Uint8Array input)', () => {
const nativeApiSpy = sandbox.spy(window.crypto.subtle, 'digest');
const libSpy = sandbox.spy(lib, 'sha384');
return new Crypto(window).sha384Base64(uint8Array([1,2,3])).then(hash => {
return new Crypto(win).sha384Base64(uint8Array([1,2,3])).then(hash => {
expect(hash).to.equal(
'hiKdxtL_vqxzgHRBVKpwApHAZDUqDb3He57T8sjh2sTcMlhn053f8dJim3o5PUf2');
expect(nativeApiSpy).to.have.been.calledOnce;
expect(libSpy).to.not.have.been.called;
});
});
}
});

24 changes: 24 additions & 0 deletions extensions/amp-crypto-polyfill/0.1/amp-crypto-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright 2017 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as lib from '../../../third_party/closure-library/sha384-generated';
import {getService} from '../../../src/service';

export function installCryptoPolyfill(win) {
getService(win, 'crypto-polyfill', () => lib);
}

installCryptoPolyfill(window);
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ declareExtension('amp-brid-player', '0.1', false);
declareExtension('amp-brightcove', '0.1', false);
declareExtension('amp-kaltura-player', '0.1', false);
declareExtension('amp-carousel', '0.1', true);
declareExtension('amp-crypto-polyfill', '0.1', false);
declareExtension('amp-dailymotion', '0.1', false);
declareExtension('amp-dynamic-css-classes', '0.1', false, 'NO_TYPE_CHECK');
declareExtension('amp-experiment', '0.1', false, 'NO_TYPE_CHECK');
Expand Down
15 changes: 15 additions & 0 deletions test/functional/test-cid.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ import {timerFor} from '../../src/timer';
import {installPlatformService} from '../../src/service/platform-impl';
import {installViewerServiceForDoc} from '../../src/service/viewer-impl';
import {installTimerService} from '../../src/service/timer-impl';
import {
installCryptoPolyfill,
} from '../../extensions/amp-crypto-polyfill/0.1/amp-crypto-polyfill';
import {
installExtensionsService,
} from '../../src/service/extensions-impl';
import * as sinon from 'sinon';

const DAY = 24 * 3600 * 1000;
Expand Down Expand Up @@ -89,6 +95,15 @@ describe('cid', () => {
ampdoc = ampdocService.getAmpDoc();
installTimerService(fakeWin);
installPlatformService(fakeWin);

// stub extensions service to provide crypto-polyfill
const extensions = installExtensionsService(fakeWin);
sandbox.stub(extensions, 'loadExtension', extensionId => {
expect(extensionId).to.equal('amp-crypto-polyfill');
installCryptoPolyfill(fakeWin);
return Promise.resolve();
});

const viewer = installViewerServiceForDoc(ampdoc);
sandbox.stub(viewer, 'whenFirstVisible', function() {
return whenFirstVisible;
Expand Down
Loading

0 comments on commit 6eea581

Please sign in to comment.