From 0e7101af53e926a907d54294456519c9e5e7c5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Wed, 22 Apr 2020 13:57:33 +0200 Subject: [PATCH 01/25] chore: change pds memory access token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clarifying why no token is needed when using memory-pds Co-authored-by: Adam Näslund --- __e2e__/src/phone.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/__e2e__/src/phone.js b/__e2e__/src/phone.js index 32c23432..bbff6a05 100644 --- a/__e2e__/src/phone.js +++ b/__e2e__/src/phone.js @@ -7,7 +7,10 @@ import Config from 'react-native-config' import AsyncStorage from '@react-native-community/async-storage' export async function createAccount() { - const pds = { provider: 'memory', access_token: 'nope' } + const pds = { + provider: 'memory', + access_token: 'not needed for provider: memory', + } const acc = { pds, } From 81478e02cc9fa01668ef455ceac40db050c1b426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Wed, 22 Apr 2020 13:59:16 +0200 Subject: [PATCH 02/25] refactor: createPermissionResults make it more readable by removing intermediate function --- lib/services/auth.js | 17 ++++++----------- lib/services/storage.js | 3 ++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/services/auth.js b/lib/services/auth.js index 37cc195a..8dff919b 100644 --- a/lib/services/auth.js +++ b/lib/services/auth.js @@ -104,18 +104,9 @@ export async function createPermissionResult({ permissions }, approved) { return undefined } - const withJwk = async () => { - const { privateKey, privateKeyPem } = await generateKey({ - use: 'enc', - }) - await storeKey({ privateKey, privateKeyPem }) - const publicKey = toPublicKey(privateKey) - return publicKey - } - const readServiceReadKeysByArea = mapReadKeys(permissions) - const permissionResult = await { + const permissionResult = { approved: await Promise.all( permissions .filter(p => approved.get(p.id)) @@ -135,7 +126,11 @@ export async function createPermissionResult({ permissions }, approved) { } // push user read-key - p.jwks.keys.push(await withJwk()) + const key = await generateKey({ + use: 'enc', + }).then(storeKey) + + p.jwks.keys.push(toPublicKey(key.privateKey)) } else if (p.type === 'READ') { p.kid = p.jwk.kid delete p.jwk diff --git a/lib/services/storage.js b/lib/services/storage.js index fcf9826f..869822c5 100644 --- a/lib/services/storage.js +++ b/lib/services/storage.js @@ -41,10 +41,11 @@ export const storeConnection = async ({ serviceId, ...rest }) => { } export const storeKey = async ({ privateKey, privateKeyPem }) => { - return AsyncStorage.setItem( + await AsyncStorage.setItem( `jwks/${privateKey.kid}`, JSON.stringify({ privateKey, privateKeyPem }), ) + return { privateKey, privateKeyPem } } export const getPrivateKey = async kid => { From b91717a606d3b7b87468cab7093adf44226969df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Wed, 22 Apr 2020 15:38:42 +0200 Subject: [PATCH 03/25] refactor: rename tokens.js to messages.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Näslund --- lib/services/__tests__/account.test.js | 21 ++++++++++++--------- lib/services/__tests__/auth.test.js | 18 +++++++++--------- lib/services/account.js | 2 +- lib/services/auth.js | 8 ++++---- lib/services/{tokens.js => messages.js} | 0 5 files changed, 26 insertions(+), 23 deletions(-) rename lib/services/{tokens.js => messages.js} (100%) diff --git a/lib/services/__tests__/account.test.js b/lib/services/__tests__/account.test.js index 90f99dc8..4d9ee386 100644 --- a/lib/services/__tests__/account.test.js +++ b/lib/services/__tests__/account.test.js @@ -3,7 +3,7 @@ import Config from 'react-native-config' import * as uuid from 'uuid' import * as crypto from '../crypto' import * as storage from '../storage' -import * as tokens from '../tokens' +import * as messages from '../messages' import { generateTestAccount } from './_helpers' jest.mock('uuid', () => ({ @@ -23,10 +23,10 @@ jest.mock('../storage', () => ({ .mockName('storage.storeKey') .mockResolvedValue(), })) -jest.mock('../tokens', () => ({ +jest.mock('../messages', () => ({ createAccountRegistration: jest .fn() - .mockName('tokens.createAccountRegistration'), + .mockName('messages.createAccountRegistration'), })) Config.OPERATOR_URL = 'aTotallyLegitOperatorUrl' @@ -67,7 +67,7 @@ describe('account', () => { privateKeyPem, }) crypto.toPublicKey.mockReturnValue(publicKey) - tokens.createAccountRegistration.mockResolvedValue(jwt) + messages.createAccountRegistration.mockResolvedValue(jwt) fetch.mockResponse(JSON.stringify({ data: { id: 'abc123' } })) }) describe('new user', () => { @@ -95,11 +95,14 @@ describe('account', () => { }) it('generates a token', async () => { const result = await accountService.save(account) - expect(tokens.createAccountRegistration).toHaveBeenCalledWith(result, { - publicKey, - privateKey, - privateKeyPem, - }) + expect(messages.createAccountRegistration).toHaveBeenCalledWith( + result, + { + publicKey, + privateKey, + privateKeyPem, + }, + ) }) it('saves account to storage', async () => { const result = await accountService.save(account) diff --git a/lib/services/__tests__/auth.test.js b/lib/services/__tests__/auth.test.js index 8c61d2bf..b2818826 100644 --- a/lib/services/__tests__/auth.test.js +++ b/lib/services/__tests__/auth.test.js @@ -7,13 +7,13 @@ import { getAccount } from '../account' import { generateTestAccount } from './_helpers' import { generateKey } from '../crypto' import Config from 'react-native-config' // this is mocked -import * as tokens from '../tokens' +import * as messages from '../messages' jest.mock('../account', () => ({ getAccount: jest.fn(), })) -jest.mock('../tokens', () => ({ +jest.mock('../messages', () => ({ createConnectionInit: jest .fn() .mockResolvedValue('jwt-for-createConnectionInit'), @@ -256,16 +256,16 @@ describe('auth', () => { afterEach(() => { generateKey.mockReset() }) - it('calls tokens.createConnection with correct arguments for empty approved', async () => { + it('calls messages.createConnection with correct arguments for empty approved', async () => { await approveConnection(connectionRequest, new Map()) - expect(tokens.createConnection).toHaveBeenCalledWith( + expect(messages.createConnection).toHaveBeenCalledWith( connectionRequest, undefined, expect.any(String), ) }) - it('calls tokens.createConnection with correct arguments, some approved', async () => { + it('calls messages.createConnection with correct arguments, some approved', async () => { const approved = new Map([ ['18710e28-7d6c-49cf-941e-0f954bb179ae', true], ['1712ec0c-9ae6-472f-9e14-46088e51f505', true], @@ -275,7 +275,7 @@ describe('auth', () => { ]) await approveConnection(connectionRequest, approved) - expect(tokens.createConnection).toHaveBeenCalledWith( + expect(messages.createConnection).toHaveBeenCalledWith( connectionRequest, { approved: expect.any(Array), @@ -283,10 +283,10 @@ describe('auth', () => { expect.any(String), ) - const permissions = tokens.createConnection.mock.calls[0][1] + const permissions = messages.createConnection.mock.calls[0][1] expect(permissions.approved).toHaveLength(4) }) - it('calls tokens.createConnection with correct arguments, none approved', async () => { + it('calls messages.createConnection with correct arguments, none approved', async () => { const approved = new Map([ ['18710e28-7d6c-49cf-941e-0f954bb179ae', false], ['1712ec0c-9ae6-472f-9e14-46088e51f505', false], @@ -296,7 +296,7 @@ describe('auth', () => { ]) await approveConnection(connectionRequest, approved) - expect(tokens.createConnection).toHaveBeenCalledWith( + expect(messages.createConnection).toHaveBeenCalledWith( connectionRequest, undefined, expect.any(String), diff --git a/lib/services/account.js b/lib/services/account.js index 8f7cc425..6de05cd1 100644 --- a/lib/services/account.js +++ b/lib/services/account.js @@ -2,7 +2,7 @@ import Config from 'react-native-config' import { v4 } from 'uuid' import { generateKey, toPublicKey } from './crypto' import { storeAccount, storeKey } from './storage' -import { createAccountRegistration } from './tokens' +import { createAccountRegistration } from './messages' export async function save(account) { try { diff --git a/lib/services/auth.js b/lib/services/auth.js index 8dff919b..26ffb6e8 100644 --- a/lib/services/auth.js +++ b/lib/services/auth.js @@ -7,7 +7,7 @@ import { createConnectionResponse, createLogin, createLoginResponse, -} from './tokens' +} from './messages' import { v4 } from 'uuid' import { generateKey, toPublicKey } from './crypto' @@ -30,9 +30,9 @@ export const initConnection = async authRequest => { headers: { 'content-type': 'application/jwt' }, body: connectionInit, }) - const data = await response.text() - const { payload } = await verify(data) - return payload + const JWTConnectionRequest = await response.text() + const { payload: connectionRequest } = await verify(JWTConnectionRequest) + return connectionRequest } catch (error) { console.error(error) throw Error('CONNECTION_INIT failed') diff --git a/lib/services/tokens.js b/lib/services/messages.js similarity index 100% rename from lib/services/tokens.js rename to lib/services/messages.js From 42dd13320925534e85ba713d2efba00dfec3d533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Wed, 22 Apr 2020 17:03:02 +0200 Subject: [PATCH 04/25] refactor: change messages.js to operatorAdapter --- lib/services/__tests__/account.test.js | 10 +++---- lib/services/__tests__/auth.test.js | 20 +++++++------ lib/services/account.js | 2 +- lib/services/auth.js | 5 ++-- .../{messages.js => operatorAdapter.js} | 30 +++++-------------- lib/services/serviceAdapter.js | 24 +++++++++++++++ 6 files changed, 52 insertions(+), 39 deletions(-) rename lib/services/{messages.js => operatorAdapter.js} (83%) create mode 100644 lib/services/serviceAdapter.js diff --git a/lib/services/__tests__/account.test.js b/lib/services/__tests__/account.test.js index 4d9ee386..5c3ab33f 100644 --- a/lib/services/__tests__/account.test.js +++ b/lib/services/__tests__/account.test.js @@ -3,7 +3,7 @@ import Config from 'react-native-config' import * as uuid from 'uuid' import * as crypto from '../crypto' import * as storage from '../storage' -import * as messages from '../messages' +import * as operatorAdapter from '../operatorAdapter' import { generateTestAccount } from './_helpers' jest.mock('uuid', () => ({ @@ -23,10 +23,10 @@ jest.mock('../storage', () => ({ .mockName('storage.storeKey') .mockResolvedValue(), })) -jest.mock('../messages', () => ({ +jest.mock('../operatorAdapter', () => ({ createAccountRegistration: jest .fn() - .mockName('messages.createAccountRegistration'), + .mockName('operatorAdapter.createAccountRegistration'), })) Config.OPERATOR_URL = 'aTotallyLegitOperatorUrl' @@ -67,7 +67,7 @@ describe('account', () => { privateKeyPem, }) crypto.toPublicKey.mockReturnValue(publicKey) - messages.createAccountRegistration.mockResolvedValue(jwt) + operatorAdapter.createAccountRegistration.mockResolvedValue(jwt) fetch.mockResponse(JSON.stringify({ data: { id: 'abc123' } })) }) describe('new user', () => { @@ -95,7 +95,7 @@ describe('account', () => { }) it('generates a token', async () => { const result = await accountService.save(account) - expect(messages.createAccountRegistration).toHaveBeenCalledWith( + expect(operatorAdapter.createAccountRegistration).toHaveBeenCalledWith( result, { publicKey, diff --git a/lib/services/__tests__/auth.test.js b/lib/services/__tests__/auth.test.js index b2818826..fd8a2bae 100644 --- a/lib/services/__tests__/auth.test.js +++ b/lib/services/__tests__/auth.test.js @@ -7,16 +7,18 @@ import { getAccount } from '../account' import { generateTestAccount } from './_helpers' import { generateKey } from '../crypto' import Config from 'react-native-config' // this is mocked -import * as messages from '../messages' +import * as operatorAdapter from '../operatorAdapter' jest.mock('../account', () => ({ getAccount: jest.fn(), })) -jest.mock('../messages', () => ({ +jest.mock('../serviceAdapter', () => ({ createConnectionInit: jest .fn() .mockResolvedValue('jwt-for-createConnectionInit'), +})) +jest.mock('../operatorAdapter', () => ({ createConnection: jest.fn().mockResolvedValue('jwt-for-createConnection'), createConnectionResponse: jest .fn() @@ -256,16 +258,16 @@ describe('auth', () => { afterEach(() => { generateKey.mockReset() }) - it('calls messages.createConnection with correct arguments for empty approved', async () => { + it('calls operatorAdapter.createConnection with correct arguments for empty approved', async () => { await approveConnection(connectionRequest, new Map()) - expect(messages.createConnection).toHaveBeenCalledWith( + expect(operatorAdapter.createConnection).toHaveBeenCalledWith( connectionRequest, undefined, expect.any(String), ) }) - it('calls messages.createConnection with correct arguments, some approved', async () => { + it('calls operatorAdapter.createConnection with correct arguments, some approved', async () => { const approved = new Map([ ['18710e28-7d6c-49cf-941e-0f954bb179ae', true], ['1712ec0c-9ae6-472f-9e14-46088e51f505', true], @@ -275,7 +277,7 @@ describe('auth', () => { ]) await approveConnection(connectionRequest, approved) - expect(messages.createConnection).toHaveBeenCalledWith( + expect(operatorAdapter.createConnection).toHaveBeenCalledWith( connectionRequest, { approved: expect.any(Array), @@ -283,10 +285,10 @@ describe('auth', () => { expect.any(String), ) - const permissions = messages.createConnection.mock.calls[0][1] + const permissions = operatorAdapter.createConnection.mock.calls[0][1] expect(permissions.approved).toHaveLength(4) }) - it('calls messages.createConnection with correct arguments, none approved', async () => { + it('calls operatorAdapter.createConnection with correct arguments, none approved', async () => { const approved = new Map([ ['18710e28-7d6c-49cf-941e-0f954bb179ae', false], ['1712ec0c-9ae6-472f-9e14-46088e51f505', false], @@ -296,7 +298,7 @@ describe('auth', () => { ]) await approveConnection(connectionRequest, approved) - expect(messages.createConnection).toHaveBeenCalledWith( + expect(operatorAdapter.createConnection).toHaveBeenCalledWith( connectionRequest, undefined, expect.any(String), diff --git a/lib/services/account.js b/lib/services/account.js index 6de05cd1..18aa11d5 100644 --- a/lib/services/account.js +++ b/lib/services/account.js @@ -2,7 +2,7 @@ import Config from 'react-native-config' import { v4 } from 'uuid' import { generateKey, toPublicKey } from './crypto' import { storeAccount, storeKey } from './storage' -import { createAccountRegistration } from './messages' +import { createAccountRegistration } from './operatorAdapter' export async function save(account) { try { diff --git a/lib/services/auth.js b/lib/services/auth.js index 26ffb6e8..785fbe09 100644 --- a/lib/services/auth.js +++ b/lib/services/auth.js @@ -1,13 +1,14 @@ import { verify } from './jwt' import { getConnection, storeConnection, storeKey } from './storage' import Config from 'react-native-config' +import { createConnectionInit } from './serviceAdapter' + import { - createConnectionInit, createConnection, createConnectionResponse, createLogin, createLoginResponse, -} from './messages' +} from './operatorAdapter' import { v4 } from 'uuid' import { generateKey, toPublicKey } from './crypto' diff --git a/lib/services/messages.js b/lib/services/operatorAdapter.js similarity index 83% rename from lib/services/messages.js rename to lib/services/operatorAdapter.js index 8b5c4813..9551adc6 100644 --- a/lib/services/messages.js +++ b/lib/services/operatorAdapter.js @@ -3,8 +3,6 @@ import { getAccount, getAccountKeys } from './storage' import Config from 'react-native-config' import { schemas } from '@egendata/messaging' -const nowSeconds = () => Math.floor(Date.now() / 1000) - export const createAccountRegistration = async ( // eslint-disable-next-line camelcase { id, pds: { provider, access_token } }, @@ -28,26 +26,6 @@ export const createAccountRegistration = async ( ) } -export const createConnectionInit = async ({ aud, iss, sid }) => { - const { publicKey, privateKey, privateKeyPem } = await getAccountKeys() - const now = nowSeconds() - return sign( - { - type: 'CONNECTION_INIT', - aud: iss, - iss: aud, - sid, - iat: now, - exp: now + 60, - }, - { - jwk: privateKey, - pem: privateKeyPem, - }, - { jwk: publicKey, alg: schemas.algs[0] }, - ) -} - export const createConnection = async ( { iss, sid }, permissions, @@ -131,3 +109,11 @@ export const createLoginResponse = async loginPayload => { { jwk: publicKey, alg: schemas.algs[0] }, ) } + +// function postToOperator(body) { +// return fetch(Config.OPERATOR_URL, { +// method: 'POST', +// headers: { 'content-type': 'application/jwt' }, +// body, +// }) +// } diff --git a/lib/services/serviceAdapter.js b/lib/services/serviceAdapter.js new file mode 100644 index 00000000..63e271fd --- /dev/null +++ b/lib/services/serviceAdapter.js @@ -0,0 +1,24 @@ +import { sign } from './jwt' +import { schemas } from '@egendata/messaging' +import { getAccountKeys } from './storage' +const nowSeconds = () => Math.floor(Date.now() / 1000) + +export const createConnectionInit = async ({ aud, iss, sid }) => { + const { publicKey, privateKey, privateKeyPem } = await getAccountKeys() + const now = nowSeconds() + return sign( + { + type: 'CONNECTION_INIT', + aud: iss, + iss: aud, + sid, + iat: now, + exp: now + 60, + }, + { + jwk: privateKey, + pem: privateKeyPem, + }, + { jwk: publicKey, alg: schemas.algs[0] }, + ) +} From 650c2ca1e80dac0a3b6b953795b656161ce791d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Wed, 22 Apr 2020 18:46:51 +0200 Subject: [PATCH 05/25] chore: add method and test for getting recipients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Näslund --- .../__tests__/operatorAdapter.test.js | 29 +++++++++++++++ lib/services/operatorAdapter.js | 35 ++++++++++++++----- 2 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 lib/services/__tests__/operatorAdapter.test.js diff --git a/lib/services/__tests__/operatorAdapter.test.js b/lib/services/__tests__/operatorAdapter.test.js new file mode 100644 index 00000000..47fdbec7 --- /dev/null +++ b/lib/services/__tests__/operatorAdapter.test.js @@ -0,0 +1,29 @@ +import * as operatorAdapter from '../operatorAdapter' +jest.mock('../jwt', () => ({ + sign: jest.fn().mockResolvedValue(), +})) + +jest.mock('../storage', () => ({ + getAccountKeys: jest.fn().mockReturnValue({ + publicKey: 'pub key', + privateKey: 'priv key', + privateKeyPem: 'priv key pem', + }), + getConnection: jest.fn(), +})) + +describe('operatorAdapter', () => { + describe('getRecipients', () => { + it('creates RECIPIENTS_READ_REQUEST and sends it to operator', async () => { + await operatorAdapter.getRecipients({ + domain: 'myCV.work', + area: 'photos', + }) + expect(fetch).toHaveBeenCalledWith(undefined, { + headers: { 'content-type': 'application/jwt' }, + method: 'POST', + body: undefined, + }) + }) + }) +}) diff --git a/lib/services/operatorAdapter.js b/lib/services/operatorAdapter.js index 9551adc6..4721d229 100644 --- a/lib/services/operatorAdapter.js +++ b/lib/services/operatorAdapter.js @@ -1,5 +1,5 @@ import { sign } from './jwt' -import { getAccount, getAccountKeys } from './storage' +import { getAccount, getAccountKeys, getConnection } from './storage' import Config from 'react-native-config' import { schemas } from '@egendata/messaging' @@ -110,10 +110,29 @@ export const createLoginResponse = async loginPayload => { ) } -// function postToOperator(body) { -// return fetch(Config.OPERATOR_URL, { -// method: 'POST', -// headers: { 'content-type': 'application/jwt' }, -// body, -// }) -// } +export async function getRecipients({ domain, area }) { + const { publicKey, privateKey, privateKeyPem } = await getAccountKeys() + const connectionId = getConnection(domain) + + return sign( + { + type: 'RECIPIENTS_READ_REQUEST', + sub: connectionId, + paths: [{ domain, area }], + }, + { + jwk: privateKey, + pem: privateKeyPem, + }, + { jwk: publicKey, alg: schemas.algs[0] }, + ).then(postToOperator) +} +// ToOoOoToOT +// poopstoopidooperator +export function postToOperator(body) { + return fetch(Config.OPERATOR_URL, { + method: 'POST', + headers: { 'content-type': 'application/jwt' }, + body, + }) +} From f5f290785e5a42b18bec5a744d3f1bfd0c4a3ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Wed, 22 Apr 2020 18:47:03 +0200 Subject: [PATCH 06/25] chore: add todo comments --- lib/services/auth.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/services/auth.js b/lib/services/auth.js index 785fbe09..424bf803 100644 --- a/lib/services/auth.js +++ b/lib/services/auth.js @@ -133,6 +133,12 @@ export async function createPermissionResult({ permissions }, approved) { p.jwks.keys.push(toPublicKey(key.privateKey)) } else if (p.type === 'READ') { + // todo: l8r if there are no recipients then create JWE with empty data + + // todo: get jwe recipients + + // if there are then add it to the recipeintseassesez + p.kid = p.jwk.kid delete p.jwk } From f9eac54a5dd38dc553fb0c419b06c819b1d9619f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 23 Apr 2020 17:02:41 +0200 Subject: [PATCH 07/25] refactor: begin to refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Näslund --- ios/Podfile.lock | 10 ++-- jest.config.js | 6 ++ lib/services/__tests__/auth.test.js | 57 ++++++++++++------ lib/services/auth.js | 83 +------------------------- lib/services/operatorAdapter.js | 91 ++++++++++++++++++++++++++++- 5 files changed, 143 insertions(+), 104 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 526e36a2..7aae3298 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -372,10 +372,10 @@ SPEC CHECKSUMS: React-jsi: cb2cd74d7ccf4cffb071a46833613edc79cdf8f7 React-jsiexecutor: d5525f9ed5f782fdbacb64b9b01a43a9323d2386 React-jsinspector: fa0ecc501688c3c4c34f28834a76302233e29dc0 - react-native-camera: 88f19d7240667a8dae9d76414986c2893d712744 + react-native-camera: 9bf6e0cdce0d7dc059d9836eb381d96f58f83d83 react-native-config: 55548054279d92e0e4566ea15a8b9b81028ec342 react-native-jose: e85455676e6190291bdb61f6814c6036ba13fb3d - react-native-simple-crypto: e8aa2319a18926523ba389a654a78dcdf9aa6547 + react-native-simple-crypto: e1e958139bae411c5d76a2438b004f601639ac6e React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76 React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360 React-RCTBlob: d89293cc0236d9cb0933d85e430b0bbe81ad1d72 @@ -386,8 +386,8 @@ SPEC CHECKSUMS: React-RCTText: 9ccc88273e9a3aacff5094d2175a605efa854dbe React-RCTVibration: a49a1f42bf8f5acf1c3e297097517c6b3af377ad ReactCommon: 198c7c8d3591f975e5431bec1b0b3b581aa1c5dd - ReactNativePermissions: f3beb8871251594a8ea2c6b19a3f8252d5c7379d - RNCAsyncStorage: 2e2e3feb9bdadc752a026703d8c4065ca912e75a + ReactNativePermissions: f0aec11433d43d45953c678b6982921d83ad94c8 + RNCAsyncStorage: 621bad7a889b5bf1583a52547f2dcd3a4d1ff15e RNEmulatorCheck: 00924e42c62a5a3a8751aaaa167fab0c5b12fbb9 RNGestureHandler: 5329a942fce3d41c68b84c2c2276ce06a696d8b0 RNSVG: 8ba35cbeb385a52fd960fd28db9d7d18b4c2974f @@ -395,4 +395,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: fae4834878e9866413145d252fa5aaae0cfd59cb -COCOAPODS: 1.8.4 +COCOAPODS: 1.9.1 diff --git a/jest.config.js b/jest.config.js index 89f76f0a..03c3a466 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,4 +16,10 @@ module.exports = { '/__e2e__/', ], cacheDirectory: '.jest/cache', + transformIgnorePatterns: [ + 'node_modules/(?!(react-native|@egendata/react-native-jose|react-native-collapsible|react-native-modal|react-native-animatable)/)', + ], + unmockedModulePathPatterns: ['react-native-jose'], + autoMock: false, + verbose: true, } diff --git a/lib/services/__tests__/auth.test.js b/lib/services/__tests__/auth.test.js index fd8a2bae..5cf674b8 100644 --- a/lib/services/__tests__/auth.test.js +++ b/lib/services/__tests__/auth.test.js @@ -1,33 +1,52 @@ -import { - initConnection, - createPermissionResult, - approveConnection, -} from '../auth' +import { initConnection, approveConnection } from '../auth' import { getAccount } from '../account' import { generateTestAccount } from './_helpers' import { generateKey } from '../crypto' import Config from 'react-native-config' // this is mocked import * as operatorAdapter from '../operatorAdapter' +jest.unmock('@egendata/react-native-jose') +jest.mock('../jwt.js', () => { + const realReactNativeJose = jest.requireActual('react-native-jose') + return { + verify: realReactNativeJose.verify, + // sign: jest + // .fn() + // .mockResolvedValue() + // .mockResolvedValue('jwt-for-createConnectionResponse'), + // verify: jest.fn().mockResolvedValue({ payload: { foo: 'bar' } }), + } +}) jest.mock('../account', () => ({ getAccount: jest.fn(), })) +jest.mock('../storage', () => ({ + getAccountKeys: jest.fn().mockReturnValue({ + publicKey: 'pub key', + privateKey: 'priv key', + privateKeyPem: 'priv key pem', + }), + getAccount: jest.fn().mockReturnValue({ id: 1 }), + storeConnection: jest.fn(), +})) jest.mock('../serviceAdapter', () => ({ createConnectionInit: jest .fn() .mockResolvedValue('jwt-for-createConnectionInit'), })) -jest.mock('../operatorAdapter', () => ({ - createConnection: jest.fn().mockResolvedValue('jwt-for-createConnection'), - createConnectionResponse: jest - .fn() - .mockResolvedValue('jwt-for-createConnectionResponse'), -})) - -jest.mock('../jwt', () => ({ - verify: jest.fn().mockResolvedValue({ payload: { foo: 'bar' } }), -})) +jest.mock('../operatorAdapter', () => { + const operatorAdapterActual = jest.requireActual('../operatorAdapter.js') + return { + createConnection: jest.fn().mockResolvedValue('jwt-for-createConnection'), + getApprovedPermissionRequestWithKeys: + operatorAdapterActual.getApprovedPermissionRequestWithKeys, + newCreateConnection: operatorAdapterActual.newCreateConnection, + createConnectionResponse: jest + .fn() + .mockResolvedValue('jwt-for-createConnectionResponse'), + } +}) jest.mock('../crypto', () => { const actualCrypto = jest.requireActual('../crypto') @@ -175,7 +194,7 @@ describe('auth', () => { ], } }) - describe('#createPermissionResult', () => { + describe('#operatorAdapter.getApprovedPermissionRequestWithKeys', () => { beforeEach(() => { generateKey.mockResolvedValueOnce({ privateKey: appKeyBaseData }) generateKey.mockResolvedValueOnce({ privateKey: appKeyExperience }) @@ -245,7 +264,10 @@ describe('auth', () => { ['fc284cf5-b1af-4fac-b793-7d1adf8a9c60', true], ]) - const result = await createPermissionResult(connectionRequest, approved) + const result = await operatorAdapter.getApprovedPermissionRequestWithKeys( + connectionRequest, + approved, + ) expect(result).toEqual(expected) }) @@ -260,7 +282,6 @@ describe('auth', () => { }) it('calls operatorAdapter.createConnection with correct arguments for empty approved', async () => { await approveConnection(connectionRequest, new Map()) - expect(operatorAdapter.createConnection).toHaveBeenCalledWith( connectionRequest, undefined, diff --git a/lib/services/auth.js b/lib/services/auth.js index 424bf803..6e025d8d 100644 --- a/lib/services/auth.js +++ b/lib/services/auth.js @@ -1,16 +1,13 @@ import { verify } from './jwt' -import { getConnection, storeConnection, storeKey } from './storage' +import { getConnection, storeConnection } from './storage' import Config from 'react-native-config' import { createConnectionInit } from './serviceAdapter' import { - createConnection, - createConnectionResponse, createLogin, createLoginResponse, + newCreateConnection, } from './operatorAdapter' -import { v4 } from 'uuid' -import { generateKey, toPublicKey } from './crypto' export const authenticationRequestHandler = async ({ payload }) => { const existingConnection = await getConnection(payload.iss) @@ -42,24 +39,7 @@ export const initConnection = async authRequest => { export const approveConnection = async (connectionRequest, approved) => { try { - const connectionId = v4() - const permissionsResult = await createPermissionResult( - connectionRequest, - approved, - ) - const connection = await createConnection( - connectionRequest, - permissionsResult, - connectionId, - ) - - const connectionResponse = await createConnectionResponse(connection) - - await fetch(Config.OPERATOR_URL, { - method: 'POST', - headers: { 'content-type': 'application/jwt' }, - body: connectionResponse, - }) + const connectionId = await newCreateConnection(connectionRequest, approved) await storeConnection({ serviceId: connectionRequest.iss, @@ -91,60 +71,3 @@ export const approveLogin = async ({ connection, sessionId }) => { throw Error('Could not approve Login') } } - -function mapReadKeys(permissions) { - return permissions - .filter(p => p.type === 'READ') - .reduce((map, { domain, area, jwk }) => { - return map.set(`${domain}|${area}`, jwk) - }, new Map()) -} - -export async function createPermissionResult({ permissions }, approved) { - if (!permissions) { - return undefined - } - - const readServiceReadKeysByArea = mapReadKeys(permissions) - - const permissionResult = { - approved: await Promise.all( - permissions - .filter(p => approved.get(p.id)) - .map(async p => { - if (p.type === 'WRITE') { - if (!p.jwks) { - p.jwks = { - keys: [], - } - } - // push service read-keys to jwks-keys - const serviceReadKey = readServiceReadKeysByArea.get( - `${p.domain}|${p.area}`, - ) - if (serviceReadKey) { - p.jwks.keys.push(serviceReadKey) - } - - // push user read-key - const key = await generateKey({ - use: 'enc', - }).then(storeKey) - - p.jwks.keys.push(toPublicKey(key.privateKey)) - } else if (p.type === 'READ') { - // todo: l8r if there are no recipients then create JWE with empty data - - // todo: get jwe recipients - - // if there are then add it to the recipeintseassesez - - p.kid = p.jwk.kid - delete p.jwk - } - return p - }), - ), - } - return permissionResult.approved.length ? permissionResult : undefined -} diff --git a/lib/services/operatorAdapter.js b/lib/services/operatorAdapter.js index 4721d229..37633553 100644 --- a/lib/services/operatorAdapter.js +++ b/lib/services/operatorAdapter.js @@ -1,7 +1,9 @@ import { sign } from './jwt' -import { getAccount, getAccountKeys, getConnection } from './storage' +import { getAccount, getAccountKeys, getConnection, storeKey } from './storage' import Config from 'react-native-config' import { schemas } from '@egendata/messaging' +import { v4 } from 'uuid' +import { generateKey, toPublicKey } from './crypto' export const createAccountRegistration = async ( // eslint-disable-next-line camelcase @@ -136,3 +138,90 @@ export function postToOperator(body) { body, }) } + +export async function newCreateConnection( + connectionRequest, + approvedPermissions, +) { + const connectionId = v4() + + const permissionsResult = await getApprovedPermissionRequestWithKeys( + connectionRequest, + approvedPermissions, + ) + const connection = await createConnection( + connectionRequest, + permissionsResult, + connectionId, + ) + + const connectionResponse = await createConnectionResponse(connection) + + await fetch(Config.OPERATOR_URL, { + method: 'POST', + headers: { 'content-type': 'application/jwt' }, + body: connectionResponse, + }) + + return connectionId +} + +export async function getApprovedPermissionRequestWithKeys( + { permissions: requestedPermissions }, + approved, +) { + if (!requestedPermissions) { + return undefined + } + + const readServiceReadKeysByArea = mapReadKeys(requestedPermissions) + + const permissionResult = { + approved: await Promise.all( + requestedPermissions + .filter(p => approved.get(p.id)) + .map(async p => { + if (p.type === 'WRITE') { + if (!p.jwks) { + p.jwks = { + keys: [], + } + } + // push service read-keys to jwks-keys + const serviceReadKey = readServiceReadKeysByArea.get( + `${p.domain}|${p.area}`, + ) + if (serviceReadKey) { + p.jwks.keys.push(serviceReadKey) + } + + // push user read-key + const key = await generateKey({ + use: 'enc', + }).then(storeKey) + + p.jwks.keys.push(toPublicKey(key.privateKey)) + } else if (p.type === 'READ') { + // todo: l8r if there are no recipients then create JWE with empty data + + // todo: get jwe recipients + + // if there are then add it to the recipeintseassesez + + p.kid = p.jwk.kid + delete p.jwk + } + return p + }), + ), + } + return permissionResult.approved.length ? permissionResult : undefined +} + +function mapReadKeys(permissions) { + return permissions + .filter(p => p.type === 'READ') + .reduce((map, { domain, area, jwk }) => { + return map.set(`${domain}|${area}`, jwk) + }, new Map()) +} From 6b50987f7d1a076c2ce6090e9137b50e1148f82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 30 Apr 2020 10:11:23 +0200 Subject: [PATCH 08/25] chore: give up trying to unmock libs for jest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Näslund --- jest.config.js | 6 --- lib/services/__tests__/auth.test.js | 83 ++++++++++++++++------------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/jest.config.js b/jest.config.js index 03c3a466..89f76f0a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,10 +16,4 @@ module.exports = { '/__e2e__/', ], cacheDirectory: '.jest/cache', - transformIgnorePatterns: [ - 'node_modules/(?!(react-native|@egendata/react-native-jose|react-native-collapsible|react-native-modal|react-native-animatable)/)', - ], - unmockedModulePathPatterns: ['react-native-jose'], - autoMock: false, - verbose: true, } diff --git a/lib/services/__tests__/auth.test.js b/lib/services/__tests__/auth.test.js index 5cf674b8..3cc62912 100644 --- a/lib/services/__tests__/auth.test.js +++ b/lib/services/__tests__/auth.test.js @@ -5,18 +5,20 @@ import { generateKey } from '../crypto' import Config from 'react-native-config' // this is mocked import * as operatorAdapter from '../operatorAdapter' -jest.unmock('@egendata/react-native-jose') -jest.mock('../jwt.js', () => { - const realReactNativeJose = jest.requireActual('react-native-jose') - return { - verify: realReactNativeJose.verify, - // sign: jest - // .fn() - // .mockResolvedValue() - // .mockResolvedValue('jwt-for-createConnectionResponse'), - // verify: jest.fn().mockResolvedValue({ payload: { foo: 'bar' } }), - } -}) +jest.mock('../jwt.js', () => ({ + sign: jest.fn().mockImplementation(input => { + const type = input.type + if (type === 'CONNECTION') { + return { + payload: 'connection_payload', + permissions: input.permissions, + } + } else if (type === 'CONNECTION_RESPONSE') { + return input.payload + } + }), + verify: jest.fn().mockResolvedValue({ payload: { foo: 'bar' } }), +})) jest.mock('../account', () => ({ getAccount: jest.fn(), })) @@ -280,14 +282,21 @@ describe('auth', () => { afterEach(() => { generateKey.mockReset() }) - it('calls operatorAdapter.createConnection with correct arguments for empty approved', async () => { + + it('sends payload to operator without permissions, if no permissions approved', async () => { await approveConnection(connectionRequest, new Map()) - expect(operatorAdapter.createConnection).toHaveBeenCalledWith( - connectionRequest, - undefined, - expect.any(String), - ) + expect(fetch).toHaveBeenCalledWith('https://smoothoperator.work', { + body: { + payload: 'connection_payload', + permissions: undefined, + }, + headers: { + 'content-type': 'application/jwt', + }, + method: 'POST', + }) }) + it('calls operatorAdapter.createConnection with correct arguments, some approved', async () => { const approved = new Map([ ['18710e28-7d6c-49cf-941e-0f954bb179ae', true], @@ -298,16 +307,18 @@ describe('auth', () => { ]) await approveConnection(connectionRequest, approved) - expect(operatorAdapter.createConnection).toHaveBeenCalledWith( - connectionRequest, - { - approved: expect.any(Array), + expect(fetch).toHaveBeenCalledWith('https://smoothoperator.work', { + body: { + payload: 'connection_payload', + permissions: { + approved: expect.any(Array), + }, }, - expect.any(String), - ) - - const permissions = operatorAdapter.createConnection.mock.calls[0][1] - expect(permissions.approved).toHaveLength(4) + headers: { + 'content-type': 'application/jwt', + }, + method: 'POST', + }) }) it('calls operatorAdapter.createConnection with correct arguments, none approved', async () => { const approved = new Map([ @@ -319,19 +330,15 @@ describe('auth', () => { ]) await approveConnection(connectionRequest, approved) - expect(operatorAdapter.createConnection).toHaveBeenCalledWith( - connectionRequest, - undefined, - expect.any(String), - ) - }) - it('posts to correct url with correct content type', async () => { - await approveConnection(connectionRequest, new Map()) - expect(fetch).toHaveBeenCalledWith('https://smoothoperator.work', { - headers: { 'content-type': 'application/jwt' }, + body: { + payload: 'connection_payload', + permissions: undefined, + }, + headers: { + 'content-type': 'application/jwt', + }, method: 'POST', - body: 'jwt-for-createConnectionResponse', }) }) }) From c3363845ee708c2d132442bd3519f2ed14d0b53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 30 Apr 2020 10:50:17 +0200 Subject: [PATCH 09/25] refactor: finish refactoring of createConnection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Näslund --- lib/services/__tests__/auth.test.js | 10 +++++---- lib/services/auth.js | 4 ++-- lib/services/operatorAdapter.js | 34 +++++++++++------------------ 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/lib/services/__tests__/auth.test.js b/lib/services/__tests__/auth.test.js index 3cc62912..1716b496 100644 --- a/lib/services/__tests__/auth.test.js +++ b/lib/services/__tests__/auth.test.js @@ -40,10 +40,12 @@ jest.mock('../serviceAdapter', () => ({ jest.mock('../operatorAdapter', () => { const operatorAdapterActual = jest.requireActual('../operatorAdapter.js') return { - createConnection: jest.fn().mockResolvedValue('jwt-for-createConnection'), + createConnectionMessage: jest + .fn() + .mockResolvedValue('jwt-for-createConnection'), getApprovedPermissionRequestWithKeys: operatorAdapterActual.getApprovedPermissionRequestWithKeys, - newCreateConnection: operatorAdapterActual.newCreateConnection, + createConnection: operatorAdapterActual.createConnection, createConnectionResponse: jest .fn() .mockResolvedValue('jwt-for-createConnectionResponse'), @@ -297,7 +299,7 @@ describe('auth', () => { }) }) - it('calls operatorAdapter.createConnection with correct arguments, some approved', async () => { + it('calls fetch with correct arguments, some approved', async () => { const approved = new Map([ ['18710e28-7d6c-49cf-941e-0f954bb179ae', true], ['1712ec0c-9ae6-472f-9e14-46088e51f505', true], @@ -320,7 +322,7 @@ describe('auth', () => { method: 'POST', }) }) - it('calls operatorAdapter.createConnection with correct arguments, none approved', async () => { + it('calls fetch with correct arguments, none approved', async () => { const approved = new Map([ ['18710e28-7d6c-49cf-941e-0f954bb179ae', false], ['1712ec0c-9ae6-472f-9e14-46088e51f505', false], diff --git a/lib/services/auth.js b/lib/services/auth.js index 6e025d8d..4e07f54a 100644 --- a/lib/services/auth.js +++ b/lib/services/auth.js @@ -6,7 +6,7 @@ import { createConnectionInit } from './serviceAdapter' import { createLogin, createLoginResponse, - newCreateConnection, + createConnection, } from './operatorAdapter' export const authenticationRequestHandler = async ({ payload }) => { @@ -39,7 +39,7 @@ export const initConnection = async authRequest => { export const approveConnection = async (connectionRequest, approved) => { try { - const connectionId = await newCreateConnection(connectionRequest, approved) + const connectionId = await createConnection(connectionRequest, approved) await storeConnection({ serviceId: connectionRequest.iss, diff --git a/lib/services/operatorAdapter.js b/lib/services/operatorAdapter.js index 37633553..395ef14a 100644 --- a/lib/services/operatorAdapter.js +++ b/lib/services/operatorAdapter.js @@ -28,7 +28,7 @@ export const createAccountRegistration = async ( ) } -export const createConnection = async ( +export const createConnectionMessage = async ( { iss, sid }, permissions, connectionId, @@ -139,31 +139,23 @@ export function postToOperator(body) { }) } -export async function newCreateConnection( - connectionRequest, - approvedPermissions, -) { +export async function createConnection(connectionRequest, approvedPermissions) { const connectionId = v4() - const permissionsResult = await getApprovedPermissionRequestWithKeys( + return getApprovedPermissionRequestWithKeys( connectionRequest, approvedPermissions, ) - const connection = await createConnection( - connectionRequest, - permissionsResult, - connectionId, - ) - - const connectionResponse = await createConnectionResponse(connection) - - await fetch(Config.OPERATOR_URL, { - method: 'POST', - headers: { 'content-type': 'application/jwt' }, - body: connectionResponse, - }) - - return connectionId + .then(permissionsResult => + createConnectionMessage( + connectionRequest, + permissionsResult, + connectionId, + ), + ) + .then(createConnectionResponse) + .then(postToOperator) + .then(() => connectionId) } export async function getApprovedPermissionRequestWithKeys( From 58e0aa6a732b294967ea15c5f11fc0748e28b29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 28 May 2020 14:50:53 +0200 Subject: [PATCH 10/25] chore: fix e2e:watch command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adam Näslund --- __e2e__/README.md | 5 +++++ package.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/__e2e__/README.md b/__e2e__/README.md index e18bed42..ca879d17 100644 --- a/__e2e__/README.md +++ b/__e2e__/README.md @@ -7,3 +7,8 @@ const phone = require(`${phone_e2e_dist_dir}/`) phone.Config.OPERATOR_URL = 'http://localhost:3000/api' await phone.createAccount() ``` + + +## React native modules in e2e-tests + +To simplify and speed up some javascript implementations of react native functions including cryptographic JOSE functions, some modules are replaced in the test scenario, with code from the /src directory, using webpack. Similar to how the JOSE functions are also replaced for platform specific code when building for IOS and Android. \ No newline at end of file diff --git a/package.json b/package.json index ae454ffc..71735b82 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "lint": "eslint '**/*.js'", "prettier": "eslint '**/*.js' --fix", "e2e:build": "cd __e2e__/ && webpack", - "e2e:start": "node __e2e__/dist/index.js", - "e2e:watch": "npm run e2e:build && nodemon __e2e__/dist/ --watch __e2e__/dist/" + "e2e:start": "OPERATOR_URL=http://localhost:3000 node __e2e__/dist/index.js", + "e2e:watch": "nodemon --ignore __e2e__/dist/ --exec 'npm run e2e:build && npm run e2e:start'" }, "contributors": [ "Adam Näslund ", From 2b41fe9f205ebdf2dab6cc2a85d27526d1c7b2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 28 May 2020 14:53:35 +0200 Subject: [PATCH 11/25] chore: add flow for adding recipients to JWE --- __e2e__/src/native/react-native-jose.js | 45 +++++++++++ .../__tests__/operatorAdapter.test.js | 76 ++++++++++++++++++ lib/services/operatorAdapter.js | 80 ++++++++++++++++--- 3 files changed, 188 insertions(+), 13 deletions(-) diff --git a/__e2e__/src/native/react-native-jose.js b/__e2e__/src/native/react-native-jose.js index c6fb6e1f..aef5db39 100644 --- a/__e2e__/src/native/react-native-jose.js +++ b/__e2e__/src/native/react-native-jose.js @@ -1,6 +1,51 @@ import * as jwt from 'jwt-lite' +import { privateDecrypt, publicEncrypt } from 'crypto' export const sign = (payload, keys, header) => jwt.sign(payload, keys.jwk, header) export const verify = (token, jwk) => jwt.verify(token, jwk) export const decode = (token, options) => jwt.decode(token, options) + +export const addRecipient = async ( + jwe, + ownerKeys, + recipientKey, + alg = 'RSA-OAEP', +) => { + console.log('ownerkeys', ownerKeys) + console.log( + 'adding recipient, current list:', + jwe.recipients.map(({ header: { kid } }) => kid), + ) + const ownersEncryptedKey = jwe.recipients.find( + recipient => recipient.header.kid === ownerKeys.privateKey.kid, + ) + if (!ownersEncryptedKey) { + throw new Error('no matching recipient for owner key') + } + console.log(ownersEncryptedKey.encrypted_key) + const newEncryptedKey = await reEncryptCek( + ownersEncryptedKey.encrypted_key, // the encrypted key of the encrypted key of the owner + ownerKeys, + recipientKey, + alg, + ) + + jwe.recipients.push({ + header: { + kid: recipientKey.kid, + alg, + }, + encrypted_key: newEncryptedKey, + }) + return jwe +} + +const reEncryptCek = (ownersEncryptedKey, ownerKey, recipientKey, _alg) => { + const decryptedCek = privateDecrypt( + ownerKey.privateKey, + Buffer.from(ownersEncryptedKey, 'base64'), + ) + // recipient => recipient.header.kid === ownerKey.kid + return base64url(publicEncrypt(recipientKey.publicKey, decryptedCek)) +} diff --git a/lib/services/__tests__/operatorAdapter.test.js b/lib/services/__tests__/operatorAdapter.test.js index 47fdbec7..38c72706 100644 --- a/lib/services/__tests__/operatorAdapter.test.js +++ b/lib/services/__tests__/operatorAdapter.test.js @@ -26,4 +26,80 @@ describe('operatorAdapter', () => { }) }) }) + describe('addRecipient', () => { + it('returns jwe with new recipient', async () => { + const oldJWE = { + domain: 'http://localhost:8346', + area: 'favorite_cats', + recipients: [ + { + header: { + kid: + 'http://localhost:8346/jwks/342m8JJLQ7VhS-YcWzvqS6OPjzXnd-b08PuESLIzohk', + alg: 'RSA-OAEP', + }, + encrypted_key: + 'WNAru3_AbZsx5QxYD3cCohGC_beJYBarGJCQCOBbFYE7w5DiO8p1E-VWMvGWBma0f2fUrlN7YWpgoYDBWU1N8oIk7vZDovsI_GF3zan2l_oXYvTCJdinRHyc1jiGpQLtdgIXoHTcdYqlLOQgJcUg0wzvKtWDQAXE7HddndtNtQUhQps7D_eHNZ4DSkhtmXd7W16lsjSwEhJg458Q5Zz9B4XCXtg1ayiRex8fKpa4Lu1207_XHxCpfFSLcweAq4FIr1NZQCzrykdnLLO3pOOAVde7a0mcn48d08K3xbGsOuDAVKYOTCJXfWb4-rLGzUd99KaWZNt6idAjbXWm5MtSmw', + }, + { + header: { + kid: + 'egendata://jwks/rX0ptAh_UnSp0mGoYjq5iLH3euoRdBqB50LPZKEoscw', + alg: 'RSA-OAEP', + }, + encrypted_key: + 'ghUqrwSE0wiHXnk7Q6sj8UZtfLBK8KGOOpvPlY-X-VO2o6TwU-F2XZ0K9YdaQz7FHHDWr35NMrkOLFeUsPLfQTki5_L2g0Iw8z7r96CeRIz6dN-iBru9MqGvFcFoWy-kDewAbKpOKRxeQpRrOd4Qw5IgkjjmOB44mcg-EFUlXu-S1EjoEU7Rw9I0YC9-LwF3C01Q4xEjPqRGHiFug-RYLaYK0Ba2MV5f9aYZh9YYSRgS62RltIf5Ww2gI76MtAShEUKxo4eM4UGC87YZScQk5wwkD4_66JhsWTHun6QnqjpWsymgLJRQ_wDsXYetzQjsOa-lg4D_2AgKHUrVrqM8rw', + }, + ], + } + + const newRecipientKey = { + n: + 'qS1ShZotuImce-zDJCCzFD2-rHKCFBVZQ7MUtkv1qOc8CTIg8SCrMFG0-N45-I90o2V0wvnUd6Gy18C1U-PoncOW7atYH_mubMAT9OufG_oNEYxcvnLtkcQ7iJfHEdpGSZG8zbIGPWqcvSOoBsR4AAF-3s7s8lmXTzv3c_r6LrTkHg8vP3wnp_0660aAwD9nQIB5tchxxFJinGY1TChIRx3afxDHqGeURdup_aLM6RzWRw46Pt1rhktY2_ZCpo756Qw3ZVGaraofpOY1w4g2HkkIjZJ-PXkdXZdSVuLMJvASDesVtPv3ZeMMzX6Jq2wTHO5NYgz4jOQQ4KPcx4pksw', + e: 'AQAB', + d: + 'pM_Ygvuu6wZ1Am2ntjx8-Y0lgo6TlsktizydQvNBQejznenOGdq_q3UOHx0v0KzA7qXaWFBW4q0OtA2zGSUA6yEumh_A3HW7rYp6ZrJc8T5rGNtrRsZkFwvbC7kBYK0KqIVoL-PtHEwOolxoRx-D4E2Usa9ZOsh5FeHPspegv3o4p61hwbhn0LK6aZIvF0I4pLDVx7aMu1T6LHwhujvVyPFpLRQLOESbvGji-NR07I4mCSSZsm5IeAL7GbjzJpaPDCV537vin4_GKVfvJ1Mx3H0hN6LYysqwypB6xt9Abv0gl--tzYV9yNQl3rP83m2URckWO4jwtPkko2GlZNkAoQ', + p: + '2MMWxeOzMGJBc0Xi5CP5zyyNLZYQlEUYrVn0xNNSb4WkSflM2cpyEUWD7vLDIDQbX7hdGFwuCe75TlcKcF3d0_WTbwVhgwCsN8oVhnNokN-kiJZ_ewo32UHyjyXYA8z2hVBschPVhQwE2qiFSuF3Wi2up5J82r-nI_FXG4DGR1k', + q: + 'x80b3fycgkAaDuucmB8ymMORKdCtwEh8o5pr7eJjFyq1ETbXsg7QMqr_tI5WGsaeq8jH-dlZx5ZcQNK1SWV7gChEzTKQzkcANVUsV-fW24tsYcsg1gdqX-fl48rogsyUkH87VZKndLUaj_VxndshqOP30ZwVF2cUraDamu8nVus', + dp: + 'lfo96oQGuoZxZLHJMDMYKFlaAV2gcQZx8ZeZPQo-Mn2UU76ThumFDSA9DfqYOdLz0cH9X9p_3E2l36dnyKGZ14tGDH37nym6_wrq49E8W2jyLbN71wUV6VOw4Yy8rryFIW6o6jGA_gJ35VbOiyX_b7zF6Jn5m10Z50uYCqaKClk', + dq: + 'CrdShk51Kns7qo8yf-o0cYMTtxVtLEH3BWNT5JdezzBIM9soKHGo8v6-5jU4IwmCGx6SszDYIt9KpWNnu78Ip7ABOKw8ngOq3DFsRm611GKe9oPJiBEvwGMUrmoEnHdShIl-ajGKb7UC7rOwW1IUdRV9Bi4D55RsxH87GlI3Xu8', + qi: + 'DT-hn8eazryP6iKzXU1QrQTmYnCRJ7lxmcDyEcW5C49JZ_PylOG4qgabic6M48jxcn714jloYcbLGx9gfxVdWRuyYwWjZTLR9gS8_2n4GnYwLyD8GqPfMx6TjelqbVlPnogjfPsJoQzSV99skNTimbY6v8Ft7_Rucpjrm8B12vw', + kty: 'RSA', + use: 'sig', + kid: 'egendata://jwks/kAvUQu002YzNbXVdRmoNqLGQNPSnxeGDA3JjHAeJG2Y', + } + const privateKeyPhone = { + n: + 'ueCP0HcTK2GKJp6-gaD3SJ-CjOfuP6vLHfkcugY4iWcs4LmAtqByxhvHhFWBakgLCaZgwCp0or1r7IcmJL5LNy_z7fYrX15hYOf8HH_38I7968uSjhLbRi01xCTtv2R-k0VpFFKVW-V1AZDUCz8e7zdQhuDpwF1UX8RepVFWqeriMqzPwWod66GP-8MDJRnZPff7zeeGnQi3kN-wAfdVZvnggoBg6ZEClLtwSGzdH5F0DfYk-snnvVxJbIM0-Or8zPyIucQd2hkSK1afFGXimz0Li82javS49NUbgeFmYZ-g93xXZSTTx4Z-zea1C3liGGtrPAz7gANY8VR1IqlQxw', + e: 'AQAB', + d: + 'lKeYvLMOfKpEb4CTgV53hfgz03cFnpxJFI6PP-MLwi_mv078NpJ5WCENXrN3jcVSNoR-ahFKOIDfWEn54nbh9p_-KLiwlVQI8xR1F2Hsq9HgF302lzNTdHthvZ1_GotHg4aGdD9bviPzgK4QN3JizhPh7gzgRP0fJnwI6ZP0iGy4mg2T5L0-M72OB9t6lUO3CyBnYPOxD8Ow5F0MLoYmGaq_JfO7LXCZx7S4VTQrBE72SYBvwGkXbiKye5iTQqonSfvIpmdYuXM8pd_FmavupzB-DoFdC4nY7l9n9mBU2rehKbrSrzCDBTdKdE54L6cRKx0n3_ldBoXM6G_sqh4fSQ', + p: + '6JdRrj4wypuW-vX_ask-uheJGETnOdnas43Et_FUNDxoCNotK3gIOoAQekr1aA4i8ZgoXo1vGhEkqH179InfzreV1omhBdd_dz86KakixnctvMpJ65_o7PM3oKbAQOV_mGiA02X-tF61KOmENlSNqqxnKrnqufTf3BLch5Tg05s', + q: + 'zJWqN65Juia-8zlELnqCHSX6C2OhlDAeusMi2_4Bx3Agpmfm46zDIM6hLq_vmY-mxg5wZWgPl1G9fTzob_xpkaxpwWHTLm9Visx6flSlMw1s4SrQEP7M7JGaKq53VViBrj3QxW5udnpgDRCE_sKljfuX7Go73UULjDvbV5ipWEU', + dp: + 'Zcn7RB8RaUnIPFI2Eny6B-TO6aEV9Fpj_NpZMgraR_X7rYwV4oUoTLnI_EwbtAsjvclSOXb6HVVNTrOD8NP571SmrXoTzyOtM_mmsZ7EikiT6qA403ZrEG-sc5EmaABH4-IwJtPnMPaVn676XnCIgx3qFGfC0tjYs05J1sgP0Gs', + dq: + 'gjkyJEc4ftly6nclQ0CP2eX2h5FfpGgM52yWn9nLYBurbMDuYzXw7s0YJBOxO9oImkFOof3fDr7lEvbWLZJJ0IQivQl71y7fEH6f6hIPJbQB_kG2N1s5LcxwiYKMSzMPOM34OfPVNG0o_qfpQBC-OOZRCheFC4-LjjP7poJyKNE', + qi: + 'eQaVBht5vsnozmI_My6OcVheMsps6NeiWEwUgmv3Bdxr1X0qd0WbFyJrgZXHLAwcd_JvWsyhmrIhfbBTtkQJKKfeC8U850VaMM4EBI9Eu6e9lqbANbOWstfFLS1xAYG3-ufPIGDycsI-Od8W6PtMO4_yTDxFzxXOJB4fRdluwao', + kty: 'RSA', + use: 'sig', + kid: 'egendata://jwks/o7LRekajUbDTaXFY5TaGRfuVV5tJFBNw4ZcG8RozYZ0', + } + const newJWE = await operatorAdapter.addRecipient( + oldJWE, + privateKeyPhone, + newRecipientKey, + ) + + expect(newJWE.recipients).toHaveLength(3) + }) + }) }) diff --git a/lib/services/operatorAdapter.js b/lib/services/operatorAdapter.js index 395ef14a..7c2dae9f 100644 --- a/lib/services/operatorAdapter.js +++ b/lib/services/operatorAdapter.js @@ -1,9 +1,36 @@ -import { sign } from './jwt' -import { getAccount, getAccountKeys, getConnection, storeKey } from './storage' +import { sign, verify } from './jwt' +import { + getAccount, + getAccountKeys, + getConnection, + storeKey, + getPrivateKey, +} from './storage' +import { addRecipient } from '@egendata/react-native-jose' import Config from 'react-native-config' import { schemas } from '@egendata/messaging' import { v4 } from 'uuid' import { generateKey, toPublicKey } from './crypto' +const recipientsKey = { + n: + 'ueCP0HcTK2GKJp6-gaD3SJ-CjOfuP6vLHfkcugY4iWcs4LmAtqByxhvHhFWBakgLCaZgwCp0or1r7IcmJL5LNy_z7fYrX15hYOf8HH_38I7968uSjhLbRi01xCTtv2R-k0VpFFKVW-V1AZDUCz8e7zdQhuDpwF1UX8RepVFWqeriMqzPwWod66GP-8MDJRnZPff7zeeGnQi3kN-wAfdVZvnggoBg6ZEClLtwSGzdH5F0DfYk-snnvVxJbIM0-Or8zPyIucQd2hkSK1afFGXimz0Li82javS49NUbgeFmYZ-g93xXZSTTx4Z-zea1C3liGGtrPAz7gANY8VR1IqlQxw', + e: 'AQAB', + d: + 'lKeYvLMOfKpEb4CTgV53hfgz03cFnpxJFI6PP-MLwi_mv078NpJ5WCENXrN3jcVSNoR-ahFKOIDfWEn54nbh9p_-KLiwlVQI8xR1F2Hsq9HgF302lzNTdHthvZ1_GotHg4aGdD9bviPzgK4QN3JizhPh7gzgRP0fJnwI6ZP0iGy4mg2T5L0-M72OB9t6lUO3CyBnYPOxD8Ow5F0MLoYmGaq_JfO7LXCZx7S4VTQrBE72SYBvwGkXbiKye5iTQqonSfvIpmdYuXM8pd_FmavupzB-DoFdC4nY7l9n9mBU2rehKbrSrzCDBTdKdE54L6cRKx0n3_ldBoXM6G_sqh4fSQ', + p: + '6JdRrj4wypuW-vX_ask-uheJGETnOdnas43Et_FUNDxoCNotK3gIOoAQekr1aA4i8ZgoXo1vGhEkqH179InfzreV1omhBdd_dz86KakixnctvMpJ65_o7PM3oKbAQOV_mGiA02X-tF61KOmENlSNqqxnKrnqufTf3BLch5Tg05s', + q: + 'zJWqN65Juia-8zlELnqCHSX6C2OhlDAeusMi2_4Bx3Agpmfm46zDIM6hLq_vmY-mxg5wZWgPl1G9fTzob_xpkaxpwWHTLm9Visx6flSlMw1s4SrQEP7M7JGaKq53VViBrj3QxW5udnpgDRCE_sKljfuX7Go73UULjDvbV5ipWEU', + dp: + 'Zcn7RB8RaUnIPFI2Eny6B-TO6aEV9Fpj_NpZMgraR_X7rYwV4oUoTLnI_EwbtAsjvclSOXb6HVVNTrOD8NP571SmrXoTzyOtM_mmsZ7EikiT6qA403ZrEG-sc5EmaABH4-IwJtPnMPaVn676XnCIgx3qFGfC0tjYs05J1sgP0Gs', + dq: + 'gjkyJEc4ftly6nclQ0CP2eX2h5FfpGgM52yWn9nLYBurbMDuYzXw7s0YJBOxO9oImkFOof3fDr7lEvbWLZJJ0IQivQl71y7fEH6f6hIPJbQB_kG2N1s5LcxwiYKMSzMPOM34OfPVNG0o_qfpQBC-OOZRCheFC4-LjjP7poJyKNE', + qi: + 'eQaVBht5vsnozmI_My6OcVheMsps6NeiWEwUgmv3Bdxr1X0qd0WbFyJrgZXHLAwcd_JvWsyhmrIhfbBTtkQJKKfeC8U850VaMM4EBI9Eu6e9lqbANbOWstfFLS1xAYG3-ufPIGDycsI-Od8W6PtMO4_yTDxFzxXOJB4fRdluwao', + kty: 'RSA', + use: 'sig', + kid: 'egendata://jwks/o7LRekajUbDTaXFY5TaGRfuVV5tJFBNw4ZcG8RozYZ0', +} export const createAccountRegistration = async ( // eslint-disable-next-line camelcase @@ -113,30 +140,52 @@ export const createLoginResponse = async loginPayload => { } export async function getRecipients({ domain, area }) { - const { publicKey, privateKey, privateKeyPem } = await getAccountKeys() - const connectionId = getConnection(domain) - + const { + publicKey: publicSigningKey, + privateKey: privateSigningKey, + privateKeyPem: privateSigningKeyPem, + } = await getAccountKeys() + const connection = await getConnection(domain) + if (!connection) { + return [] + } + console.log(connection.connectionId, 'connectionID') + console.log(privateSigningKey.kid, 'private keyid from storage') return sign( { type: 'RECIPIENTS_READ_REQUEST', - sub: connectionId, + sub: connection.connectionId, paths: [{ domain, area }], + iss: domain, + aud: Config.OPERATOR_URL, }, { - jwk: privateKey, - pem: privateKeyPem, + jwk: privateSigningKey, + pem: privateSigningKeyPem, }, - { jwk: publicKey, alg: schemas.algs[0] }, - ).then(postToOperator) + { jwk: publicSigningKey, alg: schemas.algs[0] }, + ) + .then(postToOperator) + .then(verify) + .then(({ payload: { paths } }) => paths[0]) + .then(async jwe => { + const [privateKeys] = ( + await Promise.all( + jwe.recipients.map(({ header: { kid } }) => kid).map(getPrivateKey), + ) + ).filter(Boolean) + return { jwe, privateKeys } + }) + .then(({ jwe, privateKeys }) => + addRecipient(jwe, privateKeys, recipientsKey), + ) } -// ToOoOoToOT -// poopstoopidooperator export function postToOperator(body) { return fetch(Config.OPERATOR_URL, { method: 'POST', headers: { 'content-type': 'application/jwt' }, body, - }) + }).then(e => e.text()) } export async function createConnection(connectionRequest, approvedPermissions) { @@ -194,6 +243,11 @@ export async function getApprovedPermissionRequestWithKeys( p.jwks.keys.push(toPublicKey(key.privateKey)) } else if (p.type === 'READ') { + const recipients = await getRecipients({ + domain: p.domain, + area: p.area, + }) + console.log(recipients) // todo: l8r if there are no recipients then create JWE with empty data // todo: get jwe recipients From 226f0b290e05e14af77b2331735b804791b59d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Wed, 3 Jun 2020 14:21:10 +0200 Subject: [PATCH 12/25] chore: add gradle properties to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6d898263..e1e4a3e2 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ build/ .idea .gradle local.properties +gradle.properties *.iml *.aab From 4c394fec0b0578f2005068ee127bf4c8762b8191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Wed, 3 Jun 2020 14:22:17 +0200 Subject: [PATCH 13/25] chore: check if CI env in fastline files --- android/fastlane/Appfile | 7 ++++++- ios/fastlane/Matchfile | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/android/fastlane/Appfile b/android/fastlane/Appfile index 2e0d4555..0f72fb12 100644 --- a/android/fastlane/Appfile +++ b/android/fastlane/Appfile @@ -1,2 +1,7 @@ -json_key_file("/home/travis/build/egendata/app/android/google_play_store.json") +if ENV['CI'] + json_key_file("/home/travis/build/egendata/app/android/google_play_store.json") +else + json_key_file("../google_play_store.json") +end + package_name("com.egendata") diff --git a/ios/fastlane/Matchfile b/ios/fastlane/Matchfile index bde3c6d8..b1733822 100644 --- a/ios/fastlane/Matchfile +++ b/ios/fastlane/Matchfile @@ -1,5 +1,8 @@ -git_url("https://#{ENV['GITHUB_CERT_TOKEN']}@github.com/Iteam1337/egendata-ios-certificates.git") - +if ENV['CI'] + git_url("https://#{ENV['GITHUB_CERT_TOKEN']}@github.com/Iteam1337/egendata-ios-certificates.git") +else + git_url("git@github.com:Iteam1337/egendata-ios-certificates.git") +end storage_mode("git") type("appstore") # The default type, can be: appstore, adhoc, enterprise or development From f23007de8a2c5691ab386ec4244c2981370279c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 4 Jun 2020 10:51:19 +0200 Subject: [PATCH 14/25] docs: improve fastlane documentation --- README.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e5ed89da..4cacad67 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ An example app for managing consents and viewing data ### Linux and Android * Install Android Studio https://developer.android.com/studio/install - * In the project directory create the file `android/local.properties` with the content `sdk.dir = /home/USERNAME/Android/Sdk` + * In the project directory create the file `android/local.properties` with the content `sdk.dir = /home/USERNAME/Android/sdk` on linux and, `sdk.dir = /Users/mgg/Library/Android/sdk` on mac, replacing username with your user's name * Approve the licenses of the SDK packages by running ` /home/USERNAME/Android/Sdk/tools/bin/sdkmanager --licenses` * If you get `Could not find tools.jar` then you need to point gradle to the JDK installation. * You can find it with `2>/dev/null find / -name tools.jar -path "*jdk*"` @@ -51,7 +51,11 @@ or whatever is the adress of the operator you want to use. Note that *OPERATOR_U - `npm run android` is only needed the first time or when adding dependencies (it runs Jetifier to migrate libraries to AndroidX; after that you can run it from Android Studio if you prefer) - if you want to run it on an actual device you need to run adb reverse tcp:8081 tcp:8081 so that the phone can reach the Metro bundler - +- Add a gradle.properties file as /android/gradle.properties with the following contents + ``` + android.useAndroidX=true + android.enableJetifier=true + ``` ### __iOS__ - Update Cocoapods if version < 1.7.5 (check with `pod --version`) @@ -137,11 +141,11 @@ All the relevant files for how this is currently set up can be found in `ios/fas `xcode-select --install` -2. Access to the private repository holding the certificates and the provisioning profile. +2. Access to the private repository (Iteam1337/egendata-ios-certificates) holding the certificates and the provisioning profile. Your personal account should be invited as fastlane will use the -3. Edit the `git_url(...)` in `ios/fastlane/Matchfile` to the ssh version of the git url. +3. The certf-repo-passphrase. Currently stored in lastpass under `Egendata iOS Certificate Password`. You will be asked for this password when running the fastlane command. -4. Username and password for the apple user who is performing this operation. This user needs to be a part of the appstore connect team. +4. Username and password for the apple user who is performing this operation. This user needs to be a part of the appstore connect team. Currently stored in lastpass under `Egendata iOS Certificate Password`. You will be asked for these credentials when running the fastlane command. *NOTE: Remember to change `.env`-file (correct OPERATOR_URL etc.) before doing the steps below* @@ -153,6 +157,8 @@ fastlane manual_alpha_release *NOTE: Fastlane command might error with `error: Multiple commands produce ...`, if so, run again.* *NOTE: Fastlane command might error with ` error: The sandbox is not in sync with the Podfile.lock`, if so, see the `If build fails` section .* +The app should now have been released in testflight. To access it and invite other people. Make sure you get invited to the team at https://appstoreconnect.apple.com. + ### Android (Google Play) *NOTE: Remember to change `.env`-file (correct OPERATOR_URL etc.) before doing the steps below* From d7466731a101ef695c6c4daf0e4364ae29bcd67a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 4 Jun 2020 19:13:19 +0200 Subject: [PATCH 15/25] feat: add button for removing consent --- lib/components/Consents/ConsentListItem.js | 19 ++++++++++++------ lib/components/Consents/index.js | 14 ++++++++++--- lib/services/operatorAdapter.js | 23 ++++++++++++++++++++++ lib/services/storage.js | 10 ++++++++++ 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/lib/components/Consents/ConsentListItem.js b/lib/components/Consents/ConsentListItem.js index c799ffa6..db103223 100644 --- a/lib/components/Consents/ConsentListItem.js +++ b/lib/components/Consents/ConsentListItem.js @@ -6,6 +6,7 @@ import { Icon } from '../elements/Icon/Icon' const Container = styled(TouchableOpacity)` flex-direction: row; + min-height: 100px align-items: center; ` @@ -21,6 +22,7 @@ const Logo = styled(Image)` ` const IconContainer = styled(View)` + flex-direction: row; position: absolute; top: 0; right: 0; @@ -35,12 +37,17 @@ const ConsentListItem = ({ }) => ( - + + remove consent + + diff --git a/lib/components/Consents/index.js b/lib/components/Consents/index.js index 4e9f28ee..363c7b64 100644 --- a/lib/components/Consents/index.js +++ b/lib/components/Consents/index.js @@ -2,10 +2,11 @@ import React from 'react' import { Image, View } from 'react-native' import { WrapWithHeader, ScrollViewWrap } from '../view/Wrapper' import ConsentList from './ConsentList' -import { getConnections } from '../../services/storage' +import { getConnections, deleteConnection } from '../../services/storage' import { Paragraph } from '../typography/Typography' import { Spinner } from '../elements/Spinner/Spinner' import { reducer, actions } from './consentReducer' +import { removeFromOperator } from '../../services/operatorAdapter' const initialState = { connections: [], @@ -26,8 +27,15 @@ const Consents = ({ navigation }) => { .then(() => dispatch({ type: actions.IS_LOADING, payload: false })) }, [navigation]) - const handleConsentPress = _consent => { - // navigation.navigate('Consent', { consent }) + const handleConsentPress = consent => { + return removeFromOperator(consent) + .then(() => deleteConnection(consent)) + .then(() => + dispatch({ + type: actions.SET_CONNECTIONS, + payload: [], + }), + ) } if (state.isLoading) { diff --git a/lib/services/operatorAdapter.js b/lib/services/operatorAdapter.js index 7c2dae9f..9d35aeff 100644 --- a/lib/services/operatorAdapter.js +++ b/lib/services/operatorAdapter.js @@ -271,3 +271,26 @@ function mapReadKeys(permissions) { return map.set(`${domain}|${area}`, jwk) }, new Map()) } + +export async function removeFromOperator({connectionId, serviceId}){ + const { id } = await getAccount() + + const { + publicKey: publicSigningKey, + privateKey: privateSigningKey, + privateKeyPem: privateSigningKeyPem, + } = await getAccountKeys() + return sign( + { + type: 'DELETE_CONSENT', + aud: Config.OPERATOR_URL, + iss: `egendata://account/${id}`, + sub: connectionId, + }, + { + jwk: privateSigningKey, + pem: privateSigningKeyPem, + }, + { jwk: publicSigningKey, alg: schemas.algs[0] }) + .then(postToOperator) +} \ No newline at end of file diff --git a/lib/services/storage.js b/lib/services/storage.js index 869822c5..fcbdcc82 100644 --- a/lib/services/storage.js +++ b/lib/services/storage.js @@ -40,6 +40,16 @@ export const storeConnection = async ({ serviceId, ...rest }) => { ) } +export const deleteConnection = async ({ serviceId }) => { + const existingConnections = await getConnections() + delete existingConnections[serviceId] + + return AsyncStorage.setItem( + 'connections', + JSON.stringify(existingConnections), + ) +} + export const storeKey = async ({ privateKey, privateKeyPem }) => { await AsyncStorage.setItem( `jwks/${privateKey.kid}`, From e38e90343636880ad59870c0deb5678f0b5c30b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Mon, 8 Jun 2020 12:38:21 +0200 Subject: [PATCH 16/25] chore: disable getRecipients for now --- lib/services/operatorAdapter.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/services/operatorAdapter.js b/lib/services/operatorAdapter.js index 9d35aeff..11fd325c 100644 --- a/lib/services/operatorAdapter.js +++ b/lib/services/operatorAdapter.js @@ -243,11 +243,11 @@ export async function getApprovedPermissionRequestWithKeys( p.jwks.keys.push(toPublicKey(key.privateKey)) } else if (p.type === 'READ') { - const recipients = await getRecipients({ - domain: p.domain, - area: p.area, - }) - console.log(recipients) + // const recipients = await getRecipients({ + // domain: p.domain, + // area: p.area, + // }) + // console.log(recipients) // todo: l8r if there are no recipients then create JWE with empty data // todo: get jwe recipients @@ -272,7 +272,7 @@ function mapReadKeys(permissions) { }, new Map()) } -export async function removeFromOperator({connectionId, serviceId}){ +export async function removeFromOperator({ connectionId, serviceId }) { const { id } = await getAccount() const { @@ -291,6 +291,6 @@ export async function removeFromOperator({connectionId, serviceId}){ jwk: privateSigningKey, pem: privateSigningKeyPem, }, - { jwk: publicSigningKey, alg: schemas.algs[0] }) - .then(postToOperator) -} \ No newline at end of file + { jwk: publicSigningKey, alg: schemas.algs[0] }, + ).then(postToOperator) +} From 562e5b50e21ba95ef4c96056bf57e81784e94b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 11 Jun 2020 12:09:53 +0200 Subject: [PATCH 17/25] chore: replace mgg with USERNAME in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4cacad67..e2df0cec 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ An example app for managing consents and viewing data ### Linux and Android * Install Android Studio https://developer.android.com/studio/install - * In the project directory create the file `android/local.properties` with the content `sdk.dir = /home/USERNAME/Android/sdk` on linux and, `sdk.dir = /Users/mgg/Library/Android/sdk` on mac, replacing username with your user's name + * In the project directory create the file `android/local.properties` with the content `sdk.dir = /home/USERNAME/Android/sdk` on linux and, `sdk.dir = /Users/USERNAME/Library/Android/sdk` on mac, replacing USERNAME with your user's name * Approve the licenses of the SDK packages by running ` /home/USERNAME/Android/Sdk/tools/bin/sdkmanager --licenses` * If you get `Could not find tools.jar` then you need to point gradle to the JDK installation. * You can find it with `2>/dev/null find / -name tools.jar -path "*jdk*"` From 118ee73094ee5cf43e83c393f86bfc01f079fffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Mon, 15 Jun 2020 09:48:17 +0200 Subject: [PATCH 18/25] docs: correct naming in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2df0cec..f209f82b 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ The app should now have been released in testflight. To access it and invite oth 1. Download the Google Play, the release.keystore and the gradle.properties (it's in LastPass) - Place the `.json`-file somewhere, you'll need to point to it from `android/fastlane/Appfile` - `json_key_file("/path/to/egendata_google_play.json")` + `json_key_file("/path/to/google_play_store.json")` - Place the `release.keystore` in `android/app` - Create `gradle.properties` in `android` and paste from lastpass. From 086445370e55a2b08cb309da7a47d926f4a3c1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Mon, 15 Jun 2020 12:01:42 +0200 Subject: [PATCH 19/25] docs: add more instructions for releasing android --- README.md | 14 ++++++++++---- android/fastlane/Appfile | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f209f82b..023ac93b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ An example app for managing consents and viewing data - Install Watchman `brew install watchman` ### Linux and Android - +* Make sure you have java 8 installed, the project is only compatible with this version. Later versions breaks the build. + - With brew you can install java8 with `brew cask install adoptopenjdk8` + - Then remember to se your JAVA_HOME env to this version (and/or add it to your .bashrc): `export JAVA_HOME=/Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home` * Install Android Studio https://developer.android.com/studio/install * In the project directory create the file `android/local.properties` with the content `sdk.dir = /home/USERNAME/Android/sdk` on linux and, `sdk.dir = /Users/USERNAME/Library/Android/sdk` on mac, replacing USERNAME with your user's name * Approve the licenses of the SDK packages by running ` /home/USERNAME/Android/Sdk/tools/bin/sdkmanager --licenses` @@ -23,7 +25,7 @@ An example app for managing consents and viewing data * You can find it with `2>/dev/null find / -name tools.jar -path "*jdk*"` * If you don't have JDK installed then install it * Create the file `~/.gradle/gradle.properties` with the line `org.gradle.java.home = /PATH/TO/JDK` - * Set up the device which will run the app (API Level 26, Android 8.0) https://facebook.github.io/react-native/docs/getting-started.html#preparing-the-android-device + * Install and open Android studio. Open the android folder inside Android Studio. Set up the device which will run the app (API Level 26, Android 8.0) by opening AVD manager. The AVD manager can be opened from the dropdown menu next to the play button. * (Optionally, if you want it to automatically reload on code change) Install Watchman https://facebook.github.io/watchman/docs/install.html#installing-from-source ## Config @@ -171,7 +173,11 @@ The app should now have been released in testflight. To access it and invite oth 2. ``` -cd android -fastlane alpha +* cd android +* fastlane android_alpha +* After fastlane has ran, this could take several minutes. You should see the message: "Successfully finished the upload to Google Play" +* Make sure you are invited to the google play account hosting the app. Then, to access the console (currently Iteam's account): https://play.google.com/apps/publish/?account=7914539322420463189#ManageReleaseTrackPlace:p=com.egendata&appid=4972061446016688220&releaseTrackId=4700968953354340768 +* Or enter the account, press Egendata app -> Appversioner -> Intern testkanal. + ``` diff --git a/android/fastlane/Appfile b/android/fastlane/Appfile index 0f72fb12..fbe5260e 100644 --- a/android/fastlane/Appfile +++ b/android/fastlane/Appfile @@ -1,7 +1,7 @@ if ENV['CI'] json_key_file("/home/travis/build/egendata/app/android/google_play_store.json") else - json_key_file("../google_play_store.json") + json_key_file("./google_play_store.json") end package_name("com.egendata") From 53ca2bd2de4a29bfd84b34ca949f02a5cc6ae0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Mon, 15 Jun 2020 12:02:59 +0200 Subject: [PATCH 20/25] chore: update gradle --- android/gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index aff6a99c..506c3728 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jul 03 11:49:32 CEST 2019 +#Thu Jun 11 14:32:29 CEST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip From fb4e6866bf2ab3c180dd5f7fafcc72e081c37b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Mon, 15 Jun 2020 12:15:02 +0200 Subject: [PATCH 21/25] chore: print operatorURL when creating account --- lib/services/account.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/account.js b/lib/services/account.js index 18aa11d5..a8731a56 100644 --- a/lib/services/account.js +++ b/lib/services/account.js @@ -30,7 +30,7 @@ export async function save(account) { publicKey, privateKeyPem, }) - + console.log('Creating account with operator url:', Config.OPERATOR_URL) await fetch(Config.OPERATOR_URL, { method: 'POST', headers: { 'content-type': 'application/jwt' }, From ded775ec3a58e1ded170c8cee313fe25a1803d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Mon, 15 Jun 2020 12:16:28 +0200 Subject: [PATCH 22/25] chore: update gradle version --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 7921886b..ca6ac02a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -13,7 +13,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.2' + classpath 'com.android.tools.build:gradle:3.6.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 5d70d9a49569eb3a21d06821b960688c4735b9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Mon, 15 Jun 2020 12:17:37 +0200 Subject: [PATCH 23/25] chore: bump version --- .gitignore | 1 + android/app/build.gradle | 4 ++-- android/fastlane/README.md | 4 ++-- android/gradle.properties | 23 ++++++----------------- ios/Egendata-tvOS/Info.plist | 2 +- ios/Egendata-tvOSTests/Info.plist | 2 +- ios/Egendata.xcodeproj/project.pbxproj | 4 ++-- ios/Egendata/Info.plist | 2 +- ios/EgendataTests/Info.plist | 2 +- 9 files changed, 17 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index e1e4a3e2..8aeee9e5 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,4 @@ __e2e__/dist # Other google_play_store.json egendata_google_play.json +*.hprof \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 528f99ae..d8b845dd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -125,8 +125,8 @@ android { applicationId "com.egendata" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 83 - versionName "2.4.0" + versionCode 87 + versionName "2.5.0" ndk { abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a" } diff --git a/android/fastlane/README.md b/android/fastlane/README.md index 1e029c9a..db087d8c 100644 --- a/android/fastlane/README.md +++ b/android/fastlane/README.md @@ -16,9 +16,9 @@ or alternatively using `brew cask install fastlane` # Available Actions ## Android -### android alpha +### android build ``` -fastlane android alpha +fastlane android build ``` Runs all the tests ### android android_alpha diff --git a/android/gradle.properties b/android/gradle.properties index 5a568cae..56fa36cc 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,20 +1,9 @@ -# Project-wide Gradle settings. +## gradle.properties -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true +MYAPP_RELEASE_STORE_FILE=release.keystore +MYAPP_RELEASE_KEY_ALIAS=upload +MYAPP_RELEASE_STORE_PASSWORD=mydataf33lsgood +MYAPP_RELEASE_KEY_PASSWORD=toog00dmydata android.useAndroidX=true android.enableJetifier=true +org.gradle.jvmargs=-Xmx2048m diff --git a/ios/Egendata-tvOS/Info.plist b/ios/Egendata-tvOS/Info.plist index 515daf71..bfd16644 100644 --- a/ios/Egendata-tvOS/Info.plist +++ b/ios/Egendata-tvOS/Info.plist @@ -19,7 +19,7 @@ CFBundleSignature ???? CFBundleVersion - 79 + 83 LSRequiresIPhoneOS UILaunchStoryboardName diff --git a/ios/Egendata-tvOSTests/Info.plist b/ios/Egendata-tvOSTests/Info.plist index 59625cd4..d165d863 100644 --- a/ios/Egendata-tvOSTests/Info.plist +++ b/ios/Egendata-tvOSTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 79 + 83 diff --git a/ios/Egendata.xcodeproj/project.pbxproj b/ios/Egendata.xcodeproj/project.pbxproj index 471feac1..4fe6aea7 100644 --- a/ios/Egendata.xcodeproj/project.pbxproj +++ b/ios/Egendata.xcodeproj/project.pbxproj @@ -1426,7 +1426,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 79; + CURRENT_PROJECT_VERSION = 83; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = BSE2J6442R; HEADER_SEARCH_PATHS = ( @@ -1461,7 +1461,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 79; + CURRENT_PROJECT_VERSION = 83; DEVELOPMENT_TEAM = BSE2J6442R; HEADER_SEARCH_PATHS = ( "$(inherited)", diff --git a/ios/Egendata/Info.plist b/ios/Egendata/Info.plist index b1a170f5..3146c72f 100644 --- a/ios/Egendata/Info.plist +++ b/ios/Egendata/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 79 + 83 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/ios/EgendataTests/Info.plist b/ios/EgendataTests/Info.plist index 54a15ab2..cc3eb772 100644 --- a/ios/EgendataTests/Info.plist +++ b/ios/EgendataTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 79 + 83 From 73a550d1a9fad68fec564a6e4d0828c21618b4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 2 Jul 2020 14:17:03 +0200 Subject: [PATCH 24/25] chore: bump egendata-messaging version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 92b64fd5..7ef26f14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1426,11 +1426,11 @@ } }, "@egendata/messaging": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@egendata/messaging/-/messaging-0.6.0.tgz", - "integrity": "sha512-2chZYW1IdLQOgEaySog2Gcz1XxgVx8BMOLcL4PlTv16TKvf5GM3ijVub9WrEjs9KP3EB3BpMp36qB2+xBEezHw==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@egendata/messaging/-/messaging-0.8.0.tgz", + "integrity": "sha512-2BjBUy0zaui0DorjJfec/dRpyHcJsiz3t3vxSm2D9uK9KoUM5Kq/O1LPypwuEflg5FR2DT+g8+7jmgwxd/EHmQ==", "requires": { - "http-errors": "^1.7.2", + "http-errors": "^1.7.3", "isomorphic-fetch": "^2.2.1", "joi-browser": "^13.4.0", "js-base64": "^2.5.1" diff --git a/package.json b/package.json index 71735b82..37312ae5 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ ], "license": "Apache-2.0", "dependencies": { - "@egendata/messaging": "0.6.0", + "@egendata/messaging": "^0.8.0", "@egendata/react-native-jose": "0.5.0", "@egendata/react-native-simple-crypto": "1.0.2", "@react-native-community/async-storage": "1.6.1", From 2bb470bb157372ba87217a894ffdcd67fd4ec7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20Gr=C3=A5borg?= Date: Thu, 2 Jul 2020 14:32:03 +0200 Subject: [PATCH 25/25] chore: remove misstake --- android/gradle.properties | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 android/gradle.properties diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index 56fa36cc..00000000 --- a/android/gradle.properties +++ /dev/null @@ -1,9 +0,0 @@ -## gradle.properties - -MYAPP_RELEASE_STORE_FILE=release.keystore -MYAPP_RELEASE_KEY_ALIAS=upload -MYAPP_RELEASE_STORE_PASSWORD=mydataf33lsgood -MYAPP_RELEASE_KEY_PASSWORD=toog00dmydata -android.useAndroidX=true -android.enableJetifier=true -org.gradle.jvmargs=-Xmx2048m