Skip to content

Commit

Permalink
Merge pull request #5 from tozny/bugfix/sdk-parity
Browse files Browse the repository at this point in the history
Update SDK for parity with Swift
  • Loading branch information
ericmann committed Dec 14, 2017
2 parents d74bfbc + ae971c7 commit a8ce4ce
Show file tree
Hide file tree
Showing 9 changed files with 7,636 additions and 34 deletions.
5 changes: 4 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"presets": ["es2015"]
"presets": ["env"],
"plugins": [
"transform-runtime"
]
}
11 changes: 6 additions & 5 deletions lib/__tests__/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { default as Client } from '../client'
import { default as Config } from '../config'
import { default as Crypto } from '../crypto'

import { default as EAKInfo } from '../types/eakInfo'
import { default as Meta } from '../types/meta'
import { default as Record } from '../types/record'
import { default as RecordData } from '../types/recordData'
Expand Down Expand Up @@ -33,7 +34,7 @@ describe('Client', () => {
it('can encrypt and decrypt a record', async () => {
let ak = await Crypto.randomKey()
let encryptedAk = await Crypto.encryptAk(cryptoKeys.privateKey, ak, cryptoKeys.publicKey)
let eak = {eak: encryptedAk, authorizer_public_key: {curve25519: cryptoKeys.publicKey}}
let eak = new EAKInfo(encryptedAk, clientId, cryptoKeys.publicKey, clientId, signingKeys.publicKey)

let client = new Client(config)
let encrypted = await client.encrypt('type', {field1: 'this is a test', field2: 'another'}, eak)
Expand All @@ -49,7 +50,7 @@ describe('Client', () => {
it('signs a document while encrypting', async () => {
let ak = await Crypto.randomKey()
let encryptedAk = await Crypto.encryptAk(cryptoKeys.privateKey, ak, cryptoKeys.publicKey)
let eak = {eak: encryptedAk, authorizer_public_key: {curve25519: cryptoKeys.publicKey}}
let eak = new EAKInfo(encryptedAk, clientId, cryptoKeys.publicKey, clientId, signingKeys.publicKey)

let client = new Client(config)
let data = new RecordData({field1: 'this is a test', field2: 'another'})
Expand Down Expand Up @@ -118,7 +119,7 @@ describe('Client', () => {
it('signs both plaintext fields and meta', async () => {
let ak = await Crypto.randomKey()
let encryptedAk = await Crypto.encryptAk(cryptoKeys.privateKey, ak, cryptoKeys.publicKey)
let eak = {eak: encryptedAk, authorizer_public_key: {curve25519: cryptoKeys.publicKey}}
let eak = new EAKInfo(encryptedAk, clientId, cryptoKeys.publicKey, clientId, signingKeys.publicKey)

let client = new Client(config)
let data = new RecordData({field1: 'this is a test', field2: 'another'})
Expand Down Expand Up @@ -170,7 +171,7 @@ describe('Client', () => {

let recordInfo = new RecordInfo(meta, data)

expect(recordInfo.stringify()).toBe('{"writerId":null,"userId":null,"type":null,"plain":{"field":"value"}}{"dataField":"value"}')
expect(recordInfo.stringify()).toBe('{"plain":{"field":"value"},"type":null,"user_id":null,"writer_id":null}{"dataField":"value"}')
})

it('properly marshalls signed document fields', async () => {
Expand All @@ -192,7 +193,7 @@ describe('Client', () => {
it('encrypts individual fields', async () => {
let ak = await Crypto.randomKey()
let encryptedAk = await Crypto.encryptAk(cryptoKeys.privateKey, ak, cryptoKeys.publicKey)
let eak = {eak: encryptedAk, authorizer_public_key: {curve25519: cryptoKeys.publicKey}}
let eak = new EAKInfo(encryptedAk, clientId, cryptoKeys.publicKey, clientId, signingKeys.publicKey)

let ticketInfo = {
accountBalance: '50.25',
Expand Down
134 changes: 120 additions & 14 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { default as Crypto } from './crypto'
import { default as Config } from './config'
import { default as ClientDetails } from './types/clientDetails'
import { default as ClientInfo } from './types/clientInfo'
import { default as EAKInfo } from './types/eakInfo'
import { default as IncomingSharingPolicy } from './types/incomingSharingPolicy'
import { default as KeyPair } from './types/keyPair'
import { default as Meta } from './types/meta'
Expand All @@ -43,12 +44,15 @@ import { default as QueryResult } from './types/queryResult'
import { default as Record } from './types/record'
import { default as RecordData } from './types/recordData'
import { default as RecordInfo } from './types/recordInfo'
import { default as SignedDocument } from './types/signedDocument'
import { default as SigningKey } from './types/signingKey'

const DEFAULT_QUERY_COUNT = 100
const DEFAULT_API_URL = 'https://api.e3db.com'
const EMAIL = /(.+)@(.+){2,}\.(.+){2,}/

/* eslint-disable no-buffer-constructor */

/**
* Fallback polyfill to allow for HTTP Basic authentication from either Node
* or browser-based JavaScript.
Expand All @@ -59,6 +63,8 @@ let btoa = function(str) {
return new Buffer(str).toString('base64')
}

/* eslint-enable */

/**
* Potentially refresh the authorization token used during requests to the E3DB server.
*
Expand Down Expand Up @@ -134,14 +140,9 @@ async function checkStatus(response) {
* @param {string} readerId Authorized reader
* @param {string} type Record type for which the key will be used
*
* @returns {Promise<string|null>} Decrypted access key on success, NULL if no key exists.
* @returns {Promise<EAKInfo|null>} Encrypted access key on success, NULL if no key exists.
*/
async function getAccessKey(client, writerId, userId, readerId, type) {
let cacheKey = `${writerId}.${userId}.${type}`
if (client._akCache[cacheKey] !== undefined) {
return Promise.resolve(client._akCache[cacheKey])
}

async function getEncryptedAccessKey(client, writerId, userId, readerId, type) {
let response = await oauthFetch(
client,
client.config.apiUrl +
Expand All @@ -167,9 +168,39 @@ async function getAccessKey(client, writerId, userId, readerId, type) {

return checkStatus(response)
.then(response => response.json())
.then(eak => Crypto.decryptEak(client.config.privateKey, eak))
.then(eak => EAKInfo.decode(eak))
}

/**
* Retrieve an access key from the server.
*
* @param {Client} client E3DB client instance
* @param {string} writerId Writer/Authorizer for the access key
* @param {string} userId Record subject
* @param {string} readerId Authorized reader
* @param {string} type Record type for which the key will be used
*
* @returns {Promise<string|null>} Decrypted access key on success, NULL if no key exists.
*/
async function getAccessKey(client, writerId, userId, readerId, type) {
let cacheKey = `${writerId}.${userId}.${type}`
if (client._akCache[cacheKey] !== undefined) {
return Promise.resolve(client._akCache[cacheKey])
}

return getEncryptedAccessKey(client, writerId, userId, readerId, type)
.then(eak => {
if (eak === null) {
return Promise.resolve(null)
}

return Crypto.decryptEak(client.config.privateKey, eak)
})
.then(key => {
client._akCache[cacheKey] = key
if (key !== null) {
client._akCache[cacheKey] = key
}

return Promise.resolve(key)
})
}
Expand Down Expand Up @@ -331,6 +362,12 @@ export default class Client {
* Get an access key from the cache if it exists, otherwise decrypt
* the provided EAK and populate the cache.
*
* @param {string} writerId
* @param {string} userId
* @param {string} readerId
* @param {string} type
* @param {EAKInfo} eak
*
* @returns {Promise<string>}
*/
async _getCachedAk(writerId, userId, readerId, type, eak) {
Expand Down Expand Up @@ -371,6 +408,60 @@ export default class Client {
return ClientInfo.decode(json)
}

/**
* Create a key for the current client as a writer if one does not exist
* in the cache already. If no access key does exist, create a random one
* and store it with the server.
*
* @param {string} type Record type for this key
*
* @returns {Promise<EAKInfo>}
*/
async createWriterKey(type) {
let ak = await getAccessKey(
this,
this.config.clientId,
this.config.clientId,
this.config.clientId,
type
)

if (ak === null) {
ak = await Crypto.randomKey()
await putAccessKey(
this,
this.config.clientId,
this.config.clientId,
this.config.clientId,
type,
ak
)
}

let eak = await Crypto.encryptAk(this.config.privateKey, ak, this.config.publicKey)

return new EAKInfo(
eak,
this.config.clientId,
this.confing.publicKey,
this.config.clientId,
this.config.publicSignKey
)
}

/**
* Get a key for the current client as the reader of a specific record written by someone else.
*
* @param {string} writerId Writer of the record in the database
* @param {string} userID Subject of the record in the database
* @param {string} type Type of record
*
* @returns {Promise<EAKInfo>}
*/
async getReaderKey(writerId, userId, type) {
return getEncryptedAccessKey(this, writerId, userId, this.config.clientId, type)
}

/**
* Retrieve information about a client, primarily its UUID and public key,
* based either on an already-known client ID or a discoverable client
Expand Down Expand Up @@ -541,12 +632,16 @@ export default class Client {
* Decrypt an encrypted record using the AK wrapped and encrypted for the current
* client. The key will be cached for future use.
*
* @param {Record} record Record instance with encrypted data for decryption
* @param {object} eak Encrypted access key instance
* @param {Record} record Record instance with encrypted data for decryption
* @param {EAKInfo} eak Encrypted access key instance
*
* @returns {Promise<Record>}
*/
async decrypt(record, eak) {
if (eak.signerSigningKey === null) {
throw new Error('EAKInfo has no signing key!')
}

let ak = await this._getCachedAk(
record.meta.writerId,
record.meta.userId,
Expand All @@ -555,7 +650,16 @@ export default class Client {
eak
)

return Crypto.decryptRecord(record, ak)
let decrypted = await Crypto.decryptRecord(record, ak)
let info = new RecordInfo(decrypted.meta, decrypted.data)
let signed = new SignedDocument(info, decrypted.signature)

let verify = await this.verify(signed, eak.signerSigningKey.ed25519)
if (!verify) {
throw new Error('Document failed verification')
}

return decrypted
}

/**
Expand Down Expand Up @@ -774,7 +878,8 @@ export default class Client {
async share(type, readerId) {
if (readerId === this.config.clientId) {
return Promise.resolve(true)
} else if (EMAIL.test(readerId)) {
}
if (EMAIL.test(readerId)) {
let clientInfo = await this.clientInfo(readerId)
return this.share(type, clientInfo.clientId)
}
Expand Down Expand Up @@ -819,7 +924,8 @@ export default class Client {
async revoke(type, readerId) {
if (readerId === this.config.clientId) {
return Promise.resolve(true)
} else if (EMAIL.test(readerId)) {
}
if (EMAIL.test(readerId)) {
let clientInfo = await this.clientInfo(readerId)
return this.revoke(type, clientInfo.clientId)
}
Expand Down
4 changes: 2 additions & 2 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ export default class Crypto {
* to further decrypt a protected record.
*
* @param {string} readerKey Base64url-encoded private key for the reader (current client)
* @param {object} encryptedAk Encrypted access key
* @param {EAKInfo} encryptedAk Encrypted access key
*
* @return {Promise<string>} Raw binary string of the access key
*/
static async decryptEak(readerKey, encryptedAk) {
await sodium.ready
let encodedEak = encryptedAk.eak
let publicKey = await this.b64decode(encryptedAk.authorizer_public_key.curve25519)
let publicKey = await this.b64decode(encryptedAk.authorizerPublicKey.curve25519)
let privateKey = await this.b64decode(readerKey)

let [eak, nonce] = await Promise.all(
Expand Down

0 comments on commit a8ce4ce

Please sign in to comment.