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

✨ Subscriptions encryption part 1 #21670

Merged
merged 16 commits into from
Apr 22, 2019
6 changes: 6 additions & 0 deletions build-system/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,12 @@ app.use('/bind/ecommerce/sizes', (req, res) => {
}, 1000); // Simulate network delay.
});

/*
//TODO(chenshay): Accept '?crypto=bla'
implement authorizer here.
this is for local testing.
*/

// Simulated subscription entitlement
app.use('/subscription/:id/entitlements', (req, res) => {
cors.assertCors(req, res, ['GET']);
Expand Down
20 changes: 19 additions & 1 deletion examples/amp-subscriptions.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
}
</script>

<script keys type="application/json">
{
"google.com": "ENCRYPT({\"accessRequirements\": [\"norcal.com:premium\"], \"key\":\"aBcDef781-2-4/sjfdi\"})"
"local": "ENCRYPT({\"accessRequirements\": [\"whateveryouwant:12\"], \"key\":\"yourkey\"})"
}
</script>

<link href='https://fonts.googleapis.com/css?family=Georgia|Open+Sans|Roboto' rel='stylesheet' type='text/css'>
<style amp-custom>
Expand Down Expand Up @@ -214,7 +220,7 @@
srcset="img/hero@1x.jpg 1x, img/hero@2x.jpg 2x"
layout="responsive" width="360" placeholder
alt="Curabitur convallis, urna quis pulvinar feugiat, purus diam posuere turpis, sit amet tincidunt purus justo et mi."
height="216">
height="116">
</amp-img>
</figure>

Expand Down Expand Up @@ -250,6 +256,18 @@ <h1 itemprop="headline">Lorem Ipsum</h1>
Login or subscribe to read more.
</section>

<br/>

<div>
before
<script type="application/octet-stream" encrypted>
tniRwntes210WEY+s/BK+dkpd39U9UO75lfADFfpXA7ASUpyreAyVI4TwZ0DCsLJpHC4ckp0+zQ93vYDdH+aSvXm8ksjCIjw1I82pQbcV04CK1NJ5ha9gJavE5bVobMNBAIM6PbfRIFUf+8oE8Tmv5SiDHu9CUHheB4MOunXBKqZzlyW3HgtImY56w6vqLKxPjDwhalvEf4rg2Xi+QlFzeE7YCPCPo68T4T1v9R+RCuYY2QDvLs3Re5Hb6tfnvds/7fO8NCdD6zrZUv0NENoZQvyvEcYt3hKQJe7N908JPg=
</script>
after
</div>

<br/>

<div subscriptions-section="content" class="article-body" itemprop="articleBody">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ export class GoogleSubscriptionsPlatform {
granted: true, //swgEntitlements.getEntitlementForThis makes sure this is true.
grantReason: GrantReason.SUBSCRIBER, // there is no other case of subscription for SWG as of now.
dataObject: swgEntitlement.json(),
// TODO(chenshay): Do this and test it after swg-js library is upgraded.
// decryptedDocumentKey: swgEntitlements.decryptedDocumentKey,
});
});
}
Expand Down
18 changes: 17 additions & 1 deletion extensions/amp-subscriptions/0.1/amp-subscriptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import {CSS} from '../../../build/amp-subscriptions-0.1.css';
import {CryptoHandler} from './crypto-handler';
import {Dialog} from './dialog';
import {DocImpl} from './doc-impl';
import {Entitlement} from './entitlement';
Expand Down Expand Up @@ -110,6 +111,9 @@ export class SubscriptionService {

/** @private {!Object<string, ?Promise<string>>} */
this.readerIdPromiseMap_ = {};

/** @private {!CryptoHandler} */
this.cryptoHandler_ = new CryptoHandler(ampdoc);
}

/**
Expand Down Expand Up @@ -227,6 +231,10 @@ export class SubscriptionService {
*/
resolveEntitlementsToStore_(serviceId, entitlement) {
this.platformStore_.resolveEntitlement(serviceId, entitlement);
if (entitlement.decryptedDocumentKey) {
this.cryptoHandler_.tryToDecryptDocument(
chenshay marked this conversation as resolved.
Show resolved Hide resolved
entitlement.decryptedDocumentKey);
}
this.subscriptionAnalytics_.serviceEvent(
SubscriptionAnalyticsEvents.ENTITLEMENT_RESOLVED,
serviceId
Expand Down Expand Up @@ -346,7 +354,7 @@ export class SubscriptionService {
viewerPlatform.getEntitlements().then(entitlement => {
devAssert(entitlement, 'Entitlement is null');
// Viewer authorization is redirected to use local platform instead.
this.platformStore_.resolveEntitlement('local',
this.resolveEntitlementsToStore_('local',
/** @type {!./entitlement.Entitlement}*/ (entitlement));
}).catch(reason => {
this.platformStore_.reportPlatformFailureAndFallback('local');
Expand Down Expand Up @@ -378,6 +386,14 @@ export class SubscriptionService {
return readerId;
}

/**
* @param {string} serviceId
* @return {?string}
*/
getEncryptedDocumentKey(serviceId) {
return this.cryptoHandler_.getEncryptedDocumentKey(serviceId);
}

/**
* Returns the singleton Dialog instance
* @return {!Dialog}
Expand Down
102 changes: 102 additions & 0 deletions extensions/amp-subscriptions/0.1/crypto-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Copyright 2019 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 {iterateCursor} from '../../../src/dom';
import {tryParseJson} from '../../../src/json';


export class CryptoHandler {

/**
* Creates an instance of CryptoHandler.
* @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc
*/
constructor(ampdoc) {
/** @private @const {!../../../src/service/ampdoc-impl.AmpDoc} */
this.ampdoc_ = ampdoc;

/** @private {?Promise} */
this.decryptionPromise_ = null;

const parsedEncryptedKeys =
this.ampdoc_.getRootNode().querySelector('script[keys]');
/** @type {?JsonObject} */
this.encryptedKeys_ = (parsedEncryptedKeys &&
tryParseJson(parsedEncryptedKeys.textContent)) || null;
}

/**
* This method is used for testing.
* @return {?JsonObject}
*/
getEncryptedKeys() {
return this.encryptedKeys_;
}

/**
* @param {string} serviceId Who you want to decrypt the key.
* For example: 'google.com'
* @return {?string}
*/
getEncryptedDocumentKey(serviceId) {
// Doing this for testing.
const encryptedKeys = this.getEncryptedKeys();
if (!encryptedKeys) {
return null;
}
return encryptedKeys[serviceId] || null;
}

/**
* @param {string} decryptedDocumentKey
* @return {!Promise}
*/
tryToDecryptDocument(decryptedDocumentKey) {
if (this.decryptionPromise_) {
return this.decryptionPromise_;
}
this.decryptionPromise_ = this.ampdoc_.whenReady().then(() => {
const encryptedSections =
this.ampdoc_.getRootNode().querySelectorAll('script[encrypted]');
const promises = [];
iterateCursor(encryptedSections, encryptedSection => {
promises.push(
this.decryptDocumentContent_(encryptedSection.textContent,
decryptedDocumentKey).then(decryptedContent => {
encryptedSection./*OK*/outerHTML = decryptedContent;
}));
});
return Promise.all(promises);
});
return this.decryptionPromise_;
}

/**
* @private
* @param {string} encryptedContent
* @param {string} documentKey
* @return {Promise<string>}
*/
decryptDocumentContent_(
// eslint-disable-next-line no-unused-vars
encryptedContent, documentKey) {
// Don't really return this. Placeholder for the real thing.
// const placeholder = encryptedContent.trim() + documentKey;
const placeholder = '<h2><i> abc </i></h2>';
return Promise.resolve(placeholder);
}
}
8 changes: 6 additions & 2 deletions extensions/amp-subscriptions/0.1/entitlement.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ export class Entitlement {
* @param {boolean} [input.granted]
* @param {?GrantReason} [input.grantReason]
* @param {?JsonObject} [input.dataObject]
* @param {?string} [input.decryptedDocumentKey]
*/
constructor({source, raw = '', service, granted = false,
grantReason = '', dataObject}) {
grantReason = '', dataObject, decryptedDocumentKey}) {
/** @const {string} */
this.raw = raw;
/** @const {string} */
Expand All @@ -62,6 +63,8 @@ export class Entitlement {
this.grantReason = grantReason;
/** @const {?JsonObject} */
this.data = dataObject;
/** @const {?string} */
this.decryptedDocumentKey = decryptedDocumentKey;
}

/**
Expand Down Expand Up @@ -104,8 +107,9 @@ export class Entitlement {
const granted = json['granted'] || false;
const grantReason = json['grantReason'];
const dataObject = json['data'] || null;
const decryptedDocumentKey = json['decryptedDocumentKey'] || null;
return new Entitlement({source, raw, service: '',
granted, grantReason, dataObject});
granted, grantReason, dataObject, decryptedDocumentKey});
}

/**
Expand Down
13 changes: 10 additions & 3 deletions extensions/amp-subscriptions/0.1/local-subscription-platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import {PageConfig} from '../../../third_party/subscriptions-project/config';
import {Services} from '../../../src/services';
import {UrlBuilder} from './url-builder';
import {assertHttpsUrl} from '../../../src/url';
import {addParamToUrl, assertHttpsUrl} from '../../../src/url';
import {closestAncestorElementBySelector} from '../../../src/dom';
import {dev, devAssert, userAssert} from '../../../src/log';
import {dict} from '../../../src/utils/object';
Expand Down Expand Up @@ -188,12 +188,19 @@ export class LocalSubscriptionPlatform {
getEntitlements() {
return this.urlBuilder_.buildUrl(this.authorizationUrl_,
/* useAuthData */ false)
.then(fetchUrl =>
.then(fetchUrl => {
const encryptedDocumentKey =
this.serviceAdapter_.getEncryptedDocumentKey('local');
if (encryptedDocumentKey) {
//TODO(chenshay): if crypt, switch to 'post'
fetchUrl = addParamToUrl(fetchUrl, 'crypt', encryptedDocumentKey);
chenshay marked this conversation as resolved.
Show resolved Hide resolved
}
this.xhr_.fetchJson(fetchUrl, {credentials: 'include'})
.then(res => res.json())
.then(resJson => {
return Entitlement.parseFromJson(resJson);
}));
});
});
}

/** @override */
Expand Down
9 changes: 9 additions & 0 deletions extensions/amp-subscriptions/0.1/service-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ export class ServiceAdapter {
return this.subscriptionService_.getReaderId(serviceId);
}

/**
* Returns the encrypted document key for the specified service.
* @param {string} serviceId
* @return {?string}
*/
getEncryptedDocumentKey(serviceId) {
return this.subscriptionService_.getEncryptedDocumentKey(serviceId);
}

/**
* Returns the analytics service for subscriptions.
* @return {!./analytics.SubscriptionAnalytics}
Expand Down
45 changes: 45 additions & 0 deletions extensions/amp-subscriptions/0.1/test/test-amp-subscriptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,51 @@ describes.fakeWin('AmpSubscriptions', {amp: true}, env => {
});
});

describe('SwG Encryption', () => {
let platformStore;
let entitlement;
let decryptedDocumentKey;

beforeEach(() => {
platformStore = new PlatformStore(['local']);
subscriptionService.platformStore_ = platformStore;
decryptedDocumentKey = 'decryptedDocumentKey';
entitlement = new Entitlement({
source: 'local',
raw: 'raw',
granted: true,
grantReason: GrantReason.SUBSCRIBER,
dataObject: {
test: 'a1',
},
decryptedDocumentKey,
});
});

it('should try to decrypt document', () => {
const stub = sandbox.stub(subscriptionService.cryptoHandler_,
'tryToDecryptDocument');
subscriptionService.resolveEntitlementsToStore_('serviceId', entitlement);
expect(stub).to.be.calledWith(decryptedDocumentKey);
});

it('should NOT try to decrypt document', () => {
entitlement = new Entitlement({
source: 'local',
raw: 'raw',
granted: true,
grantReason: GrantReason.SUBSCRIBER,
dataObject: {
test: 'a1',
},
});
const stub = sandbox.stub(subscriptionService.cryptoHandler_,
'tryToDecryptDocument');
subscriptionService.resolveEntitlementsToStore_('serviceId', entitlement);
expect(stub).to.not.be.called;
});
});

describe('AccessVars', () => {
let platformStore;
let entitlement;
Expand Down