Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate @web5/identity-agent to create identity on app launch #62

Merged
merged 4 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions blob-polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { getArrayBufferForBlob } from "react-native-blob-jsi-helper";
import { ReadableStream } from "web-streams-polyfill";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have https://github.com/browserify/stream-browserify and readable-stream. do we need to add this or can we make do with the stream polyfills we already have?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially had tried that, but it's incompatible with this part of Web5 https://github.com/TBD54566975/web5-js/blob/v0.7.11/packages/web5-user-agent/src/utils.ts#L8


const decoder = new TextDecoder();

/**
* React Native's Blob implementation has a constructor that currently only supports
* constructing from parts that are of type: Array<Blob | String>.
Comment on lines +7 to +8
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Native's Blob can be found at ./node_modules/react-native/Libraries/Blob/Blob.js

*
* Web5 packages need to be able to construct a Blob where one of the parts is of type `Uint8Array`.
*
* This function updates the constructor of Blob to convert any parts that are of type `Uint8Array`
* into a `string`, so that it can properly work with React Native's Blob implementation.
*/
function monkeyPatchBlobConstructor() {
const OriginalBlob = global.Blob;

const blobProxyHandler = {
construct(target: any, argumentsList: any) {
const blobParts = argumentsList[0];
const options = argumentsList[1];

if (blobParts) {
for (const [index, element] of blobParts.entries()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how many times are we potentially looping here? should we do an upstream PR to fix this inside of react native itself?

Copy link
Member

@shamilovtim shamilovtim Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is, potentially, very expensive code. if we are using blobs elsewhere we risk torpedoing the app's performance. i wonder what an upstream solution would involve?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. It'll loop as for as many entries that are in blobParts

  2. I don't really know what an upstream solution would involve here. This was the majority of what took me a week to get this work done. Just figuring out that it was the problem was most of the battle. It's expensive, for sure. But it's working, and it can be our baseline for the time being.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should probably slap a TODO here and we should create an issue that tracks the TODO. We'll want visibility into the performance impact and it would be a good issue that could be contributed upstream by either OSS stakeholders or our own teams

Copy link
Contributor Author

@amika-sq amika-sq Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a task for us: #64
Referred to that task in a TODO in code here: f6d6587

if (element instanceof Uint8Array) {
blobParts[index] = decoder.decode(element);
}
}
}

return new target(blobParts, options);
},
};

const blobProxy = new Proxy(OriginalBlob, blobProxyHandler);
(global as any).Blob = blobProxy;
amika-sq marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* React Native's Blob implementation currently does provide a `stream()` function.
*
* Web5 packages depend on this function for a multitude of functionality.
*
* This function provides a polyfill for this function if it does not exist.
*/
function polyfillBlobStream() {
if (typeof Blob !== "undefined") {
if (!Blob.prototype.stream) {
Blob.prototype.stream = function (): ReadableStream<Uint8Array> {
const blob = this;

return new ReadableStream({
async start(controller) {
const arrayBuffer = await getArrayBuffer(blob);
controller.enqueue(arrayBuffer);
controller.close();
},
});
};
}
}
}

/**
* Web5 will often create a Blob, then immediately use the Blob's `stream()`
* to stream the data for various purposes.
*
* In React Native, Blobs are created via NativeBlobModule. Despite the API to
* create Blobs being synchronous, the actual bytes backing the Blob may not be
* available immmediately.
*
* This function looks for the backing arrayBuffer for a Blob, with small microsleeps
* until it is ultimately available via the JSI.
*/
async function getArrayBuffer(blob: Blob): Promise<Uint8Array> {
let arrayBuffer: Uint8Array | undefined = undefined;
try {
arrayBuffer = getArrayBufferForBlob(blob);
} catch {}

if (arrayBuffer && arrayBuffer.length === blob.size) {
return arrayBuffer;
} else {
// The buffer isn't available yet from the JSI.
// Microsleep to advance to the next runloop, and try again.
await sleep(1);
return getArrayBuffer(blob);
}
}

// eslint-disable-next-line require-await
amika-sq marked this conversation as resolved.
Show resolved Hide resolved
async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export function polyfillBlob() {
monkeyPatchBlobConstructor();
polyfillBlobStream();
}
8 changes: 8 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import "@tbd54566975/web5-react-native-polyfills";
import { registerRootComponent } from "expo";
import { polyfillBlob } from "./blob-polyfill";

if (!global.structuredClone) {
var structuredClone = require("realistic-structured-clone");
amika-sq marked this conversation as resolved.
Show resolved Hide resolved
global.structuredClone = structuredClone;
}

polyfillBlob();

import App from "./src/App";
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
Expand Down
7 changes: 7 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ PODS:
- React-callinvoker
- React-Core
- ReactCommon/turbomodule/core
- react-native-blob-jsi-helper (0.3.1):
- React
- React-Core
- react-native-leveldb (3.3.1):
- React-Core
- react-native-mmkv (2.10.1):
Expand Down Expand Up @@ -525,6 +528,7 @@ DEPENDENCIES:
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- react-native-bignumber (from `../node_modules/react-native-bignumber`)
- react-native-blob-jsi-helper (from `../node_modules/react-native-blob-jsi-helper`)
- react-native-leveldb (from `../node_modules/react-native-leveldb`)
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
- react-native-quick-base64 (from `../node_modules/react-native-quick-base64`)
Expand Down Expand Up @@ -626,6 +630,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/logger"
react-native-bignumber:
:path: "../node_modules/react-native-bignumber"
react-native-blob-jsi-helper:
:path: "../node_modules/react-native-blob-jsi-helper"
react-native-leveldb:
:path: "../node_modules/react-native-leveldb"
react-native-mmkv:
Expand Down Expand Up @@ -713,6 +719,7 @@ SPEC CHECKSUMS:
React-jsinspector: b511447170f561157547bc0bef3f169663860be7
React-logger: c5b527272d5f22eaa09bb3c3a690fee8f237ae95
react-native-bignumber: 9850896db4534ea31e89dfd88182e7412eace199
react-native-blob-jsi-helper: 13c10135af4614dbc0712afba5960784cd44f043
react-native-leveldb: 2abee90737d7bba6399adf7767482ab5d84caa66
react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f
react-native-quick-base64: 62290829c619fbabca4c41cfec75ae759d08fc1c
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@
"@react-navigation/bottom-tabs": "6.5.8",
"@react-navigation/native": "6.1.7",
"@react-navigation/native-stack": "6.9.13",
"@tbd54566975/dwn-sdk-js": "0.1.0",
"@tbd54566975/dwn-sdk-js": "0.2.1",
"@tbd54566975/web5": "0.7.11",
"@tbd54566975/web5-react-native-metro-config": "0.1.2",
"@tbd54566975/web5-react-native-polyfills": "0.1.3",
"@web5/identity-agent": "0.1.0-alpha-20230823-44789a4",
"expo": "49.0.3",
"expo-barcode-scanner": "12.5.3",
"expo-level": "file:assets/expo-level",
Expand All @@ -41,15 +42,19 @@
"react": "18.2.0",
"react-native": "0.72.3",
"react-native-bignumber": "0.2.1",
"react-native-blob-jsi-helper": "0.3.1",
"react-native-leveldb": "3.3.1",
"react-native-mmkv": "2.10.1",
"react-native-paper": "5.9.1",
"react-native-quick-base64": "2.0.6",
"react-native-quick-crypto": "0.6.1",
"react-native-safe-area-context": "4.7.1",
"react-native-screens": "3.22.1",
"readable-stream": "4.4.2",
"realistic-structured-clone": "3.0.0",
"typescript": "4.9.4",
"verite": "0.0.6"
"verite": "0.0.6",
"web-streams-polyfill": "3.2.1"
},
"devDependencies": {
"@babel/core": "7.22.9",
Expand Down
81 changes: 81 additions & 0 deletions patches/@web5+agent+0.1.7-alpha-20230823-44789a4.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
diff --git a/node_modules/@web5/agent/dist/esm/app-data-store.js b/node_modules/@web5/agent/dist/esm/app-data-store.js
index e8e64f0..e418b51 100644
--- a/node_modules/@web5/agent/dist/esm/app-data-store.js
+++ b/node_modules/@web5/agent/dist/esm/app-data-store.js
@@ -11,7 +11,7 @@ import { DidKeyMethod } from '@web5/dids';
import { hkdf } from '@noble/hashes/hkdf';
import { sha256 } from '@noble/hashes/sha256';
import { sha512 } from '@noble/hashes/sha512';
-import { randomBytes } from '@web5/crypto/utils';
+import { randomBytes } from '@web5/crypto/dist/esm/utils';
import { pbkdf2Async } from '@noble/hashes/pbkdf2';
import { Convert, MemoryStore } from '@web5/common';
import { CryptoKey, Jose, XChaCha20Poly1305 } from '@web5/crypto';
diff --git a/node_modules/@web5/agent/dist/esm/dwn-manager.js b/node_modules/@web5/agent/dist/esm/dwn-manager.js
index 3b68855..0c80d18 100644
--- a/node_modules/@web5/agent/dist/esm/dwn-manager.js
+++ b/node_modules/@web5/agent/dist/esm/dwn-manager.js
@@ -8,9 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
import { Jose } from '@web5/crypto';
-import * as didUtils from '@web5/dids/utils';
+import * as didUtils from '@web5/dids/dist/esm/utils';
import { Convert, removeUndefinedProperties } from '@web5/common';
-import { EventLogLevel, DataStoreLevel, MessageStoreLevel, } from '@tbd54566975/dwn-sdk-js/stores';
+import { EventLogLevel, DataStoreLevel, MessageStoreLevel, } from '@tbd54566975/dwn-sdk-js/dist/esm/src/index-stores';
import { Cid, Dwn, Message, EventsGet, DataStream, RecordsRead, MessagesGet, RecordsWrite, RecordsQuery, DwnMethodName, RecordsDelete, ProtocolsQuery, DwnInterfaceName, ProtocolsConfigure, } from '@tbd54566975/dwn-sdk-js';
import { isManagedKeyPair } from './utils.js';
import { blobToIsomorphicNodeReadable, webReadableToIsomorphicNodeReadable } from './utils.js';
diff --git a/node_modules/@web5/agent/dist/esm/kms-local.js b/node_modules/@web5/agent/dist/esm/kms-local.js
index 561cc9c..4475f51 100644
--- a/node_modules/@web5/agent/dist/esm/kms-local.js
+++ b/node_modules/@web5/agent/dist/esm/kms-local.js
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
-import { isCryptoKeyPair, checkRequiredProperty } from '@web5/crypto/utils';
+import { isCryptoKeyPair, checkRequiredProperty } from '@web5/crypto/dist/esm/utils';
import { EcdhAlgorithm, EcdsaAlgorithm, EdDsaAlgorithm, AesCtrAlgorithm, } from '@web5/crypto';
import { isManagedKey, isManagedKeyPair } from './utils.js';
import { KeyStoreMemory, PrivateKeyStoreMemory } from './store-managed-key.js';
diff --git a/node_modules/@web5/agent/dist/esm/rpc-client.js b/node_modules/@web5/agent/dist/esm/rpc-client.js
index 020df4e..c768329 100644
--- a/node_modules/@web5/agent/dist/esm/rpc-client.js
+++ b/node_modules/@web5/agent/dist/esm/rpc-client.js
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
-import { randomUuid } from '@web5/crypto/utils';
+import { randomUuid } from '@web5/crypto/dist/esm/utils';
import { createJsonRpcRequest, parseJson } from './json-rpc.js';
/**
* Client used to communicate with Dwn Servers
diff --git a/node_modules/@web5/agent/dist/esm/store-managed-key.js b/node_modules/@web5/agent/dist/esm/store-managed-key.js
index ab4cb58..1a42192 100644
--- a/node_modules/@web5/agent/dist/esm/store-managed-key.js
+++ b/node_modules/@web5/agent/dist/esm/store-managed-key.js
@@ -18,7 +18,7 @@ var __rest = (this && this.__rest) || function (s, e) {
}
return t;
};
-import { randomUuid } from '@web5/crypto/utils';
+import { randomUuid } from '@web5/crypto/dist/esm/utils';
import { Convert, removeEmptyObjects, removeUndefinedProperties } from '@web5/common';
import { isManagedKeyPair } from './utils.js';
/**
diff --git a/node_modules/@web5/agent/dist/esm/test-managed-agent.js b/node_modules/@web5/agent/dist/esm/test-managed-agent.js
index 2835aa0..cec93df 100644
--- a/node_modules/@web5/agent/dist/esm/test-managed-agent.js
+++ b/node_modules/@web5/agent/dist/esm/test-managed-agent.js
@@ -11,7 +11,7 @@ import { Jose } from '@web5/crypto';
import { Dwn } from '@tbd54566975/dwn-sdk-js';
import { LevelStore, MemoryStore } from '@web5/common';
import { DidIonMethod, DidKeyMethod, DidResolver, DidResolverCacheLevel } from '@web5/dids';
-import { MessageStoreLevel, DataStoreLevel, EventLogLevel } from '@tbd54566975/dwn-sdk-js/stores';
+import { MessageStoreLevel, DataStoreLevel, EventLogLevel } from '@tbd54566975/dwn-sdk-js/dist/esm/src/index-stores';
import { LocalKms } from './kms-local.js';
import { DidManager } from './did-manager.js';
import { DwnManager } from './dwn-manager.js';
13 changes: 13 additions & 0 deletions patches/@web5+dids+0.1.9-alpha-20230823-44789a4.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/node_modules/@web5/dids/dist/esm/did-key.js b/node_modules/@web5/dids/dist/esm/did-key.js
index 420f8ab..9a0c961 100644
--- a/node_modules/@web5/dids/dist/esm/did-key.js
+++ b/node_modules/@web5/dids/dist/esm/did-key.js
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
import { universalTypeOf } from '@web5/common';
-import { keyToMultibaseId, multibaseIdToKey } from '@web5/crypto/utils';
+import { keyToMultibaseId, multibaseIdToKey } from '@web5/crypto/dist/esm/utils';
import { Jose, Ed25519, Secp256k1, EcdsaAlgorithm, EdDsaAlgorithm, } from '@web5/crypto';
import { getVerificationMethodTypes, parseDid } from './utils.js';
const SupportedCryptoAlgorithms = [
16 changes: 16 additions & 0 deletions patches/react-native-blob-jsi-helper+0.3.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
diff --git a/node_modules/react-native-blob-jsi-helper/android/src/main/cpp/cpp-adapter.cpp b/node_modules/react-native-blob-jsi-helper/android/src/main/cpp/cpp-adapter.cpp
index 1f05fe2..faf7534 100644
--- a/node_modules/react-native-blob-jsi-helper/android/src/main/cpp/cpp-adapter.cpp
+++ b/node_modules/react-native-blob-jsi-helper/android/src/main/cpp/cpp-adapter.cpp
@@ -118,6 +118,11 @@ Java_com_reactnativeblobjsihelper_BlobJsiHelperModule_nativeInstall(JNIEnv *env,
offset,
size);
env->DeleteLocalRef(jstring);
+ if (env->ExceptionCheck()) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ throw std::runtime_error("Error calling getBufferJava");
+ }

jboolean isCopy = true;
jbyte* bytes = env->GetByteArrayElements(boxedBytes, &isCopy);
11 changes: 10 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DwnService } from "./features/dwn/dwn-service";
import { enableLegendStateReact } from "@legendapp/state/react";
import { StatusBar } from "expo-status-bar";
import { linking } from "./navigation/deep-links";
import { bootstrapIdentityAgent } from "./features/identity/identity-agent";

enableLegendStateReact();

Expand All @@ -19,7 +20,15 @@ export const theme: typeof MD3DarkTheme = {

export default function App() {
useEffect(() => {
void DwnService.initExpoLevelDwn();
const startupTasks = async () => {
await DwnService.initExpoLevelDwn();
await bootstrapIdentityAgent(
"passphrase",
"Personal",
DwnService.getDwn()
);
};
void startupTasks();
}, []);

return (
Expand Down
31 changes: 31 additions & 0 deletions src/features/identity/expo-level-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { KeyValueStore } from "@web5/common";
import { ExpoLevel } from "expo-level";

export class ExpoLevelStore implements KeyValueStore<string, any> {
private store: ExpoLevel<string, string>;

constructor(location = "DATASTORE") {
this.store = new ExpoLevel(location);
}

async clear(): Promise<void> {
await this.store.clear();
}

async close(): Promise<void> {
await this.store.close();
}

async delete(key: string): Promise<boolean> {
await this.store.del(key);
return true;
}

async get(key: string): Promise<any> {
return await this.store.get(key);
}

async set(key: string, value: any): Promise<void> {
await this.store.put(key, value);
}
}
29 changes: 29 additions & 0 deletions src/features/identity/identity-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { IdentityAgent } from "@web5/identity-agent";
import { AppDataVault, DwnManager } from "@web5/agent";
import { ExpoLevelStore } from "./expo-level-store";
import { Dwn } from "@tbd54566975/dwn-sdk-js";

export async function bootstrapIdentityAgent(
passphrase: string,
name: string,
dwn: Dwn
) {
const dwnManager = new DwnManager({ dwn });
const appData = new AppDataVault({
keyDerivationWorkFactor: 1,
store: new ExpoLevelStore("AppDataVault"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a stopgap until we create a securestore for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frankhinek is there any plans here? afaik, this is the solution that was come up with, no idea about secure stores yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my question is are we supposed to handle encryption using the device's native libraries (e.g. using keychain) or is web5 internally handling encryption for us?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to @frankhinek's document, that encryption is already done before being put into the store: https://hackmd.io/ShKU_Q9aQC6C96v7SaXl3A?view#First-Launch

@frankhinek: lmk if that's not correct, and we need to do some additional encryption on top!

});

console.log("Creating IdentityAgent...");
const agent = await IdentityAgent.create({ dwnManager, appData });
console.log("Starting IdentityAgent...");
await agent.start({ passphrase });

console.log(`Creating ${name} ManagedIdentity...`);
const identity = await agent.identityManager.create({
name,
didMethod: "ion",
kms: "local",
});
console.log(`Created ${identity.name} ManagedIdentity: ${identity.did}`);
}
Loading