-
Alexey Lunin authoredAlexey Lunin authored
AgentStore.ts 17.92 KiB
import { makeAutoObservable, runInAction } from "mobx";
import {
Agent,
AutoAcceptCredential,
AutoAcceptProof,
BasicMessageEventTypes,
BasicMessageRole,
BasicMessageStateChangedEvent,
ConnectionsModule,
ConsoleLogger,
CredentialsModule,
DidsModule,
HttpOutboundTransport,
InitConfig,
KeyDerivationMethod,
KeyDidRegistrar,
KeyDidResolver,
LogLevel,
MediationRecipientModule,
PeerDidRegistrar,
PeerDidResolver,
ProofsModule,
V2CredentialProtocol,
V2ProofProtocol,
W3cCredentialService,
W3cCredentialsModule,
WsOutboundTransport,
} from "@aries-framework/core";
import { agentDependencies } from "@aries-framework/react-native";
import {
AnonCredsCredentialFormatService,
AnonCredsModule,
AnonCredsProofFormatService,
LegacyIndyProofFormatService,
V1CredentialProtocol,
V1ProofProtocol,
} from "@aries-framework/anoncreds";
import {
IndyVdrAnonCredsRegistry,
IndyVdrIndyDidRegistrar,
IndyVdrIndyDidResolver,
IndyVdrModule,
} from "@aries-framework/indy-vdr";
import { AnonCredsRsModule } from "@aries-framework/anoncreds-rs";
import Config from "react-native-config";
import { anoncreds } from "@hyperledger/anoncreds-react-native";
import { indyVdr } from "@hyperledger/indy-vdr-react-native";
import { AskarModule } from "@aries-framework/askar";
import { ariesAskar } from "@hyperledger/aries-askar-react-native";
import uuid from "react-native-uuid";
import indyLedgers from "src/configs/ledgers/indy";
import { getMnemonicArrayFromWords } from "src/utils/generic";
import {
getGuid,
getImapConfig,
getPasscode,
getPassphrase,
setGuid,
setPasscode,
setPassphrase,
} from "src/utils/keychain";
import { RootStore } from "src/store/rootStore";
// @ts-ignore
import argon2 from "react-native-argon2";
import { salt } from "src/constants/constants";
import {
WalletConfig,
WalletExportImportConfig,
} from "@aries-framework/core/build/types";
import { LegacyIndyCredentialFormatService } from "@aries-framework/anoncreds/build/formats/LegacyIndyCredentialFormatService";
import { infoToast, successToast, warningToast } from "src/utils/toast";
import { Buffer } from "buffer";
import Realm from "realm";
import MimeParser from "@vereign/lib-mime";
import notifee, {
AndroidImportance,
AuthorizationStatus,
} from "@notifee/react-native";
import Email from "../db-models/Email";
import TrustedEmailSender from "../db-models/TrustedEmailSender";
import axios from "axios";
import { NativeModules } from "react-native";
import { addHours } from "date-fns";
import { JsonLdCredentialFormatService } from "../credo/JsonLdCredentialFormatService";
const { VereignImapModule } = NativeModules;
class AgentStore {
private _rootStore: RootStore;
private _agent: Agent | null = null;
private _realm!: Realm;
public get agent(): Agent {
return this._agent as Agent;
}
private _navigation!: any;
public get agentCreated() {
return !!this._agent;
}
public injectNavigation = (navigation: any) => {
this._navigation = navigation;
};
public injectRealm = (realm: Realm) => {
this._realm = realm;
};
public get active(): boolean {
return this._agent?.isInitialized || false;
}
constructor(rootStore: RootStore) {
this._rootStore = rootStore;
makeAutoObservable(this);
this.generateGuidAndPassphraseIfNotPresented();
}
public generateGuidAndPassphraseIfNotPresented = async () => {
const guid = await getGuid();
if (!guid) {
const guid = uuid.v4() as string;
const mnemonicWordsList = getMnemonicArrayFromWords(8);
const mnemonic = mnemonicWordsList.join(" ");
await setPassphrase(mnemonic);
await setGuid(guid);
}
};
public createAgent = async () => {
const guid = await getGuid();
const passcode = await getPasscode();
if (!passcode) {
return; // it's not possible to create an agent without passcode
}
const config: InitConfig = {
label: guid,
logger: new ConsoleLogger(LogLevel.test),
walletConfig: {
storage: {
type: "sqlite",
},
id: guid,
key: passcode,
keyDerivationMethod: KeyDerivationMethod.Argon2IMod,
},
endpoints: [],
};
const legacyIndyCredentialFormatService =
new LegacyIndyCredentialFormatService();
const legacyIndyProofFormatService = new LegacyIndyProofFormatService();
const modules = {
connections: new ConnectionsModule({
autoAcceptConnections: true,
}),
credentials: new CredentialsModule({
autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
credentialProtocols: [
new V1CredentialProtocol({
indyCredentialFormat: legacyIndyCredentialFormatService,
}),
new V2CredentialProtocol({
credentialFormats: [
legacyIndyCredentialFormatService,
new AnonCredsCredentialFormatService(),
new JsonLdCredentialFormatService()
],
}),
],
}),
proofs: new ProofsModule({
autoAcceptProofs: AutoAcceptProof.ContentApproved,
proofProtocols: [
new V1ProofProtocol({
indyProofFormat: legacyIndyProofFormatService,
}),
new V2ProofProtocol({
proofFormats: [
legacyIndyProofFormatService,
new AnonCredsProofFormatService(),
],
}),
],
}),
anoncreds: new AnonCredsModule({
registries: [new IndyVdrAnonCredsRegistry()],
autoCreateLinkSecret: true,
}),
anoncredsRs: new AnonCredsRsModule({
anoncreds,
autoCreateLinkSecret: true,
}),
indyVdr: new IndyVdrModule({
indyVdr,
networks: indyLedgers,
}),
dids: new DidsModule({
registrars: [
new IndyVdrIndyDidRegistrar(),
new KeyDidRegistrar(),
new PeerDidRegistrar(),
],
resolvers: [
new IndyVdrIndyDidResolver(),
new KeyDidResolver(),
new PeerDidResolver(),
],
}),
askar: new AskarModule({
ariesAskar,
}),
mediationRecipient: new MediationRecipientModule({
// mediatorPickupStrategy: MediatorPickupStrategy.Implicit,
mediatorInvitationUrl: Config.MEDIATOR_URL,
}),
w3c: new W3cCredentialsModule()
};
const newAgent = new Agent({
config,
dependencies: agentDependencies,
modules,
});
const w3cCredentialService = newAgent.context.dependencyManager.resolve(W3cCredentialService);
// TODO Hack dor demo purpose
// @ts-ignore
w3cCredentialService.verifyCredential = () => ({ isValid: true })
newAgent.registerOutboundTransport(new WsOutboundTransport());
newAgent.registerOutboundTransport(new HttpOutboundTransport());
runInAction(() => {
this._agent = newAgent;
});
console.log("staring to listen for messages");
newAgent.events.on(
BasicMessageEventTypes.BasicMessageStateChanged,
(e: BasicMessageStateChangedEvent) => this._processAgentMessage(e)
);
};
public startAgent = async () => {
console.debug("AGENT_STORE: initializing agent");
if (!this._agent) {
throw new Error("Agent has not been created");
}
if (!this._agent.isInitialized) {
console.debug(
"AGENT_STORE: Wallet is not initialized. call agent.initialize()"
);
await this._agent.initialize();
}
if (!this.agent.isInitialized) {
console.debug("AGENT_STORE: Wallet is NOT initialized");
throw new Error("agent not initialized");
} else {
console.debug("AGENT_STORE: Wallet is initialized");
}
};
private _processAgentMessage = async (ev: BasicMessageStateChangedEvent) => {
if (ev.payload.basicMessageRecord.role === BasicMessageRole.Receiver) {
let emailContent = ev.payload.basicMessageRecord.content;
try {
emailContent = Buffer.from(emailContent, "base64").toString();
} catch (e: any) {
console.error("Error decoding base64", e);
warningToast("Email content is not base64 encoded");
}
try {
let parser = new MimeParser(emailContent);
const val = (header: string[] | null) => {
return (header && header[0]) || "";
};
const svdxBaseUrlHeader = parser.getGlobalHeaderValue("x-svdx-hostname");
const svdxBaseUrl = (val(svdxBaseUrlHeader).startsWith("https://") || val(svdxBaseUrlHeader).startsWith("http://")) ? val(svdxBaseUrlHeader) : `https://${svdxBaseUrlHeader}`;
console.log(`base url parsed from mime is: ${svdxBaseUrl}`);
const messageIdHeader = parser.getGlobalHeaderValue("message-id");
const messageId = val(messageIdHeader);
const svdxIdHeader = parser.getGlobalHeaderValue("x-message-id");
const svdxId = val(svdxIdHeader);
const receivedDate = new Date();
const createdAtHeader = parser.getGlobalHeaderValue("date");
let createdAt = receivedDate;
if (createdAtHeader) {
createdAt = new Date(val(createdAtHeader) as string);
}
const fromHeader = parser.getGlobalHeaderValue("from");
const from = val(fromHeader);
const toHeader = parser.getGlobalHeaderValue("to");
const to = toHeader && toHeader.length ? toHeader.join(", ") : "";
const ccHeader = parser.getGlobalHeaderValue("cc");
const cc = ccHeader && ccHeader.length ? ccHeader.join(", ") : "";
const bccHeader = parser.getGlobalHeaderValue("bcc");
const bcc = bccHeader && bccHeader.length ? bccHeader.join(", ") : "";
const subjectHeader = parser.getGlobalHeaderValue("subject");
const subject = val(subjectHeader);
const realmRecordId = new Realm.BSON.ObjectId();
this._realm.write(() => {
const newEmail: Partial<Email> = {
_id: realmRecordId,
messageId: messageId,
createdAt: createdAt,
receivedDate: receivedDate,
type: "svdx-mail",
from: from,
to: to,
cc: cc,
bcc: bcc,
subject: subject,
read: false,
svdxMime: emailContent,
svdxImapSaved: false,
svdxBaseUrl: svdxBaseUrl,
svdxMessageId: svdxId,
sealUrl: null,
};
this._realm.create("Email", newEmail);
});
infoToast("Received new message");
if (this.isTrustedSender(from)) {
await this._saveEmail(svdxBaseUrl, svdxId, realmRecordId, emailContent);
}
this._displayNewEmailReceived(from, subject, realmRecordId.toString());
} catch (e) {
warningToast("Email is received but it can not be decoded");
return;
}
}
};
public isTrustedSender = (sender: string) => {
const emails = this._realm.objects("TrustedEmailSender")
.filtered('sender == $0', sender);
return emails.length > 0;
}
public trustSender = (sender: string) => {
if (this.isTrustedSender(sender)) {
console.warn("Sender is already trusted");
return;
}
const realmRecordId = new Realm.BSON.ObjectId();
this._realm.write(() => {
const newTrustedEmailSender: Partial<TrustedEmailSender> = {
_id: realmRecordId,
sender: sender
}
this._realm.create('TrustedEmailSender', newTrustedEmailSender);
});
}
private _saveEmail = async (
svdxBaseUrl: string,
svdxMessageId: string,
realmId: Realm.BSON.ObjectId,
mime: string
) => {
try {
const config = await getImapConfig();
if (config) {
if (!(await this._checkCanSaveEmail(svdxBaseUrl,svdxMessageId))) return;
VereignImapModule.saveMessage(
config!.host,
config!.username,
config!.password,
mime,
(success: boolean, error: any) => {
if (success) {
successToast("Email is saved");
const email = this._realm.objectForPrimaryKey("Email", realmId);
if (email) {
this._realm.write(() => {
email.svdxImapSaved = true;
});
this._notifySVDXServer(svdxBaseUrl,svdxMessageId);
}
}
if (error) {
warningToast("Something went wrong " + error);
}
}
);
}
} catch (e: any) {
warningToast(e.message);
}
};
private _checkCanSaveEmail = async (svdxBaseUrl: string,svdxMessageId: string) => {
try {
console.log(`Sending request to ${svdxBaseUrl}/v1/messages/status for message with id ${svdxMessageId}`);
const response = await axios({
url: `${svdxBaseUrl}/v1/message/status`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: {
messageId: svdxMessageId,
},
});
// Check for HTTP 200 status
if (response.status === 200) {
console.log("Message has not been delivered. All is ok");
return true;
}
return false;
} catch (error: any) {
if (error.response && error.response.status === 404) {
// Handle the case where the message has been delivered already in another way
console.log("Message has been delivered already in another way");
} else {
// Handle other errors
console.error("An error occurred:", error.message);
}
}
return false;
};
private _notifySVDXServer = async (svdxBaseUrl: string,svdxMessageId: string) => {
try {
console.log(`Sending request to ${svdxBaseUrl}/v1/messages/read for message with id ${svdxMessageId}`);
await axios({
url: `${svdxBaseUrl}/v1/message/read`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: {
messageId: svdxMessageId,
},
});
console.log("Message has been marked as read");
} catch (e: any) {
console.error(
"Something went wrong during notifying SVDX server about read message",
e.message
);
}
};
private _displayNewEmailReceived = async (
from: string,
subject: string,
realmId: string
) => {
const authResult = await notifee.requestPermission();
if (authResult.authorizationStatus !== AuthorizationStatus.AUTHORIZED) {
return;
}
const channelId = await notifee.createChannel({
id: "default",
name: "Default Channel",
importance: AndroidImportance.HIGH,
});
// Display a notification
await notifee.displayNotification({
title: `New Email from ${from}`,
body: `Subject: ${subject || "[No subject]"}`,
data: {
type: "new-email-received",
realmId: realmId,
},
android: {
channelId,
// smallIcon: 'ic_launcher', // optional, defaults to 'ic_launcher'.
// pressAction is needed if you want the notification to open the app when pressed
pressAction: {
id: "default",
},
},
});
};
public checkNotSavedEmailMessages = async () => {
// get all today's not saved messages
const today = new Date();
const emails = this._realm
.objects<Email>("Email")
.filtered(
"type == $0 && svdxImapSaved == false AND receivedDate > $1",
"svdx-mail",
addHours(today, -6)
);
for (let i = 0; i < emails.length; i++) {
const mailRow = emails[i];
if (this.isTrustedSender(mailRow.from)) {
await this._saveEmail(
mailRow.svdxBaseUrl,
mailRow.svdxMessageId!,
mailRow._id!,
mailRow.svdxMime!
);
}
}
};
public stopAgent = async () => {
console.debug("Stop agent");
// TODO
console.warn("As soon as closing pool is not implemented here:");
console.warn(
"https://github.com/hyperledger/aries-framework-javascript/blob/main/packages/indy-vdr/src/pool/IndyVdrPool.ts#L83"
);
console.warn(
"Closing agent breaking initializing after shutdown and is disabled"
);
// await this._agent?.shutdown();
};
public importWallet = async (
mnemonic: string,
walletBackupFilePath: string
) => {
await this.createAgent();
const { encodedHash } = await argon2(mnemonic, salt, {
iterations: 5,
memory: 16 * 1024,
parallelism: 2,
hashLength: 20,
mode: "argon2i",
});
const walletConfig = this.agent.wallet.walletConfig as WalletConfig;
const importConfig: WalletExportImportConfig = {
key: encodedHash,
path: walletBackupFilePath,
};
await this.agent.wallet.import(walletConfig, importConfig);
await this.agent.wallet.initialize(walletConfig);
await this.agent.initialize();
await setPassphrase(mnemonic);
this._rootStore.setAuthenticated(true);
};
public exportWallet = async (toPath: string) => {
if (!this._agent) {
throw new Error("Agent has not been created");
}
const passphrase = getPassphrase();
const result = await argon2(passphrase, salt, {
iterations: 5,
memory: 16 * 1024,
parallelism: 2,
hashLength: 20,
mode: "argon2i",
});
const { encodedHash } = result;
const exportConfig: WalletExportImportConfig = {
key: encodedHash,
path: toPath,
};
await this._agent.wallet.export(exportConfig);
};
public changePin = async (newPin: string) => {
if (!this._agent) {
throw new Error("Agent has not been created");
}
const guid = await getGuid();
const oldPasscode = await getPasscode();
await this._agent.shutdown();
await this._agent.wallet.rotateKey({
id: guid,
key: oldPasscode,
rekey: newPin,
});
await this._agent.initialize();
await setPasscode(newPin);
};
public deleteWalletData = async () => {
if (!this._agent) return;
await this._agent.wallet.delete();
await this._agent.shutdown();
this._agent = null;
};
}
export type { AgentStore };
export default AgentStore;