Skip to content
This repository has been archived by the owner on Jul 31, 2020. It is now read-only.

Commit

Permalink
requestUtil handles AWS auth expiration
Browse files Browse the repository at this point in the history
Fix #11

Auditors: @diracdeltas
  • Loading branch information
ayumi committed Dec 29, 2016
1 parent 0e29c3f commit f420d1f
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 36 deletions.
5 changes: 5 additions & 0 deletions client/config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* This file includes base config vars.
* Additional config comes from the browser client when it initializes sync.
*/

module.exports = {
// Set this to the origins where client.html may be loaded
clientOrigins: ['http://localhost:8000', 'chrome-extension://mnojpmjdmbbfmejpflffifhffcmidifd'],
Expand Down
105 changes: 82 additions & 23 deletions client/requestUtil.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
'use strict'

const NONCE_COUNTER = 0

const awsSdk = require('aws-sdk')
const cryptoUtil = require('./cryptoUtil')
const s3Helper = require('../lib/s3Helper')

const CONFIG = require('./config')
const S3_MAX_RETRIES = 1
const EXPIRED_CREDENTIAL_ERRORS = [
/The provided token has expired\./,
/Invalid according to Policy: Policy expired\./
]

const checkFetchStatus = (response) => {
if (response.status >= 200 && response.status < 300) {
return response
Expand All @@ -16,6 +21,12 @@ const checkFetchStatus = (response) => {
}
}

const isExpiredCredentialError = (error) => {
return EXPIRED_CREDENTIAL_ERRORS.some((message) => {
return error.message.match(message)
})
}

const getTime = () => {
return Math.floor(Date.now() / 1000)
}
Expand All @@ -40,7 +51,7 @@ const RequestUtil = function (opts = {}) {
this.serializer = opts.serializer
this.serverUrl = opts.serverUrl
this.userId = Buffer.from(opts.keys.publicKey).toString('base64')
this.encrypt = cryptoUtil.Encrypt(this.serializer, opts.keys.secretboxKey, NONCE_COUNTER)
this.encrypt = cryptoUtil.Encrypt(this.serializer, opts.keys.secretboxKey, CONFIG.nonceCounter)
this.decrypt = cryptoUtil.Decrypt(this.serializer, opts.keys.secretboxKey)
this.sign = cryptoUtil.Sign(opts.keys.secretKey)
if (opts.credentialsBytes) {
Expand Down Expand Up @@ -71,6 +82,7 @@ RequestUtil.prototype.refreshAWSCredentials = function () {
return response.arrayBuffer()
})
.then((buffer) => {
console.log('Refreshed credentials.')
const credentials = this.parseAWSResponse(new Uint8Array(buffer))
this.saveAWSCredentials(credentials)
})
Expand Down Expand Up @@ -127,6 +139,7 @@ RequestUtil.prototype.parseAWSResponse = function (bytes) {
// The bucket name is prepended to the endpoint to build the actual request URL, e.g.
// https://brave-sync-staging.s3.dualstack.us-west-2.amazonaws.com
endpoint: `https://s3.dualstack.${region}.amazonaws.com`,
maxRetries: S3_MAX_RETRIES,
region: region,
sslEnabled: true,
useDualstack: true
Expand All @@ -146,18 +159,17 @@ RequestUtil.prototype.list = function (category, startAt) {
Bucket: this.bucket,
Prefix: prefix
}
if (startAt) {
options.StartAfter = `${prefix}/${startAt}`
}
return s3Helper.listObjects(this.s3, options)
.then((data) => {
return data.map((s3Object) => {
const parsedKey = s3Helper.parseS3Key(s3Object.Key)
// TODO: Recombine split records
const decodedData = s3Helper.s3StringToByteArray(parsedKey.recordPartString)
return decodedData
})
if (startAt) { options.StartAfter = `${prefix}/${startAt}` }
return this.withRetry(() => {
return s3Helper.listObjects(this.s3, options)
}).then((data) => {
return data.map((s3Object) => {
const parsedKey = s3Helper.parseS3Key(s3Object.Key)
// TODO: Recombine split records
const decodedData = s3Helper.s3StringToByteArray(parsedKey.recordPartString)
return decodedData
})
})
}

/**
Expand All @@ -177,15 +189,17 @@ RequestUtil.prototype.currentRecordPrefix = function (category) {
RequestUtil.prototype.put = function (category, record) {
const s3Prefix = this.currentRecordPrefix(category)
const s3Keys = s3Helper.encodeDataToS3KeyArray(s3Prefix, record)
const fetchPromises = s3Keys.map((key, _i) => {
const params = {
method: 'POST',
body: this.s3PostFormData(key)
}
return window.fetch(this.s3PostEndpoint, params)
.then(checkFetchStatus)
return this.withRetry(() => {
const fetchPromises = s3Keys.map((key, _i) => {
const params = {
method: 'POST',
body: this.s3PostFormData(key)
}
return window.fetch(this.s3PostEndpoint, params)
.then(checkFetchStatus)
})
return Promise.all(fetchPromises)
})
return Promise.all(fetchPromises)
}

RequestUtil.prototype.s3PostFormData = function (objectKey) {
Expand All @@ -204,7 +218,9 @@ RequestUtil.prototype.s3PostFormData = function (objectKey) {
* @param {string} prefix
*/
RequestUtil.prototype.s3DeletePrefix = function (prefix) {
return s3Helper.deletePrefix(this.s3, this.bucket, prefix)
return this.withRetry(() => {
return s3Helper.deletePrefix(this.s3, this.bucket, prefix)
})
}

RequestUtil.prototype.deleteUser = function () {
Expand All @@ -218,4 +234,47 @@ RequestUtil.prototype.deleteCategory = function (category) {
return this.s3DeletePrefix(`${this.apiVersion}/${this.userId}/${category}`)
}

/**
* Wrapper to call a function and refresh credentials if needed.
* @param {Function(Promise)} Function which returns a Promise.
* @param {number} retries Retries left. You probably don't need to change this.
* @param {Error=} previousError Buffer with the previous error, for internal use.
*/
RequestUtil.prototype.withRetry = function (myFun, retries = 1, previousError) {
if (retries < 0) { throw previousError }

return new Promise((resolve, reject) => {
const callMyFun = () => {
myFun()
.then((...args) => { resolve(...args) })
.catch((error) => {
const retry = () => {
try {
this.withRetry(myFun, retries - 1, error)
.then((...args) => { resolve(...args) })
.catch((error) => { reject(error) })
} catch (error) {
reject(error)
}
}
// window.fetch() requests. checkFetchStatus() appends responses.
if (error.response) {
error.response.text().then((body) => {
error.message = error.message.concat(body)
retry()
})
} else {
retry()
}
})
}
if (previousError) {
if (!isExpiredCredentialError(previousError)) { throw previousError }
this.refreshAWSCredentials().then(callMyFun)
} else {
callMyFun()
}
})
}

module.exports = RequestUtil
13 changes: 4 additions & 9 deletions client/sync.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
'use strict'

const initializer = require('./init')
const cryptoUtil = require('./cryptoUtil')
const RequestUtil = require('./requestUtil')
const messages = require('./constants/messages')
const proto = require('./constants/proto')
const serializer = require('../lib/serializer')
const conf = require('./config')

const ipc = window.chrome.ipcRenderer

Expand All @@ -22,7 +20,7 @@ var clientUserId = null
var clientKeys = {}
var config = {}

// aws sdk requests class
// RequestUtil which makes AWS requests
var requester = {}

console.log('in sync script')
Expand All @@ -47,9 +45,6 @@ const logSync = (message, logLevel = DEBUG) => {
}
}

const decrypt = cryptoUtil.Decrypt(clientSerializer, clientKeys.secretboxKey)
const encrypt = cryptoUtil.Encrypt(clientSerializer, clientKeys.secretboxKey, conf.nonceCounter)

/**
* Sets the device ID if one does not yet exist.
* @returns {Promise}
Expand All @@ -68,7 +63,7 @@ const maybeSetDeviceId = () => {
records.forEach((bytes) => {
var record = {}
try {
record = decrypt(bytes)
record = requester.decrypt(bytes)
} catch (e) {
return
}
Expand Down Expand Up @@ -108,7 +103,7 @@ const startSync = () => {
record.deviceId = new Uint8Array(record.deviceId)
record.objectId = new Uint8Array(record.objectId)
logSync(`sending record: ${JSON.stringify(record)}`)
requester.put(proto.categories[category], encrypt(record))
requester.put(proto.categories[category], requester.encrypt(record))
})
})
logSync('success')
Expand Down Expand Up @@ -141,7 +136,7 @@ Promise.all([serializer.init(''), initializer.init(window.chrome)]).then((values
credentialsBytes: null, // TODO: Start with previous session's credentials
keys: clientKeys,
serializer: clientSerializer,
syncServerUrl: config.serverUrl
serverUrl: config.serverUrl
})
return requester.refreshAWSCredentials()
})
Expand Down
47 changes: 43 additions & 4 deletions test/client/requestUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,19 +161,19 @@ test('client RequestUtil', (t) => {
region: requestUtil.region
}
t.test('RequestUtil with expired credentials', (t) => {
t.plan(2)
t.plan(3)

const args = {
const expiredArgs = {
apiVersion: clientTestHelper.CONFIG.apiVersion,
credentialsBytes: serializer.credentialsToByteArray(expiredCredentials),
keys,
serializer,
serverUrl: clientTestHelper.CONFIG.serverUrl
}

t.doesNotThrow(() => { return new RequestUtil(args) }, `${t.name} instantiates without error`)
t.doesNotThrow(() => { return new RequestUtil(expiredArgs) }, `${t.name} instantiates without error`)

t.test('#refreshAWSCredentials', (t) => {
t.test('#refreshAWSCredentials()', (t) => {
t.plan(2)
const args = {
apiVersion: clientTestHelper.CONFIG.apiVersion,
Expand All @@ -193,6 +193,45 @@ test('client RequestUtil', (t) => {
})
.catch((error) => { t.fail(error) })
})

t.test('automatic credential refresh', (t) => {
t.plan(2)

t.test(`${t.name} list()`, (t) => {
t.plan(1)
const requestUtil = new RequestUtil(expiredArgs)
requestUtil.list(proto.categories.PREFERENCES)
.then((response) => { t.equals(response.length, 0, t.name) })
.catch((error) => { t.fail(error) })
})

t.test(`${t.name} put()`, (t) => {
t.plan(2)
const record = {
action: 'CREATE',
deviceId: new Uint8Array([1]),
objectId: testHelper.uuid(),
device: {name: 'sweet'}
}
const requestUtil = new RequestUtil(expiredArgs)
requestUtil.put(proto.categories.PREFERENCES, encrypt(record))
.then((response) => {
t.pass(t.name)
testCredentialRefreshDelete(t)
})
.catch((error) => { t.fail(error) })
})

const testCredentialRefreshDelete = (t) => {
t.test(`${t.name} deleteCategory()`, (t) => {
t.plan(1)
const requestUtil = new RequestUtil(expiredArgs)
requestUtil.deleteCategory(proto.categories.PREFERENCES)
.then((response) => { t.pass(t.name) })
.catch((error) => { t.fail(error) })
})
}
})
})
}
})

0 comments on commit f420d1f

Please sign in to comment.