From c12b3d07280ca3f7cd1105dc09ffe46312aea9c4 Mon Sep 17 00:00:00 2001 From: lannka Date: Thu, 30 Jun 2016 13:50:59 -0700 Subject: [PATCH 1/6] Leverage native crypto API to hash strings. Fallback to closure lib when native API is not available or failed to execute. --- extensions/amp-analytics/0.1/crypto-impl.js | 43 ++++++++++-- .../0.1/test/test-crypto-impl.js | 66 ++++++++++++++----- third_party/closure-library/compile.sh | 3 +- .../closure-library/sha384-generated.js | 35 +++++----- third_party/closure-library/sha384.js | 8 +-- 5 files changed, 110 insertions(+), 45 deletions(-) diff --git a/extensions/amp-analytics/0.1/crypto-impl.js b/extensions/amp-analytics/0.1/crypto-impl.js index d7609acd8e56..574ae3efa55b 100644 --- a/extensions/amp-analytics/0.1/crypto-impl.js +++ b/extensions/amp-analytics/0.1/crypto-impl.js @@ -20,29 +20,62 @@ import {getService} from '../../../src/service'; export class Crypto { constructor(win) { - this.win = win; + if (win.crypto) { + this.subtle = win.crypto.subtle || win.crypto.webkitSubtle; + } } /** - * Returns the SHA-384 hash of the input string in an ArrayBuffer. + * 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} str - * @returns {!Promise} + * @returns {!Promise>} + * @throws {!Error} when input string contains chars out of range [0,255] */ sha384(str) { + if (this.subtle) { + try { + return this.subtle.digest('SHA-384', str2ab(str)) + // [].slice.call(Unit8Array) is a shim for Array.from(Unit8Array) + .then(buffer => [].slice.call(new Uint8Array(buffer)), + () => lib.sha384(str)); + } catch (e) { + // ignore, fallback to closure lib. + } + } return Promise.resolve(lib.sha384(str)); } /** * Returns the SHA-384 hash of the input string in the format of web safe - * base64 string (using -_. instead of +/=). + * base64 (using -_. instead of +/=). + * Input string cannot contain chars out of range [0,255]. * @param {string} str * @returns {!Promise} + * @throws {!Error} when input string contains chars out of range [0,255] */ sha384Base64(str) { - return Promise.resolve(lib.sha384Base64(str)); + return this.sha384(str).then(buffer => { + return lib.base64(buffer); + }); } } +// A shim for TextEncoder +function str2ab(str) { + const buf = new ArrayBuffer(str.length); + const bufView = new Uint8Array(buf); + for (let i = 0; i < str.length; i++) { + // Apply the same check as in closure lib: + // https://github.com/google/closure-library/blob/master/closure/goog/crypt/sha2_64bit.js#L169 + if (str.charCodeAt(i) > 255) { + throw Error('Characters must be in range [0,255]'); + } + bufView[i] = str.charCodeAt(i); + } + return buf; +} + export function installCryptoService(win) { return getService(win, 'crypto', () => { return new Crypto(win); diff --git a/extensions/amp-analytics/0.1/test/test-crypto-impl.js b/extensions/amp-analytics/0.1/test/test-crypto-impl.js index 2a12d3a5f293..c8f36cd94265 100644 --- a/extensions/amp-analytics/0.1/test/test-crypto-impl.js +++ b/extensions/amp-analytics/0.1/test/test-crypto-impl.js @@ -18,28 +18,58 @@ import {Crypto} from '../crypto-impl'; describe('crypto-impl', () => { - let crypto; + function testSuite(descption, crypto) { + describe(descption, () => { + it('should hash "abc" in sha384', () => { + return crypto.sha384('abc').then(buffer => { + expect(buffer.length).to.equal(48); + expect(buffer[0]).to.equal(203); + expect(buffer[1]).to.equal(0); + expect(buffer[47]).to.equal(167); + }); + }); + + it('should hash "abc" in sha384Base64', () => { + return expect(crypto.sha384Base64('abc')).to.eventually.equal( + 'ywB1P0WjXou1oD1pmsZQBycsMqsO3tFjGotgWkP_W-2AhgcroefMI1i67KE0yCWn'); + }); - beforeEach(() => { - crypto = new Crypto({}); - }); + it('should hash "foobar" in sha384Base64', () => { + return expect(crypto.sha384Base64('foobar')).to.eventually.equal( + 'PJww2fZl501RXIQpYNSkUcg6ASX9Pec5LXs3IxrxDHLqWK7fzfiaV2W_kCr5Ps8G'); + }); - it('should hash "abc" in sha384', () => { - return crypto.sha384('abc').then(buffer => { - expect(buffer.length).to.equal(48); - expect(buffer[0]).to.equal(203); - expect(buffer[1]).to.equal(0); - expect(buffer[47]).to.equal(167); + it('should throw when input contains chars out of range [0,255]', () => { + expect(() => crypto.sha384('abc今')).to.throw(); + expect(() => crypto.sha384Base64('abc今')).to.throw(); + }); }); - }); + } - it('should hash "abc" in sha384Base64', () => { - return expect(crypto.sha384Base64('abc')).to.eventually.equal( - 'ywB1P0WjXou1oD1pmsZQBycsMqsO3tFjGotgWkP_W-2AhgcroefMI1i67KE0yCWn'); - }); + testSuite('with native crypto API', new Crypto(window)); + testSuite('with crypto lib', new Crypto({})); + testSuite('with native crypto API rejects', new Crypto({ + crypto: { + subtle: { + digest: () => Promise.reject('Operation not supported'), + }, + }, + })); + testSuite('with native crypto API throws', new Crypto({ + crypto: { + subtle: { + digest: () => { + throw new Error(); + }, + }, + }, + })); - it('should hash "foobar" in sha384Base64', () => { - return expect(crypto.sha384Base64('foobar')).to.eventually.equal( - 'PJww2fZl501RXIQpYNSkUcg6ASX9Pec5LXs3IxrxDHLqWK7fzfiaV2W_kCr5Ps8G'); + it('native API result should exactly equal to crypto lib result', () => { + return Promise + .all([new Crypto(window).sha384('abc'), new Crypto({}).sha384('abc')]) + .then(results => { + expect(results[0]).to.deep.equal(results[1]); + }); }); }); diff --git a/third_party/closure-library/compile.sh b/third_party/closure-library/compile.sh index 93826bc13ccc..a321e2bad659 100644 --- a/third_party/closure-library/compile.sh +++ b/third_party/closure-library/compile.sh @@ -26,9 +26,10 @@ java -jar $1 \ --js "$2/goog/debug/error.js" \ --js "$2/goog/dom/nodetype.js" \ --js "$2/goog/math/long.js" \ + --js "$2/goog/reflect/reflect.js" \ --js "$2/goog/string/string.js" \ --js_output_file "sha384-generated.js" \ - --output_wrapper "/* Generated from closure library commit $GIT_COMMIT */%output%;export function sha384Base64(input) { return ampSha384(input) }; export function sha384(input) { return ampSha384Digest(input) };" \ + --output_wrapper "/* Generated from closure library commit $GIT_COMMIT */%output%;export function base64(input) { return ampBase64(input) }; export function sha384(input) { return ampSha384Digest(input) };" \ --manage_closure_dependencies \ --process_closure_primitives \ --use_types_for_optimization \ diff --git a/third_party/closure-library/sha384-generated.js b/third_party/closure-library/sha384-generated.js index 60de003177e8..e2676e92c1af 100644 --- a/third_party/closure-library/sha384-generated.js +++ b/third_party/closure-library/sha384-generated.js @@ -1,19 +1,20 @@ -/* Generated from closure library commit 4fa3f37e090d73374825faec334b2deb2c902c47 */var m=this;function p(a,b){var d=a.split("."),c=window||m;d[0]in c||!c.execScript||c.execScript("var "+d[0]);for(var e;d.length&&(e=d.shift());)d.length||void 0===b?c[e]?c=c[e]:c=c[e]={}:c[e]=b} -function aa(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var d=Object.prototype.toString.call(a);if("[object Window]"==d)return"object";if("[object Array]"==d||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==d||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null"; -else if("function"==b&&"undefined"==typeof a.call)return"object";return b}function r(a,b){function d(){}d.prototype=b.prototype;a.v=b.prototype;a.prototype=new d;a.prototype.constructor=a;a.u=function(a,d,f){for(var g=Array(arguments.length-2),l=2;la){var b=u[a];if(b)return b}b=new t(a|0,0>a?-1:0);-128<=a&&128>a&&(u[a]=b);return b}function z(a){isNaN(a)||!isFinite(a)?a=A():a<=-B?a=C():a+1>=B?(w[D]||(w[D]=new t(-1,2147483647)),a=w[D]):a=0>a?E(z(-a)):new t(a%F|0,a/F|0);return a}var F=4294967296,B=F*F/2;function A(){w[G]||(w[G]=y(0));return w[G]}function H(){w[I]||(w[I]=y(1));return w[I]}function ca(){w[J]||(w[J]=y(-1));return w[J]} -function C(){w[K]||(w[K]=new t(0,-2147483648));return w[K]}t.prototype.toString=function(a){a=a||10;if(2>a||36this.a){if(M(this,C())){var b=z(a),d=N(this,b),b=O(P(d,b),this);return d.toString(a)+b.b.toString(a)}return"-"+E(this).toString(a)}for(var d=z(Math.pow(a,6)),b=this,c="";;){var e=N(b,d),f=(O(b,P(e,d)).b>>>0).toString(a),b=e;if(L(b))return f+c;for(;6>f.length;)f="0"+f;c=""+f+c}};function Q(a){return 0<=a.b?a.b:F+a.b} -function L(a){return 0==a.a&&0==a.b}function M(a,b){return a.a==b.a&&a.b==b.b}function da(a){w[R]||(w[R]=y(16777216));return 0>S(a,w[R])}function S(a,b){if(M(a,b))return 0;var d=0>a.a,c=0>b.a;return d&&!c?-1:!d&&c?1:0>O(a,b).a?-1:1}function E(a){return M(a,C())?C():T(new t(~a.b,~a.a),H())} -function T(a,b){var d=a.a>>>16,c=a.a&65535,e=a.b>>>16,f=b.a>>>16,g=b.a&65535,l=b.b>>>16,n,q;q=0+((a.b&65535)+(b.b&65535));n=0+(q>>>16);n+=e+l;e=0+(n>>>16);e+=c+g;c=0+(e>>>16);c=c+(d+f)&65535;return new t((n&65535)<<16|q&65535,c<<16|e&65535)}function O(a,b){return T(a,E(b))} -function P(a,b){if(L(a)||L(b))return A();if(M(a,C()))return 1==(b.b&1)?C():A();if(M(b,C()))return 1==(a.b&1)?C():A();if(0>a.a)return 0>b.a?P(E(a),E(b)):E(P(E(a),b));if(0>b.a)return E(P(a,E(b)));if(da(a)&&da(b))return z((a.a*F+Q(a))*(b.a*F+Q(b)));var d=a.a>>>16,c=a.a&65535,e=a.b>>>16,f=a.b&65535,g=b.a>>>16,l=b.a&65535,n=b.b>>>16,q=b.b&65535,v,k,h,x;x=0+f*q;h=0+(x>>>16);h+=e*q;k=0+(h>>>16);h=(h&65535)+f*n;k+=h>>>16;h&=65535;k+=c*q;v=0+(k>>>16);k=(k&65535)+e*n;v+=k>>>16;k&=65535;k+=f*l;v+=k>>>16;k&= -65535;v=v+(d*q+c*n+e*l+f*g)&65535;return new t(h<<16|x&65535,v<<16|k)} -function N(a,b){if(L(b))throw Error("division by zero");if(L(a))return A();if(M(a,C())){if(M(b,H())||M(b,ca()))return C();if(M(b,C()))return H();var d;d=1;if(0==d)d=a;else{var c=a.a;d=32>d?new t(a.b>>>d|c<<32-d,c>>d):new t(c>>d-32,0<=c?0:-1)}d=N(d,b);c=1;if(0!=c){var e=d.b;d=32>c?new t(e<>>32-c):new t(0,e<b.a?H():ca();c=O(a,P(b,d));return T(d,N(c,b))}if(M(b,C()))return A();if(0>a.a)return 0>b.a?N(E(a),E(b)):E(N(E(a),b));if(0>b.a)return E(N(a,E(b)));e=A();for(c= -a;0<=S(c,b);){d=Math.max(1,Math.floor((c.a*F+Q(c))/(b.a*F+Q(b))));for(var f=Math.ceil(Math.log(d)/Math.LN2),f=48>=f?1:Math.pow(2,f-48),g=z(d),l=P(g,b);0>l.a||0X;X++)ha[X]=0;var ia=function(a){return Array.prototype.concat.apply(Array.prototype,arguments)}([128],ha); -function Y(a,b,d){d=void 0!==d?d:b.length;if(a.i)throw Error("this hasher needs to be reset");var c=a.f;if("string"==typeof b)for(var e=0;ef||255c;c++){var e=8*c;d[c]=new t(b[e+4]<<24|b[e+5]<<16|b[e+6]<<8|b[e+7],b[e]<<24|b[e+1]<<16|b[e+2]<<8|b[e+3])}for(c=16;80>c;c++){var e=d[c-15],b=e.b,e=e.a,f=d[c-2],g=f.b,f=f.a;d[c]=a.m(d[c-16],d[c-7],new t(b>>>1^e<<31^b>>>8^e<<24^b>>>7^e<<25,e>>>1^b<<31^e>>>8^b<<24^e>>>7),new t(g>>>19^f<<13^f>>>29^g<<3^g>>>6^f<<26,f>>>19^g<<13^g>>>29^f<<3^f>>>6))}for(var b=a.c[0],e=a.c[1],g=a.c[2],f=a.c[3],l=a.c[4],n=a.c[5],q=a.c[6],v=a.c[7],c=0;80>c;c++)var k=b.b,h=b.a,k=T(new t(k>>> -28^h<<4^h>>>2^k<<30^h>>>7^k<<25,h>>>28^k<<4^k>>>2^h<<30^k>>>7^h<<25),new t(b.b&e.b|e.b&g.b|b.b&g.b,b.a&e.a|e.a&g.a|b.a&g.a)),h=l.b,x=l.a,fa=l.b,ga=l.a,h=a.m(v,new t(h>>>14^x<<18^h>>>18^x<<14^x>>>9^h<<23,x>>>14^h<<18^x>>>18^h<<14^h>>>9^x<<23),new t(fa&n.b|~fa&q.b,ga&n.a|~ga&q.a),ja[c],d[c]),v=q,q=n,n=l,l=T(f,h),f=g,g=e,e=b,b=T(h,k);a.c[0]=T(a.c[0],b);a.c[1]=T(a.c[1],e);a.c[2]=T(a.c[2],g);a.c[3]=T(a.c[3],f);a.c[4]=T(a.c[4],l);a.c[5]=T(a.c[5],n);a.c[6]=T(a.c[6],q);a.c[7]=T(a.c[7],v)} -W.prototype.m=function(a,b,d){for(var c=(a.b^2147483648)+(b.b^2147483648),e=a.a+b.a,f=arguments.length-1;2<=f;--f)c+=arguments[f].b^2147483648,e+=arguments[f].a;arguments.length&1&&(c+=2147483648);e+=arguments.length>>1;e+=Math.floor(c/4294967296);return new t(c,e)};function ea(a){for(var b=[],d=0;da?r(ca,a,function(a){return new u(a|0,0>a?-1:0)}):new u(a|0,0>a?-1:0)}function y(a){return isNaN(a)?z():a<=-A?B():a+1>=A?da():0>a?C(y(-a)):new u(a%D|0,a/D|0)}var D=4294967296,A=D*D/2;function z(){return r(w,ea,function(){return x(0)})}function F(){return r(w,fa,function(){return x(1)})}function G(){return r(w,ga,function(){return x(-1)})}function da(){return r(w,ha,function(){return new u(-1,2147483647)})} +function B(){return r(w,ia,function(){return new u(0,-2147483648)})}function H(){return r(w,ja,function(){return x(16777216)})} +u.prototype.toString=function(a){a=a||10;if(2>a||36this.a){if(J(this,B())){var b=y(a),c=K(this,b),b=L(M(c,b),C(this));return c.toString(a)+b.b.toString(a)}return"-"+C(this).toString(a)}for(var c=y(Math.pow(a,6)),b=this,d="";;){var e=K(b,c),f=(L(b,C(M(e,c))).b>>>0).toString(a),b=e;if(I(b))return f+d;for(;6>f.length;)f="0"+f;d=""+f+d}};function N(a){return 0<=a.b?a.b:D+a.b}function I(a){return 0==a.a&&0==a.b} +function J(a,b){return a.a==b.a&&a.b==b.b}function O(a,b){if(J(a,b))return 0;var c=0>a.a,d=0>b.a;return c&&!d?-1:!c&&d?1:0>L(a,C(b)).a?-1:1}function C(a){return J(a,B())?B():L(new u(~a.b,~a.a),F())}function L(a,b){var c=a.a>>>16,d=a.a&65535,e=a.b>>>16,f=b.a>>>16,g=b.a&65535,h=b.b>>>16;a=0+((a.b&65535)+(b.b&65535));h=0+(a>>>16)+(e+h);e=0+(h>>>16);e+=d+g;c=0+(e>>>16)+(c+f)&65535;return new u((h&65535)<<16|a&65535,c<<16|e&65535)} +function M(a,b){if(I(a)||I(b))return z();if(J(a,B()))return 1==(b.b&1)?B():z();if(J(b,B()))return 1==(a.b&1)?B():z();if(0>a.a)return 0>b.a?M(C(a),C(b)):C(M(C(a),b));if(0>b.a)return C(M(a,C(b)));if(0>O(a,H())&&0>O(b,H()))return y((a.a*D+N(a))*(b.a*D+N(b)));var c=a.a>>>16,d=a.a&65535,e=a.b>>>16;a=a.b&65535;var f=b.a>>>16,g=b.a&65535,h=b.b>>>16;b=b.b&65535;var p,n,v,t;t=0+a*b;v=0+(t>>>16)+e*b;n=0+(v>>>16);v=(v&65535)+a*h;n+=v>>>16;n+=d*b;p=0+(n>>>16);n=(n&65535)+e*h;p+=n>>>16;n=(n&65535)+a*g;p=p+(n>>> +16)+(c*b+d*h+e*g+a*f)&65535;return new u((v&65535)<<16|t&65535,p<<16|n&65535)} +function K(a,b){if(I(b))throw Error("division by zero");if(I(a))return z();if(J(a,B())){if(J(b,F())||J(b,G()))return B();if(J(b,B()))return F();var c;c=1;if(0==c)c=a;else{var d=a.a;c=32>c?new u(a.b>>>c|d<<32-c,d>>c):new u(d>>c-32,0<=d?0:-1)}c=K(c,b);d=1;if(0!=d){var e=c.b;c=32>d?new u(e<>>32-d):new u(0,e<b.a?F():G();a=L(a,C(M(b,c)));return L(c,K(a,b))}if(J(b,B()))return z();if(0>a.a)return 0>b.a?K(C(a),C(b)):C(K(C(a),b));if(0>b.a)return C(K(a,C(b)));for(d=z();0<= +O(a,b);){c=Math.max(1,Math.floor((a.a*D+N(a))/(b.a*D+N(b))));for(var e=Math.ceil(Math.log(c)/Math.LN2),e=48>=e?1:Math.pow(2,e-48),f=y(c),g=M(f,b);0>g.a||0S;S++)R[S]=0;var T=function(a){return Array.prototype.concat.apply(Array.prototype,arguments)}([128],R); +function U(a,b,c){c=void 0!==c?c:b.length;if(a.i)throw Error("this hasher needs to be reset");var d=a.f;if("string"==typeof b)for(var e=0;ef||255d;d++){var e=8*d;c[d]=new u(b[e+4]<<24|b[e+5]<<16|b[e+6]<<8|b[e+7],b[e]<<24|b[e+1]<<16|b[e+2]<<8|b[e+3])}for(d=16;80>d;d++){var e=c[d-15],b=e.b,e=e.a,f=c[d-2],g=f.b,f=f.a;c[d]=a.m(c[d-16],c[d-7],new u(b>>>1^e<<31^b>>>8^e<<24^b>>>7^e<<25,e>>>1^b<<31^e>>>8^b<<24^e>>>7),new u(g>>>19^f<<13^f>>>29^g<<3^g>>>6^f<<26,f>>>19^g<<13^g>>>29^f<<3^f>>>6))}for(var b=a.c[0],e=a.c[1],g=a.c[2],f=a.c[3],h=a.c[4],p=a.c[5],n=a.c[6],v=a.c[7],d=0;80>d;d++)var t=b.b,q=b.a,t=L(new u(t>>> +28^q<<4^q>>>2^t<<30^q>>>7^t<<25,q>>>28^t<<4^t>>>2^q<<30^t>>>7^q<<25),new u(b.b&e.b|e.b&g.b|b.b&g.b,b.a&e.a|e.a&g.a|b.a&g.a)),q=h.b,E=h.a,Y=h.b,Z=h.a,q=a.m(v,new u(q>>>14^E<<18^q>>>18^E<<14^E>>>9^q<<23,E>>>14^q<<18^E>>>18^q<<14^q>>>9^E<<23),new u(Y&p.b|~Y&n.b,Z&p.a|~Z&n.a),ka[d],c[d]),v=n,n=p,p=h,h=L(f,q),f=g,g=e,e=b,b=L(q,t);a.c[0]=L(a.c[0],b);a.c[1]=L(a.c[1],e);a.c[2]=L(a.c[2],g);a.c[3]=L(a.c[3],f);a.c[4]=L(a.c[4],h);a.c[5]=L(a.c[5],p);a.c[6]=L(a.c[6],n);a.c[7]=L(a.c[7],v)} +P.prototype.m=function(a,b,c){for(var d=(a.b^2147483648)+(b.b^2147483648),e=a.a+b.a,f=arguments.length-1;2<=f;--f)d+=arguments[f].b^2147483648,e+=arguments[f].a;arguments.length&1&&(d+=2147483648);e+=arguments.length>>1;e+=Math.floor(d/4294967296);return new u(d,e)};function Q(a){for(var b=[],c=0;cb.f?Y(b,ia,112-b.f):Y(b,ia,b.g-b.f+112);for(a=127;112<=a;a--)b.h[a]=d&255,d/=256;Z(b);var d=0,c=Array(8*b.l);for(a=0;a>g&255;for(g=24;0<=g;g-=8)c[d++]=e>>g&255}b.i=!0;return c} -p("ampSha384",function(a){a=ma(a);if(!U){U={};V={};for(var b=0;65>b;b++)U[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(b),V[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.".charAt(b)}for(var b=V,d=[],c=0;c>2,e=(e&3)<<4|g>>4,g=(g&15)<<2|n>>6,n=n&63;l||(n=64,f||(g=64));d.push(b[q],b[e],b[g],b[n])}return d.join("")});p("ampSha384Digest",ma);;export function sha384Base64(input) { return ampSha384(input) }; export function sha384(input) { return ampSha384Digest(input) }; +289380356,3203993006,460393269,320620315,685471733,587496836,852142971,1086792851,1017036298,365543100,1126000580,2618297676,1288033470,3409855158,1501505948,4234509866,1607167915,987167468,1816402316,1246189591]);function la(){P.call(this,6,ma)}m(la,P);var ma=[3418070365,3238371032,1654270250,914150663,2438529370,812702999,355462360,4144912697,1731405415,4290775857,2394180231,1750603025,3675008525,1694076839,1203062813,3204075428];var W=null,X=null;l("ampBase64",function(a){if(!W){W={};X={};for(var b=0;65>b;b++)W[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(b),X[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.".charAt(b)}for(var b=X,c=[],d=0;d>2,e=(e&3)<<4|g>>4,g=(g&15)<<2|p>>6,p=p&63;h||(p=64,f||(g=64));c.push(b[n],b[e],b[g],b[p])}return c.join("")}); +l("ampSha384Digest",function(a){var b=new la;U(b,a);if(b.i)throw Error("this hasher needs to be reset");var c=8*b.j;112>b.f?U(b,T,112-b.f):U(b,T,b.g-b.f+112);for(a=127;112<=a;a--)b.h[a]=c&255,c/=256;V(b);var c=0,d=Array(8*b.l);for(a=0;a>g&255;for(g=24;0<=g;g-=8)d[c++]=e>>g&255}b.i=!0;return d});;export function base64(input) { return ampBase64(input) }; export function sha384(input) { return ampSha384Digest(input) }; diff --git a/third_party/closure-library/sha384.js b/third_party/closure-library/sha384.js index 7124e6412286..0ba7bcb767b8 100644 --- a/third_party/closure-library/sha384.js +++ b/third_party/closure-library/sha384.js @@ -28,12 +28,12 @@ var digest = function(input) { } /** - * @param {!Uint8Array|string} input The value to hash. + * @param {!Uint8Array} input The value to hash. * @return {string} Web safe base64 of the digest of the input string. */ -var base64Digest = function(input) { - return goog.crypt.base64.encodeByteArray(digest(input), /* websafe */ true); +var base64 = function(input) { + return goog.crypt.base64.encodeByteArray(input, /* websafe */ true); } -goog.exportSymbol('ampSha384', base64Digest, window); +goog.exportSymbol('ampBase64', base64, window); goog.exportSymbol('ampSha384Digest', digest, window); From 1b18655cbc3cab76bea7979e4b93174630e013e2 Mon Sep 17 00:00:00 2001 From: lannka Date: Thu, 30 Jun 2016 15:49:40 -0700 Subject: [PATCH 2/6] Address comments. --- extensions/amp-analytics/0.1/crypto-impl.js | 29 ++++++++++++++----- .../0.1/test/test-crypto-impl.js | 23 ++++++++++++++- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/extensions/amp-analytics/0.1/crypto-impl.js b/extensions/amp-analytics/0.1/crypto-impl.js index 574ae3efa55b..d1b2b3c7a88c 100644 --- a/extensions/amp-analytics/0.1/crypto-impl.js +++ b/extensions/amp-analytics/0.1/crypto-impl.js @@ -16,13 +16,16 @@ import * as lib from '../../../third_party/closure-library/sha384-generated'; import {getService} from '../../../src/service'; +import {dev} from '../../../src/log'; + +/** @const {string} */ +const TAG = 'Crypto'; export class Crypto { constructor(win) { - if (win.crypto) { - this.subtle = win.crypto.subtle || win.crypto.webkitSubtle; - } + /** @private @const {?SubtleCrypto} */ + this.subtle = getSubtle(win); } /** @@ -38,9 +41,13 @@ export class Crypto { return this.subtle.digest('SHA-384', str2ab(str)) // [].slice.call(Unit8Array) is a shim for Array.from(Unit8Array) .then(buffer => [].slice.call(new Uint8Array(buffer)), - () => lib.sha384(str)); + e => { + dev.info(TAG, 'Crypto digest promise has rejected, ' + + 'fallback to closure lib.', e); + return lib.sha384(str); + }); } catch (e) { - // ignore, fallback to closure lib. + dev.info(TAG, 'Crypto digest has thrown, fallback to closure lib.', e); } } return Promise.resolve(lib.sha384(str)); @@ -61,17 +68,23 @@ export class Crypto { } } +function getSubtle(win) { + if (!win.crypto) { + return null; + } + return win.crypto.subtle || win.crypto.webkitSubtle || null; +} + // A shim for TextEncoder function str2ab(str) { - const buf = new ArrayBuffer(str.length); - const bufView = new Uint8Array(buf); + const buf = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) { // Apply the same check as in closure lib: // https://github.com/google/closure-library/blob/master/closure/goog/crypt/sha2_64bit.js#L169 if (str.charCodeAt(i) > 255) { throw Error('Characters must be in range [0,255]'); } - bufView[i] = str.charCodeAt(i); + buf[i] = str.charCodeAt(i); } return buf; } diff --git a/extensions/amp-analytics/0.1/test/test-crypto-impl.js b/extensions/amp-analytics/0.1/test/test-crypto-impl.js index c8f36cd94265..0ce370a6c211 100644 --- a/extensions/amp-analytics/0.1/test/test-crypto-impl.js +++ b/extensions/amp-analytics/0.1/test/test-crypto-impl.js @@ -15,6 +15,9 @@ */ import {Crypto} from '../crypto-impl'; +import {Platform} from '../../../../src/platform'; +import * as lib from '../../../../third_party/closure-library/sha384-generated'; +import * as sinon from 'sinon'; describe('crypto-impl', () => { @@ -69,7 +72,25 @@ describe('crypto-impl', () => { return Promise .all([new Crypto(window).sha384('abc'), new Crypto({}).sha384('abc')]) .then(results => { - expect(results[0]).to.deep.equal(results[1]); + expect(results[0]).to.jsonEqual(results[1]); }); }); + + it('should not call closure lib when native API is available', () => { + const platform = new Platform(window); + if (!platform.isChrome() || platform.getMajorVersion() < 48) { + // Run this test only on browsers that we're confident about the existence + // of native Crypto API. + return this.skip(); + } + + const nativeApiSpy = sinon.spy(window.crypto.subtle, 'digest'); + const libSpy = sinon.spy(lib, 'sha384'); + return new Crypto(window).sha384Base64('abc').then(hash => { + expect(hash).to.equal( + 'ywB1P0WjXou1oD1pmsZQBycsMqsO3tFjGotgWkP_W-2AhgcroefMI1i67KE0yCWn'); + expect(nativeApiSpy).to.have.been.calledOnce; + expect(libSpy).to.not.have.been.called; + }); + }); }); From 379ccb626d71fc8a6b456252071ad9d1e36addf4 Mon Sep 17 00:00:00 2001 From: lannka Date: Thu, 30 Jun 2016 16:16:51 -0700 Subject: [PATCH 3/6] Address comments --- extensions/amp-analytics/0.1/crypto-impl.js | 6 +++--- extensions/amp-analytics/0.1/test/test-crypto-impl.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/amp-analytics/0.1/crypto-impl.js b/extensions/amp-analytics/0.1/crypto-impl.js index d1b2b3c7a88c..164099be3cdc 100644 --- a/extensions/amp-analytics/0.1/crypto-impl.js +++ b/extensions/amp-analytics/0.1/crypto-impl.js @@ -25,7 +25,7 @@ export class Crypto { constructor(win) { /** @private @const {?SubtleCrypto} */ - this.subtle = getSubtle(win); + this.subtle_ = getSubtle(win); } /** @@ -36,9 +36,9 @@ export class Crypto { * @throws {!Error} when input string contains chars out of range [0,255] */ sha384(str) { - if (this.subtle) { + if (this.subtle_) { try { - return this.subtle.digest('SHA-384', str2ab(str)) + return this.subtle_.digest('SHA-384', str2ab(str)) // [].slice.call(Unit8Array) is a shim for Array.from(Unit8Array) .then(buffer => [].slice.call(new Uint8Array(buffer)), e => { diff --git a/extensions/amp-analytics/0.1/test/test-crypto-impl.js b/extensions/amp-analytics/0.1/test/test-crypto-impl.js index 0ce370a6c211..94c88df68d9d 100644 --- a/extensions/amp-analytics/0.1/test/test-crypto-impl.js +++ b/extensions/amp-analytics/0.1/test/test-crypto-impl.js @@ -31,7 +31,7 @@ describe('crypto-impl', () => { expect(buffer[47]).to.equal(167); }); }); - + it('should hash "abc" in sha384Base64', () => { return expect(crypto.sha384Base64('abc')).to.eventually.equal( 'ywB1P0WjXou1oD1pmsZQBycsMqsO3tFjGotgWkP_W-2AhgcroefMI1i67KE0yCWn'); @@ -75,7 +75,7 @@ describe('crypto-impl', () => { expect(results[0]).to.jsonEqual(results[1]); }); }); - + it('should not call closure lib when native API is available', () => { const platform = new Platform(window); if (!platform.isChrome() || platform.getMajorVersion() < 48) { From bc98d0b593a468a319247c92886f21fc44d00a3b Mon Sep 17 00:00:00 2001 From: lannka Date: Thu, 30 Jun 2016 17:09:40 -0700 Subject: [PATCH 4/6] Fix test skip. --- .../0.1/test/test-crypto-impl.js | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/extensions/amp-analytics/0.1/test/test-crypto-impl.js b/extensions/amp-analytics/0.1/test/test-crypto-impl.js index 94c88df68d9d..0ed72cc15186 100644 --- a/extensions/amp-analytics/0.1/test/test-crypto-impl.js +++ b/extensions/amp-analytics/0.1/test/test-crypto-impl.js @@ -49,6 +49,11 @@ describe('crypto-impl', () => { }); } + function isModernChrome() { + const platform = new Platform(window); + return platform.isChrome() && platform.getMajorVersion() >= 45; + } + testSuite('with native crypto API', new Crypto(window)); testSuite('with crypto lib', new Crypto({})); testSuite('with native crypto API rejects', new Crypto({ @@ -76,21 +81,18 @@ describe('crypto-impl', () => { }); }); - it('should not call closure lib when native API is available', () => { - const platform = new Platform(window); - if (!platform.isChrome() || platform.getMajorVersion() < 48) { - // Run this test only on browsers that we're confident about the existence - // of native Crypto API. - return this.skip(); - } - - const nativeApiSpy = sinon.spy(window.crypto.subtle, 'digest'); - const libSpy = sinon.spy(lib, 'sha384'); - return new Crypto(window).sha384Base64('abc').then(hash => { - expect(hash).to.equal( - 'ywB1P0WjXou1oD1pmsZQBycsMqsO3tFjGotgWkP_W-2AhgcroefMI1i67KE0yCWn'); - expect(nativeApiSpy).to.have.been.calledOnce; - expect(libSpy).to.not.have.been.called; + // Run this test 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', () => { + const nativeApiSpy = sinon.spy(window.crypto.subtle, 'digest'); + const libSpy = sinon.spy(lib, 'sha384'); + return new Crypto(window).sha384Base64('abc').then(hash => { + expect(hash).to.equal( + 'ywB1P0WjXou1oD1pmsZQBycsMqsO3tFjGotgWkP_W-2AhgcroefMI1i67KE0yCWn'); + expect(nativeApiSpy).to.have.been.calledOnce; + expect(libSpy).to.not.have.been.called; + }); }); - }); + } }); From 3bc96c2543937ec81d18d38d93d2ab6429db9fa9 Mon Sep 17 00:00:00 2001 From: lannka Date: Fri, 1 Jul 2016 11:28:06 -0700 Subject: [PATCH 5/6] Use sandbox.spy instead of sinon.spy. Presubmit doesn't like it :-) --- extensions/amp-analytics/0.1/test/test-crypto-impl.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/amp-analytics/0.1/test/test-crypto-impl.js b/extensions/amp-analytics/0.1/test/test-crypto-impl.js index 0ed72cc15186..5902761a1dc3 100644 --- a/extensions/amp-analytics/0.1/test/test-crypto-impl.js +++ b/extensions/amp-analytics/0.1/test/test-crypto-impl.js @@ -85,14 +85,16 @@ describe('crypto-impl', () => { // of native Crypto API. if (isModernChrome()) { it('should not call closure lib when native API is available', () => { - const nativeApiSpy = sinon.spy(window.crypto.subtle, 'digest'); - const libSpy = sinon.spy(lib, 'sha384'); + const sandbox = sinon.sandbox.create(); + const nativeApiSpy = sandbox.spy(window.crypto.subtle, 'digest'); + const libSpy = sandbox.spy(lib, 'sha384'); return new Crypto(window).sha384Base64('abc').then(hash => { expect(hash).to.equal( 'ywB1P0WjXou1oD1pmsZQBycsMqsO3tFjGotgWkP_W-2AhgcroefMI1i67KE0yCWn'); expect(nativeApiSpy).to.have.been.calledOnce; expect(libSpy).to.not.have.been.called; }); + sandbox.restore(); }); } }); From 9e317e65df6eb75ad9d60ea07347aa49a6aba696 Mon Sep 17 00:00:00 2001 From: lannka Date: Fri, 1 Jul 2016 12:22:58 -0700 Subject: [PATCH 6/6] Add JSDoc to function str2ab. --- extensions/amp-analytics/0.1/crypto-impl.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/amp-analytics/0.1/crypto-impl.js b/extensions/amp-analytics/0.1/crypto-impl.js index 164099be3cdc..eab37fdb6bb1 100644 --- a/extensions/amp-analytics/0.1/crypto-impl.js +++ b/extensions/amp-analytics/0.1/crypto-impl.js @@ -75,7 +75,11 @@ function getSubtle(win) { return win.crypto.subtle || win.crypto.webkitSubtle || null; } -// A shim for TextEncoder +/** + * Convert a string to Unit8Array. A shim for TextEncoder. + * @param {string} str + * @returns {!Uint8Array} + */ function str2ab(str) { const buf = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) {