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

Commit

Permalink
client cryptoUtil; requestUtil .refreshAWSCredentials
Browse files Browse the repository at this point in the history
  • Loading branch information
ayumi committed Dec 29, 2016
1 parent 887ef39 commit 0e29c3f
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 166 deletions.
77 changes: 77 additions & 0 deletions client/cryptoUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use strict'

const crypto = require('../lib/crypto')

/**
* Create a function which will serialize then encrypt a sync record.
* @param {Serializer} serializer
* @param {Uint8Array} secretboxKey
* @param {number} nonceCounter
* @returns {Function}
*/
module.exports.Encrypt = (serializer, secretboxKey, nonceCounter) => {
return (syncRecord) => {
return this.encrypt(serializer, secretboxKey, syncRecord, nonceCounter)
}
}

/**
* Serialize then encrypt a sync record.
* Note this is the browser JS version, so it uses Uint8Array as the main way to
* express byte arrays.
* @param {Serializer} serializer
* @param {Uint8Array} secretboxKey
* @param {Object} syncRecord
* @param {number} nonceCounter
* @returns {Uint8Array}
*/
module.exports.encrypt = (serializer, secretboxKey, syncRecord, nonceCounter) => {
const bytes = serializer.syncRecordToByteArray(syncRecord)
const nonceRandom = Buffer.from(crypto.randomBytes(20))
const encrypted = crypto.encrypt(bytes, secretboxKey, nonceCounter, nonceRandom)
return serializer.SecretboxRecordToByteArray({
encryptedData: encrypted.ciphertext,
nonceCounter,
nonceRandom: encrypted.nonce
})
}

/**
* Create a function which will decrypt then deserialize a message.
* @param {Serializer} serializer
* @param {Uint8Array} secretboxKey
* @returns {Function}
*/
module.exports.Decrypt = (serializer, secretboxKey) => {
return (secretboxRecordBytes) => {
return this.decrypt(serializer, secretboxKey, secretboxRecordBytes)
}
}

/**
* Decrypt then deserialize a message.
* @param {Serializer} serializer
* @param {Uint8Array} secretboxKey
* @param {Uint8Array} secretboxRecordBytes
* @returns {Object}
*/
module.exports.decrypt = (serializer, secretboxKey, secretboxRecordBytes) => {
const secretboxRecord = serializer.byteArrayToSecretboxRecord(secretboxRecordBytes)
const decryptedBytes = crypto.decrypt(
secretboxRecord.encryptedData,
secretboxRecord.nonceRandom,
Buffer.from(secretboxKey)
)
return serializer.byteArrayToSyncRecord(decryptedBytes)
}

/**
* Create a function which will sign some bytes.
* @param {Uint8Array} secretKey
* @returns {Function}
*/
module.exports.Sign = (secretKey) => {
return (bytes) => {
return crypto.sign(bytes, secretKey)
}
}
112 changes: 81 additions & 31 deletions client/requestUtil.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,91 @@
'use strict'

const NONCE_COUNTER = 0

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

const checkFetchStatus = (response) => {
if (response.status >= 200 && response.status < 300) {
return response
} else {
var error = new Error(response.statusText)
error.response = response
throw error
}
}

const getTime = () => {
return Math.floor(Date.now() / 1000)
}

/**
* @param {Object} serializer
* @param {Uint8Array} credentialsBytes
* @param {string} apiVersion
* @param {string} userId
* @param {{
* apiVersion: <string>,
* credentialsBytes: <Uint8Array=>, // If missing, will be requested
* keys: {{ // User's encryption keys
* publicKey: <Uint8Array>, secretKey: <Uint8Array>,
* fingerprint: <string=>, secretboxKey: <Uint8Array>}},
* serializer: <Object>,
* serverUrl: <string>
* }} opts
*/
const RequestUtil = function (serializer, credentialsBytes, apiVersion, userId) {
if (!apiVersion) { throw new Error('Missing apiVersion.') }
this.apiVersion = apiVersion
if (!userId) { throw new Error('Missing userId.') }
this.userId = userId

this.serializer = serializer
const response = this.parseAWSResponse(credentialsBytes)
this.s3 = response.s3
this.postData = response.postData
this.expiration = response.expiration
this.bucket = response.bucket
this.region = response.region
const RequestUtil = function (opts = {}) {
if (!opts.apiVersion) { throw new Error('Missing apiVersion.') }
if (!opts.keys) { throw new Error('Missing keys.') }
if (!opts.serializer) { throw new Error('Missing serializer.') }
if (!opts.serverUrl) { throw new Error('Missing serverUrl.') }
this.apiVersion = opts.apiVersion
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.decrypt = cryptoUtil.Decrypt(this.serializer, opts.keys.secretboxKey)
this.sign = cryptoUtil.Sign(opts.keys.secretKey)
if (opts.credentialsBytes) {
const credentials = this.parseAWSResponse(opts.credentialsBytes)
this.saveAWSCredentials(credentials)
}
}

/**
* Save parsed AWS credential response to be used with AWS requests.
* @param {{s3: Object, postData: Object, expiration: string, bucket: string, region: string}}
* @return {Promise} After it resolves, the object is ready to make requests.
*/
RequestUtil.prototype.refreshAWSCredentials = function () {
const timestampString = getTime().toString()
const userId = window.encodeURIComponent(this.userId)
const url = `${this.serverUrl}/${userId}/credentials`
const bytes = this.serializer.stringToByteArray(timestampString)
const params = {
method: 'POST',
body: this.sign(bytes)
}
return window.fetch(url, params)
.then((response) => {
if (!response.ok) {
throw new Error(`Credential server response ${response.status}`)
}
return response.arrayBuffer()
})
.then((buffer) => {
const credentials = this.parseAWSResponse(new Uint8Array(buffer))
this.saveAWSCredentials(credentials)
})
}

/**
* Save parsed AWS credential response to be used with AWS requests.
* @param {{s3: Object, postData: Object, expiration: string, bucket: string, region: string}}
*/
RequestUtil.prototype.saveAWSCredentials = function (parsedResponse) {
this.s3 = parsedResponse.s3
this.postData = parsedResponse.postData
this.expiration = parsedResponse.expiration
this.bucket = parsedResponse.bucket
this.region = parsedResponse.region
this.s3PostEndpoint = `https://${this.bucket}.s3.dualstack.${this.region}.amazonaws.com`
}

Expand Down Expand Up @@ -154,18 +218,4 @@ RequestUtil.prototype.deleteCategory = function (category) {
return this.s3DeletePrefix(`${this.apiVersion}/${this.userId}/${category}`)
}

function checkFetchStatus (response) {
if (response.status >= 200 && response.status < 300) {
return response
} else {
var error = new Error(response.statusText)
error.response = response
throw error
}
}

function getTime () {
return Math.floor(Date.now() / 1000)
}

module.exports = RequestUtil
75 changes: 11 additions & 64 deletions client/sync.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'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 crypto = require('../lib/crypto')
const conf = require('./config')

const ipc = window.chrome.ipcRenderer
Expand Down Expand Up @@ -47,68 +47,8 @@ const logSync = (message, logLevel = DEBUG) => {
}
}

/**
* decrypt then deserialize a message.
* @param {Uint8Array} ciphertext
* @returns {Object}
*/
const decrypt = (ciphertext) => {
const d = clientSerializer.byteArrayToSecretboxRecord(ciphertext)
const decrypted = crypto.decrypt(d.encryptedData,
d.nonceRandom, clientKeys.secretboxKey)
if (!decrypted) {
throw new Error('Decryption failed.')
}
return clientSerializer.byteArrayToSyncRecord(decrypted)
}

/**
* serialize then encrypts a sync record
* @param {Object} message
* @returns {Uint8Array}
*/
const encrypt = (message) => {
const s = clientSerializer.syncRecordToByteArray(message)
const nonceRandom = crypto.randomBytes(20)
const encrypted = crypto.encrypt(s, clientKeys.secretboxKey,
conf.nonceCounter, nonceRandom)
return clientSerializer.SecretboxRecordToByteArray({
nonceRandom,
counter: conf.counter,
encryptedData: encrypted.ciphertext
})
}

/**
* Gets AWS creds.
* @returns {Promise}
*/
const getAWSCredentials = () => {
const serverUrl = config.serverUrl
const now = Math.floor(Date.now() / 1000).toString()
if (clientSerializer === null) {
throw new Error('Serializer not initialized.')
}
const userId = window.encodeURIComponent(clientUserId)
const request = new window.Request(`${serverUrl}/${userId}/credentials`, {
method: 'POST',
body: crypto.sign(clientSerializer.stringToByteArray(now), clientKeys.secretKey)
})
return window.fetch(request)
.then((response) => {
if (!response.ok) {
throw new Error('Credential server response ' + response.status)
}
return response.arrayBuffer()
})
.then((buffer) => {
requester = new RequestUtil(clientSerializer, new Uint8Array(buffer),
config.apiVersion, clientUserId)
if (!requester.s3) {
throw new Error('could not initialize AWS SDK')
}
})
}
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.
Expand Down Expand Up @@ -196,7 +136,14 @@ Promise.all([serializer.init(''), initializer.init(window.chrome)]).then((values
logSync(`initialized userId ${clientUserId}`)
})
.then(() => {
return getAWSCredentials()
requester = new RequestUtil({
apiVersion: config.apiVersion,
credentialsBytes: null, // TODO: Start with previous session's credentials
keys: clientKeys,
serializer: clientSerializer,
syncServerUrl: config.serverUrl
})
return requester.refreshAWSCredentials()
})
.then(() => {
logSync('successfully authenticated userId: ' + clientUserId)
Expand Down
Loading

0 comments on commit 0e29c3f

Please sign in to comment.