diff --git a/.gitignore b/.gitignore
index 6d898263..8aeee9e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,7 @@ build/
.idea
.gradle
local.properties
+gradle.properties
*.iml
*.aab
@@ -82,3 +83,4 @@ __e2e__/dist
# Other
google_play_store.json
egendata_google_play.json
+*.hprof
\ No newline at end of file
diff --git a/README.md b/README.md
index e5ed89da..023ac93b 100644
--- a/README.md
+++ b/README.md
@@ -15,15 +15,17 @@ 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`
+ * 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*"`
* 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
@@ -51,7 +53,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 +143,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,19 +159,25 @@ 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*
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.
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/__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/__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/__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,
}
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/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
diff --git a/android/fastlane/Appfile b/android/fastlane/Appfile
index 2e0d4555..fbe5260e 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/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
deleted file mode 100644
index 5a568cae..00000000
--- a/android/gradle.properties
+++ /dev/null
@@ -1,20 +0,0 @@
-# Project-wide Gradle settings.
-
-# 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
-android.useAndroidX=true
-android.enableJetifier=true
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
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
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/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
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/__tests__/account.test.js b/lib/services/__tests__/account.test.js
index 90f99dc8..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 tokens from '../tokens'
+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('../tokens', () => ({
+jest.mock('../operatorAdapter', () => ({
createAccountRegistration: jest
.fn()
- .mockName('tokens.createAccountRegistration'),
+ .mockName('operatorAdapter.createAccountRegistration'),
}))
Config.OPERATOR_URL = 'aTotallyLegitOperatorUrl'
@@ -67,7 +67,7 @@ describe('account', () => {
privateKeyPem,
})
crypto.toPublicKey.mockReturnValue(publicKey)
- tokens.createAccountRegistration.mockResolvedValue(jwt)
+ operatorAdapter.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(operatorAdapter.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..1716b496 100644
--- a/lib/services/__tests__/auth.test.js
+++ b/lib/services/__tests__/auth.test.js
@@ -1,31 +1,56 @@
-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 tokens from '../tokens'
+import * as operatorAdapter from '../operatorAdapter'
+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(),
}))
+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('../tokens', () => ({
+jest.mock('../serviceAdapter', () => ({
createConnectionInit: jest
.fn()
.mockResolvedValue('jwt-for-createConnectionInit'),
- 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 {
+ createConnectionMessage: jest
+ .fn()
+ .mockResolvedValue('jwt-for-createConnection'),
+ getApprovedPermissionRequestWithKeys:
+ operatorAdapterActual.getApprovedPermissionRequestWithKeys,
+ createConnection: operatorAdapterActual.createConnection,
+ createConnectionResponse: jest
+ .fn()
+ .mockResolvedValue('jwt-for-createConnectionResponse'),
+ }
+})
jest.mock('../crypto', () => {
const actualCrypto = jest.requireActual('../crypto')
@@ -173,7 +198,7 @@ describe('auth', () => {
],
}
})
- describe('#createPermissionResult', () => {
+ describe('#operatorAdapter.getApprovedPermissionRequestWithKeys', () => {
beforeEach(() => {
generateKey.mockResolvedValueOnce({ privateKey: appKeyBaseData })
generateKey.mockResolvedValueOnce({ privateKey: appKeyExperience })
@@ -243,7 +268,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)
})
@@ -256,16 +284,22 @@ describe('auth', () => {
afterEach(() => {
generateKey.mockReset()
})
- it('calls tokens.createConnection with correct arguments for empty approved', async () => {
- await approveConnection(connectionRequest, new Map())
- expect(tokens.createConnection).toHaveBeenCalledWith(
- connectionRequest,
- undefined,
- expect.any(String),
- )
+ it('sends payload to operator without permissions, if no permissions approved', async () => {
+ await approveConnection(connectionRequest, new Map())
+ expect(fetch).toHaveBeenCalledWith('https://smoothoperator.work', {
+ body: {
+ payload: 'connection_payload',
+ permissions: undefined,
+ },
+ headers: {
+ 'content-type': 'application/jwt',
+ },
+ method: 'POST',
+ })
})
- it('calls tokens.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],
@@ -275,18 +309,20 @@ describe('auth', () => {
])
await approveConnection(connectionRequest, approved)
- expect(tokens.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 = tokens.createConnection.mock.calls[0][1]
- expect(permissions.approved).toHaveLength(4)
+ headers: {
+ 'content-type': 'application/jwt',
+ },
+ method: 'POST',
+ })
})
- it('calls tokens.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],
@@ -296,19 +332,15 @@ describe('auth', () => {
])
await approveConnection(connectionRequest, approved)
- expect(tokens.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',
})
})
})
diff --git a/lib/services/__tests__/operatorAdapter.test.js b/lib/services/__tests__/operatorAdapter.test.js
new file mode 100644
index 00000000..38c72706
--- /dev/null
+++ b/lib/services/__tests__/operatorAdapter.test.js
@@ -0,0 +1,105 @@
+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,
+ })
+ })
+ })
+ 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/account.js b/lib/services/account.js
index 8f7cc425..a8731a56 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 './operatorAdapter'
export async function save(account) {
try {
@@ -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' },
diff --git a/lib/services/auth.js b/lib/services/auth.js
index 37cc195a..4e07f54a 100644
--- a/lib/services/auth.js
+++ b/lib/services/auth.js
@@ -1,15 +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 {
- createConnectionInit,
- createConnection,
- createConnectionResponse,
createLogin,
createLoginResponse,
-} from './tokens'
-import { v4 } from 'uuid'
-import { generateKey, toPublicKey } from './crypto'
+ createConnection,
+} from './operatorAdapter'
export const authenticationRequestHandler = async ({ payload }) => {
const existingConnection = await getConnection(payload.iss)
@@ -30,9 +28,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')
@@ -41,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 createConnection(connectionRequest, approved)
await storeConnection({
serviceId: connectionRequest.iss,
@@ -90,59 +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 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 {
- 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
- p.jwks.keys.push(await withJwk())
- } else if (p.type === 'READ') {
- 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
new file mode 100644
index 00000000..11fd325c
--- /dev/null
+++ b/lib/services/operatorAdapter.js
@@ -0,0 +1,296 @@
+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
+ { id, pds: { provider, access_token } },
+ { publicKey, privateKeyPem, privateKey },
+) => {
+ return sign(
+ {
+ type: 'ACCOUNT_REGISTRATION',
+ aud: Config.OPERATOR_URL,
+ iss: `egendata://account/${id}`,
+ pds: { provider, access_token },
+ },
+ {
+ jwk: privateKey,
+ pem: privateKeyPem,
+ },
+ {
+ jwk: publicKey,
+ alg: schemas.algs[0],
+ },
+ )
+}
+
+export const createConnectionMessage = async (
+ { iss, sid },
+ permissions,
+ connectionId,
+) => {
+ const { publicKey, privateKey, privateKeyPem } = await getAccountKeys()
+ const body = {
+ type: 'CONNECTION',
+ aud: iss,
+ iss: 'egendata://account',
+ sid,
+ sub: connectionId,
+ permissions,
+ }
+ return sign(
+ body,
+ {
+ jwk: privateKey,
+ pem: privateKeyPem,
+ },
+ {
+ jwk: publicKey,
+ alg: schemas.algs[0],
+ },
+ )
+}
+
+export const createConnectionResponse = async payload => {
+ const { id } = await getAccount()
+ const { publicKey, privateKey, privateKeyPem } = await getAccountKeys()
+ return sign(
+ {
+ type: 'CONNECTION_RESPONSE',
+ aud: Config.OPERATOR_URL,
+ iss: `egendata://account/${id}`,
+ payload,
+ },
+ {
+ jwk: privateKey,
+ pem: privateKeyPem,
+ },
+ { jwk: publicKey, alg: schemas.algs[0] },
+ )
+}
+
+export const createLogin = async ({ serviceId, connectionId }, sessionId) => {
+ if (!sessionId) {
+ throw Error('SessionId is missing')
+ }
+ const { publicKey, privateKey, privateKeyPem } = await getAccountKeys()
+ return sign(
+ {
+ type: 'LOGIN',
+ aud: serviceId,
+ sid: sessionId,
+ sub: connectionId,
+ iss: 'egendata://account',
+ },
+ {
+ jwk: privateKey,
+ pem: privateKeyPem,
+ },
+ { jwk: publicKey, alg: schemas.algs[0] },
+ )
+}
+
+export const createLoginResponse = async loginPayload => {
+ const { id } = await getAccount()
+ const { publicKey, privateKey, privateKeyPem } = await getAccountKeys()
+ return sign(
+ {
+ type: 'LOGIN_RESPONSE',
+ payload: loginPayload,
+ iss: `egendata://account/${id}`,
+ aud: Config.OPERATOR_URL,
+ },
+ {
+ jwk: privateKey,
+ pem: privateKeyPem,
+ },
+ { jwk: publicKey, alg: schemas.algs[0] },
+ )
+}
+
+export async function getRecipients({ domain, area }) {
+ 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: connection.connectionId,
+ paths: [{ domain, area }],
+ iss: domain,
+ aud: Config.OPERATOR_URL,
+ },
+ {
+ jwk: privateSigningKey,
+ pem: privateSigningKeyPem,
+ },
+ { 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),
+ )
+}
+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) {
+ const connectionId = v4()
+
+ return getApprovedPermissionRequestWithKeys(
+ connectionRequest,
+ approvedPermissions,
+ )
+ .then(permissionsResult =>
+ createConnectionMessage(
+ connectionRequest,
+ permissionsResult,
+ connectionId,
+ ),
+ )
+ .then(createConnectionResponse)
+ .then(postToOperator)
+ .then(() => 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') {
+ // 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
+
+ // 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())
+}
+
+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)
+}
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] },
+ )
+}
diff --git a/lib/services/storage.js b/lib/services/storage.js
index fcf9826f..fcbdcc82 100644
--- a/lib/services/storage.js
+++ b/lib/services/storage.js
@@ -40,11 +40,22 @@ export const storeConnection = async ({ serviceId, ...rest }) => {
)
}
-export const storeKey = async ({ privateKey, privateKeyPem }) => {
+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}`,
JSON.stringify({ privateKey, privateKeyPem }),
)
+ return { privateKey, privateKeyPem }
}
export const getPrivateKey = async kid => {
diff --git a/lib/services/tokens.js b/lib/services/tokens.js
deleted file mode 100644
index 8b5c4813..00000000
--- a/lib/services/tokens.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import { sign } from './jwt'
-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 } },
- { publicKey, privateKeyPem, privateKey },
-) => {
- return sign(
- {
- type: 'ACCOUNT_REGISTRATION',
- aud: Config.OPERATOR_URL,
- iss: `egendata://account/${id}`,
- pds: { provider, access_token },
- },
- {
- jwk: privateKey,
- pem: privateKeyPem,
- },
- {
- jwk: publicKey,
- alg: schemas.algs[0],
- },
- )
-}
-
-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,
- connectionId,
-) => {
- const { publicKey, privateKey, privateKeyPem } = await getAccountKeys()
- const body = {
- type: 'CONNECTION',
- aud: iss,
- iss: 'egendata://account',
- sid,
- sub: connectionId,
- permissions,
- }
- return sign(
- body,
- {
- jwk: privateKey,
- pem: privateKeyPem,
- },
- {
- jwk: publicKey,
- alg: schemas.algs[0],
- },
- )
-}
-
-export const createConnectionResponse = async payload => {
- const { id } = await getAccount()
- const { publicKey, privateKey, privateKeyPem } = await getAccountKeys()
- return sign(
- {
- type: 'CONNECTION_RESPONSE',
- aud: Config.OPERATOR_URL,
- iss: `egendata://account/${id}`,
- payload,
- },
- {
- jwk: privateKey,
- pem: privateKeyPem,
- },
- { jwk: publicKey, alg: schemas.algs[0] },
- )
-}
-
-export const createLogin = async ({ serviceId, connectionId }, sessionId) => {
- if (!sessionId) {
- throw Error('SessionId is missing')
- }
- const { publicKey, privateKey, privateKeyPem } = await getAccountKeys()
- return sign(
- {
- type: 'LOGIN',
- aud: serviceId,
- sid: sessionId,
- sub: connectionId,
- iss: 'egendata://account',
- },
- {
- jwk: privateKey,
- pem: privateKeyPem,
- },
- { jwk: publicKey, alg: schemas.algs[0] },
- )
-}
-
-export const createLoginResponse = async loginPayload => {
- const { id } = await getAccount()
- const { publicKey, privateKey, privateKeyPem } = await getAccountKeys()
- return sign(
- {
- type: 'LOGIN_RESPONSE',
- payload: loginPayload,
- iss: `egendata://account/${id}`,
- aud: Config.OPERATOR_URL,
- },
- {
- jwk: privateKey,
- pem: privateKeyPem,
- },
- { jwk: publicKey, alg: schemas.algs[0] },
- )
-}
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 ae454ffc..37312ae5 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 ",
@@ -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",