diff --git a/package-lock.json b/package-lock.json
index d6ecae6b..befb92f8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@sardinefish/x509": "^1.2.1",
"@semaphore-protocol/identity": "^4.3.0",
"@shadcn/ui": "^0.0.4",
+ "@simplewebauthn/browser": "^11.0.0",
"async-mutex": "^0.4.0",
"axios": "^1.7.7",
"buffer": "^6.0.3",
@@ -3585,6 +3586,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@simplewebauthn/browser": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-11.0.0.tgz",
+ "integrity": "sha512-KEGCStrl08QC2I561BzxqGiwoknblP6O1YW7jApdXLPtIqZ+vgJYAv8ssLCdm1wD8HGAHd49CJLkUF8X70x/pg==",
+ "dependencies": {
+ "@simplewebauthn/types": "^11.0.0"
+ }
+ },
+ "node_modules/@simplewebauthn/types": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-11.0.0.tgz",
+ "integrity": "sha512-b2o0wC5u2rWts31dTgBkAtSNKGX0cvL6h8QedNsKmj8O4QoLFQFR3DBVBUlpyVEhYKA+mXGUaXbcOc4JdQ3HzA=="
+ },
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"license": "MIT",
diff --git a/package.json b/package.json
index f30a8e30..4e29a0d3 100755
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"@sardinefish/x509": "^1.2.1",
"@semaphore-protocol/identity": "^4.3.0",
"@shadcn/ui": "^0.0.4",
+ "@simplewebauthn/browser": "^11.0.0",
"async-mutex": "^0.4.0",
"axios": "^1.7.7",
"buffer": "^6.0.3",
diff --git a/passkey.zip b/passkey.zip
new file mode 100644
index 00000000..8eb47bc0
Binary files /dev/null and b/passkey.zip differ
diff --git a/src/components/NavHeader/index.tsx b/src/components/NavHeader/index.tsx
index 4196165d..4829076e 100644
--- a/src/components/NavHeader/index.tsx
+++ b/src/components/NavHeader/index.tsx
@@ -142,6 +142,10 @@ export default function NavHeader({
return 'Disabled';
};
+ if (pathname === '/lock') {
+ return null;
+ }
+
return (
{pathname !== '/home' ? (
diff --git a/src/entries/Background/rpc.ts b/src/entries/Background/rpc.ts
index 201a46a7..8b019366 100644
--- a/src/entries/Background/rpc.ts
+++ b/src/entries/Background/rpc.ts
@@ -48,6 +48,7 @@ const charwise = require('charwise');
import { BookmarkManager } from '../../reducers/bookmarks';
import { AttestationObject } from '@eternis/tlsn-js';
+
export enum BackgroundActiontype {
get_requests = 'get_requests',
clear_requests = 'clear_requests',
@@ -88,6 +89,16 @@ export enum BackgroundActiontype {
get_logging_level = 'get_logging_level',
prepare_notarization = 'prepare_notarization',
get_notarization_status = 'get_notarization_status',
+ request_create_identity = 'request_create_identity',
+ request_unlock_extension = 'request_unlock_extension',
+ unlock_extension = 'unlock_extension',
+ close_auth_popup = 'close_auth_popup',
+ identity_updated = 'identity_updated',
+}
+
+export enum AuthActiontype {
+ web_authn_authenticate = 'web_authn_authenticate',
+ web_authn_register = 'web_authn_register',
}
export type BackgroundAction = {
@@ -136,6 +147,8 @@ export type RequestHistory = {
type?: string;
};
+let identitySecret: string | undefined = undefined;
+
export const initRPC = () => {
browser.runtime.onMessage.addListener(
(request, sender, sendResponse): any => {
@@ -194,6 +207,15 @@ export const initRPC = () => {
return true;
case BackgroundActiontype.get_notarization_status:
return handleGetNotarizationStatus(request);
+ case BackgroundActiontype.request_unlock_extension:
+ return handleRequestUnlockExtension(request);
+ case BackgroundActiontype.request_create_identity:
+ return handleRequestCreateIdentity(request);
+ case BackgroundActiontype.close_auth_popup:
+ return handleCloseAuthPopup(request);
+ case BackgroundActiontype.identity_updated:
+ return handleIdentityUpdated(request);
+ default:
break;
}
},
@@ -305,6 +327,7 @@ async function handleRetryProveReqest(
...req,
notaryUrl,
websocketProxyUrl,
+ identitySecret,
},
});
@@ -358,6 +381,7 @@ export async function handleProveRequestStart(
body,
notaryUrl,
websocketProxyUrl,
+ identitySecret,
},
});
@@ -410,6 +434,7 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
body,
notaryUrl,
websocketProxyUrl,
+ identitySecret,
},
});
}
@@ -846,6 +871,7 @@ async function handleNotarizeRequest(request: BackgroundAction) {
body,
notaryUrl,
websocketProxyUrl,
+ identitySecret,
},
});
} catch (e) {
@@ -1066,3 +1092,99 @@ async function handleRunPluginCSRequest(request: BackgroundAction) {
return defer.promise;
}
+
+let authWindow: number | undefined = undefined;
+const createAuthPopup = async (left: number, top: number, width: number) => {
+ const popup = await chrome.windows.create({
+ url: 'auth.html',
+ type: 'panel',
+ width,
+ height: 1,
+ left,
+ top,
+ focused: true,
+ state: 'normal',
+ });
+ authWindow = popup.id;
+};
+
+async function handleRequestUnlockExtension(request: BackgroundAction) {
+ try {
+ const { left, top, width } = request.data;
+ await createAuthPopup(left, top, width);
+ setTimeout(() => {
+ chrome.runtime.sendMessage({
+ type: AuthActiontype.web_authn_authenticate,
+ });
+ }, 300);
+ } catch (e) {
+ console.error('error', e);
+ }
+}
+
+async function handleRequestCreateIdentity(request: BackgroundAction) {
+ try {
+ const { left, top, width, username, userId } = request.data;
+ if (!username || !userId) {
+ throw new Error('username and userId are required');
+ }
+
+ await createAuthPopup(left, top, width);
+ setTimeout(() => {
+ chrome.runtime.sendMessage({
+ type: AuthActiontype.web_authn_register,
+ data: {
+ username,
+ userId,
+ },
+ });
+ }, 300);
+ } catch (e) {
+ console.error('error', e);
+ }
+}
+
+async function handleCloseAuthPopup(request: BackgroundAction) {
+ try {
+ if (authWindow) {
+ // Remove all popup windows otherwise popup.html will keep stacking up
+ // await chrome.windows.remove(authWindow);
+ const windows = await chrome.windows.getAll();
+ await Promise.all(
+ windows.map(async (window) => {
+ if (window.type === 'popup') {
+ await chrome.windows.remove(window.id!);
+ }
+ }),
+ );
+
+ // Calling open popup forces the popup to remain open as focus is lost from previous remove calls
+ await chrome.windows.create({
+ url: 'popup.html',
+ type: 'popup',
+ width: 1,
+ height: 1,
+ });
+
+ // send message unlock extension
+ if (request && request.data && request.data.userId) {
+ chrome.runtime.sendMessage({
+ type: BackgroundActiontype.unlock_extension,
+ data: {
+ userId: request.data.userId,
+ },
+ });
+ }
+
+ authWindow = undefined;
+ }
+ } catch (e) {
+ console.error('error', e);
+ }
+}
+
+async function handleIdentityUpdated(request: BackgroundAction) {
+ if (request.data.identitySecret) {
+ identitySecret = request.data.identitySecret;
+ }
+}
diff --git a/src/entries/Offscreen/Offscreen.tsx b/src/entries/Offscreen/Offscreen.tsx
index e96a63cf..a21e7652 100644
--- a/src/entries/Offscreen/Offscreen.tsx
+++ b/src/entries/Offscreen/Offscreen.tsx
@@ -13,7 +13,7 @@ import { BackgroundActiontype } from '../Background/rpc';
import browser from 'webextension-polyfill';
import { Proof } from '../../utils/types';
import { Method } from '@eternis/tlsn-js/wasm/pkg';
-import { IdentityManager } from '../../reducers/identity';
+import { Identity } from '@semaphore-protocol/identity';
const { init, verify_attestation, Prover, NotarizedSession, TlsProof }: any =
Comlink.wrap(new Worker(new URL('./worker.ts', import.meta.url)));
@@ -212,6 +212,7 @@ async function createProof(options: {
};
body?: any;
id: string;
+ identitySecret: string;
}): Promise
{
const {
url,
@@ -221,10 +222,14 @@ async function createProof(options: {
notaryUrl,
websocketProxyUrl,
id,
+ identitySecret,
} = options;
- const identityManager = new IdentityManager();
- const identity = await identityManager.getIdentity();
+ if (!identitySecret) {
+ throw new Error('IdentitySecret is required, extension is not unlocked');
+ }
+
+ const identity = new Identity(identitySecret);
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
diff --git a/src/entries/Popup/Popup.tsx b/src/entries/Popup/Popup.tsx
index c67cff1c..86af5415 100644
--- a/src/entries/Popup/Popup.tsx
+++ b/src/entries/Popup/Popup.tsx
@@ -44,11 +44,18 @@ import { getConnection } from '../Background/db';
import NavHeader from '../../components/NavHeader';
import Websites from '../../pages/Websites';
import AttestationDetails from '../../pages/AttestationDetails';
+import Locked from '../../pages/Locked';
+import {
+ initIdentity,
+ setIdentity,
+ useIdentity,
+} from '../../reducers/identity';
const Popup = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
+ const { loading, identity } = useIdentity();
useEffect(() => {
(async () => {
@@ -70,12 +77,18 @@ const Popup = () => {
type: BackgroundActiontype.get_prove_requests,
data: tab?.id,
});
+
+ dispatch(await initIdentity());
})();
}, []);
useEffect(() => {
- chrome.runtime.onMessage.addListener((request) => {
+ chrome.runtime.onMessage.addListener(async (request) => {
switch (request.type) {
+ case BackgroundActiontype.unlock_extension: {
+ dispatch(await setIdentity(request.data.userId));
+ break;
+ }
case BackgroundActiontype.push_action: {
if (
request.data.tabId === store.getState().requests.activeTab?.id ||
@@ -95,6 +108,14 @@ const Popup = () => {
});
}, []);
+ if (loading) {
+ return <>>;
+ }
+
+ if (!identity) {
+ return ;
+ }
+
return (
diff --git a/src/entries/Popup/auth.html b/src/entries/Popup/auth.html
new file mode 100644
index 00000000..36421da5
--- /dev/null
+++ b/src/entries/Popup/auth.html
@@ -0,0 +1,10 @@
+
+
+
+
+
Eternis Passkey Authentication
+
+
+
+
+
\ No newline at end of file
diff --git a/src/entries/Popup/auth.ts b/src/entries/Popup/auth.ts
new file mode 100644
index 00000000..a6bcbc17
--- /dev/null
+++ b/src/entries/Popup/auth.ts
@@ -0,0 +1,108 @@
+import { BackgroundActiontype, AuthActiontype } from '../Background/rpc';
+import { arrayBufferToBase64, base64ToArrayBuffer } from '../../utils/misc';
+
+const webAuthnAuthenticate = async () => {
+ try {
+ const randomChallenge = new Uint8Array(16);
+ const assertion = await navigator.credentials.get({
+ publicKey: {
+ challenge: randomChallenge,
+ rpId: 'eternis.ai',
+ userVerification: 'required',
+ timeout: 60000,
+ },
+ });
+
+ if (!assertion) {
+ return;
+ }
+
+ // @ts-expect-error assertion type needs to be defined globally
+ const userId = new Uint8Array(assertion.response.userHandle);
+ const userIdBase64 = arrayBufferToBase64(userId);
+ chrome.runtime.sendMessage({
+ type: BackgroundActiontype.close_auth_popup,
+ data: {
+ userId: userIdBase64,
+ },
+ });
+ } catch (error) {
+ console.error('error', error);
+ chrome.runtime.sendMessage({
+ type: BackgroundActiontype.close_auth_popup,
+ data: {
+ userId: null,
+ },
+ });
+ }
+};
+
+const webAuthnRegister = async (username: string, userIdBase64: string) => {
+ try {
+ const userId = base64ToArrayBuffer(userIdBase64);
+
+ const publicKeyCredentialCreationOptions = {
+ challenge: new Uint8Array(16), // not required as we are not going to verify the challenge
+ rp: {
+ name: 'Pangea Attestor',
+ id: 'eternis.ai',
+ transports: ['internal'],
+ },
+ user: {
+ id: userId,
+ name: username,
+ displayName: username,
+ },
+ pubKeyCredParams: [
+ { alg: -7, type: 'public-key' },
+ { alg: -257, type: 'public-key' },
+ ],
+ authenticatorSelection: {
+ residentKey: 'preferred',
+ requireResidentKey: false,
+ authenticatorAttachment: 'platform',
+ userVerification: 'required',
+ },
+ timeout: 60000,
+ attestation: 'none',
+ hints: [],
+ extensions: {
+ credProps: true,
+ },
+ };
+
+ const credential = await navigator.credentials.create({
+ // @ts-expect-error type not supported
+ publicKey: publicKeyCredentialCreationOptions,
+ });
+
+ if (!credential) {
+ return;
+ }
+
+ chrome.runtime.sendMessage({
+ type: BackgroundActiontype.close_auth_popup,
+ data: {
+ userId: userIdBase64,
+ },
+ });
+ } catch (error) {
+ console.error('error', error);
+ chrome.runtime.sendMessage({
+ type: BackgroundActiontype.close_auth_popup,
+ data: {
+ userId: null,
+ },
+ });
+ }
+};
+
+// start listening for messages from background
+chrome.runtime.onMessage.addListener((message) => {
+ if (message.type === AuthActiontype.web_authn_authenticate) {
+ webAuthnAuthenticate();
+ } else if (message.type === AuthActiontype.web_authn_register) {
+ const { username, userId } = message.data;
+ webAuthnRegister(username, userId);
+ }
+});
diff --git a/src/manifest.json b/src/manifest.json
index 3bc1998f..1f31a065 100755
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -25,7 +25,7 @@
],
"web_accessible_resources": [
{
- "resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js"],
+ "resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js", "auth.html", "auth.bundle.js"],
"matches": ["http://*/*", "https://*/*", "
"]
}
],
diff --git a/src/pages/AttestationDetails/index.tsx b/src/pages/AttestationDetails/index.tsx
index 903b891d..45ecf32b 100644
--- a/src/pages/AttestationDetails/index.tsx
+++ b/src/pages/AttestationDetails/index.tsx
@@ -7,7 +7,7 @@ import { useIdentity } from '../../reducers/identity';
import { VERIFIER_APP_URL } from '../../utils/constants';
import { AttestationObject, decodeAppData, Attribute } from '@eternis/tlsn-js';
export default function AttestationDetails() {
- const [identity] = useIdentity();
+ const { identity } = useIdentity();
const params = useParams<{ host: string; requestId: string }>();
const request = useRequestHistory(params.requestId);
diff --git a/src/pages/Locked/Lock.tsx b/src/pages/Locked/Lock.tsx
new file mode 100644
index 00000000..38c15990
--- /dev/null
+++ b/src/pages/Locked/Lock.tsx
@@ -0,0 +1,111 @@
+import React, { useCallback, useState } from 'react';
+import logo from '../../assets/img/icon-128.png';
+import { BackgroundActiontype } from '../../entries/Background/rpc';
+import { ArrowRightCircleIcon, XIcon } from 'lucide-react';
+import { Identity } from '@semaphore-protocol/identity';
+import { useDispatch } from 'react-redux';
+import { setIdentity } from '../../reducers/identity';
+
+export default function Lock() {
+ const dispatch = useDispatch();
+ const [identitySecret, setIdentitySecret] = useState('');
+
+ const handleUseExistingAccount = useCallback(async () => {
+ try {
+ await chrome.runtime.sendMessage({
+ type: BackgroundActiontype.request_unlock_extension,
+ data: {
+ left: window.screenX + 24,
+ top: window.screenY,
+ width: window.innerWidth - 48,
+ },
+ });
+ } catch (error) {
+ console.error(error);
+ }
+ }, []);
+
+ const handleUnlockAccount = useCallback(async () => {
+ if (identitySecret.length !== 44) {
+ return;
+ }
+
+ try {
+ const storage = await chrome.storage.sync.get('identityPublicKey');
+ const identityPublicKey = storage?.identityPublicKey;
+ if (!identityPublicKey) {
+ return;
+ }
+
+ const identity = new Identity(identitySecret);
+ const testPublicKey = [
+ identity.publicKey[0].toString(),
+ identity.publicKey[1].toString(),
+ ];
+ if (JSON.stringify(testPublicKey) !== identityPublicKey) {
+ throw new Error('Invalid identity secret');
+ return;
+ }
+
+ dispatch(await setIdentity(identitySecret));
+ } catch (error) {
+ console.error(error);
+ setIdentitySecret('');
+ }
+ }, [identitySecret, dispatch, setIdentity]);
+
+ return (
+
+
+
+ Welcome back to Pangea Attestor
+
+
+
Unlock your account
+
+
+
+ Login with Passkey
+
+
+
+
+
+
+
setIdentitySecret(e.target.value)}
+ placeholder="Enter your identity secret"
+ autoFocus
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ handleUnlockAccount();
+ }
+ }}
+ />
+ {identitySecret.length === 44 ? (
+
+ ) : identitySecret.length > 0 ? (
+
{
+ setIdentitySecret('');
+ }}
+ className="w-4 h-4 absolute right-3 top-1/2 transform -translate-y-1/2 cursor-pointer"
+ />
+ ) : null}
+
+
+ );
+}
diff --git a/src/pages/Locked/NewIdentity.tsx b/src/pages/Locked/NewIdentity.tsx
new file mode 100644
index 00000000..93fbc8cc
--- /dev/null
+++ b/src/pages/Locked/NewIdentity.tsx
@@ -0,0 +1,214 @@
+import React, { useState, useCallback, useEffect } from 'react';
+import { BackgroundActiontype } from '../../entries/Background/rpc';
+import { useDispatch } from 'react-redux';
+import {
+ ArrowLeft,
+ CheckIcon,
+ CopyIcon,
+ EyeIcon,
+ EyeOffIcon,
+} from 'lucide-react';
+import {
+ arrayBufferToBase64,
+ generateRandomArrayBuffer,
+} from '../../utils/misc';
+import { setIdentity } from '../../reducers/identity';
+
+export default function NewIdentity({
+ setCreateNewIdentity,
+}: {
+ setCreateNewIdentity: (value: boolean) => void;
+}) {
+ const dispatch = useDispatch();
+ const [username, setUsername] = useState('');
+ const [identitySecret, setIdentitySecret] = useState('');
+ const [isPasskeySupported, setIsPasskeySupported] = useState(false);
+ const [userConsent, setUserConsent] = useState(false);
+
+ const [showIdentitySecret, setShowIdentitySecret] = useState(false);
+ const [isCopied, setIsCopied] = useState(false);
+
+ const allowPasskey = username.length > 0 && isPasskeySupported === true;
+ const allowAccountCreate = username.length > 0 && userConsent === true;
+
+ useEffect(() => {
+ const userAgent = navigator.userAgent.toLowerCase();
+ if (userAgent.indexOf('mac') !== -1) {
+ setIsPasskeySupported(true);
+ }
+ if (userAgent.indexOf('win') !== -1) {
+ setIsPasskeySupported(true);
+ }
+ }, []);
+
+ useEffect(() => {
+ const identitySecret = generateRandomArrayBuffer(32);
+ setIdentitySecret(arrayBufferToBase64(identitySecret));
+ }, []);
+
+ const handleCreateIdentity = useCallback(async () => {
+ if (!allowPasskey) {
+ return;
+ }
+
+ try {
+ if (!username) {
+ return;
+ }
+
+ const userId = identitySecret;
+
+ await chrome.runtime.sendMessage({
+ type: BackgroundActiontype.request_create_identity,
+ data: {
+ left: window.screenX + 24,
+ top: window.screenY,
+ width: window.innerWidth - 48,
+ username,
+ userId,
+ },
+ });
+ } catch (error) {
+ console.log('error', error);
+ await chrome.windows.create({
+ url: 'popup.html',
+ type: 'popup',
+ width: 1,
+ height: 1,
+ });
+ }
+ }, [username, dispatch, identitySecret, allowPasskey]);
+
+ const handleCreateAccount = useCallback(async () => {
+ if (!allowAccountCreate) {
+ return;
+ }
+
+ const userId = identitySecret;
+
+ try {
+ dispatch(await setIdentity(userId));
+ } catch (error) {
+ console.log('error', error);
+ }
+ }, [allowAccountCreate, identitySecret, dispatch, setIdentity]);
+
+ const handleCopyIdentitySecret = useCallback(() => {
+ navigator.clipboard.writeText(identitySecret);
+ setIsCopied(true);
+ setTimeout(() => {
+ setIsCopied(false);
+ }, 3000);
+ }, [identitySecret]);
+
+ return (
+ <>
+
+
setCreateNewIdentity(false)}
+ className="cursor-pointer opacity-50 hover:opacity-100 h-4 w-4 ml-5"
+ />
+
+
+ {/*
+
Welcome to Pangea Attestor
*/}
+
+
+
Username (Required)
+
setUsername(e.target.value)}
+ />
+
+
+
+
handleCreateIdentity()}
+ className={`
+ bg-[#092EEA] text-white text-sm py-5 w-full rounded-lg text-center font-medium cursor-pointer hover:bg-[#092EEA]/80
+ ${!allowPasskey ? 'opacity-50 cursor-not-allowed hover:bg-[#092EEA]' : ''}
+ `}
+ >
+ Secure with Passkey
+
+ {isPasskeySupported
+ ? '(Recommended)'
+ : isPasskeySupported === false
+ ? 'Not Supported'
+ : ''}
+
+
+
+
+
+
+
+
+ Save Your Identity Secret
+
+ {isCopied ? (
+
+ ) : (
+
handleCopyIdentitySecret()}
+ className="cursor-pointer opacity-50 hover:opacity-100 w-4 h-4"
+ />
+ )}
+ {showIdentitySecret ? (
+ setShowIdentitySecret(!showIdentitySecret)}
+ className="cursor-pointer opacity-50 hover:opacity-100 w-4 h-4"
+ />
+ ) : (
+ setShowIdentitySecret(!showIdentitySecret)}
+ className="cursor-pointer opacity-50 hover:opacity-100 w-4 h-4"
+ />
+ )}
+
+
+
+
+
+
setUserConsent(e.target.checked)}
+ />
+
+
Identity Secret is the only way to generate your account.
+
It needs to be kept securely and not shared with anyone.
+
+ Your account cannot be recovered if you lose your Identity Secret.
+
+
+
+
+
+
handleCreateAccount()}
+ className={`
+ bg-[#FFFFFF] text-[#092EEA] border border-[#E4E6EA] text-sm py-5 w-full rounded-lg text-center font-medium cursor-pointer hover:bg-gray-100
+ ${!allowAccountCreate ? 'opacity-50 cursor-not-allowed hover:bg-[#FFFFFF]' : ''}
+ `}
+ >
+ Create Account
+
+
+
+ >
+ );
+}
diff --git a/src/pages/Locked/Setup.tsx b/src/pages/Locked/Setup.tsx
new file mode 100644
index 00000000..227c36c3
--- /dev/null
+++ b/src/pages/Locked/Setup.tsx
@@ -0,0 +1,117 @@
+import React, { useCallback, useState } from 'react';
+import logo from '../../assets/img/icon-128.png';
+import { BackgroundActiontype } from '../../entries/Background/rpc';
+import { ArrowRightCircleIcon, XIcon } from 'lucide-react';
+import { useDispatch } from 'react-redux';
+import { setIdentity } from '../../reducers/identity';
+
+export default function Setup({
+ setCreateNewIdentity,
+}: {
+ setCreateNewIdentity: (value: boolean) => void;
+}) {
+ const dispatch = useDispatch();
+ const [showIdentityInput, setShowIdentityInput] = useState(false);
+ const [identitySecret, setIdentitySecret] = useState('');
+ const handleUseExistingAccount = useCallback(async () => {
+ try {
+ await chrome.runtime.sendMessage({
+ type: BackgroundActiontype.request_unlock_extension,
+ data: {
+ left: window.screenX + 24,
+ top: window.screenY,
+ width: window.innerWidth - 48,
+ },
+ });
+ } catch (error) {
+ console.log('error', error);
+ }
+ }, []);
+
+ const handleCreateAccount = useCallback(async () => {
+ if (identitySecret.length !== 44) {
+ return;
+ }
+
+ const userId = identitySecret;
+
+ try {
+ dispatch(await setIdentity(userId));
+ } catch (error) {
+ console.log('error', error);
+ }
+ }, [identitySecret, dispatch, setIdentity]);
+
+ return (
+
+
+
Welcome to Pangea Attestor
+
+
+
setCreateNewIdentity(true)}
+ className="bg-[#092EEA] text-white text-sm py-5 w-full rounded-lg text-center font-medium cursor-pointer hover:bg-[#092EEA]/80"
+ >
+ Get Started
+
+
+
+
+
+
+
+ I already have an account
+
+
+ With Passkey
+
+
+ {showIdentityInput ? (
+
+
setIdentitySecret(e.target.value)}
+ placeholder="Enter your identity secret"
+ autoFocus
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ handleCreateAccount();
+ }
+ }}
+ />
+ {identitySecret.length === 44 ? (
+
+ ) : (
+
{
+ setIdentitySecret('');
+ setShowIdentityInput(false);
+ }}
+ className="w-4 h-4 absolute right-3 top-1/2 transform -translate-y-1/2 cursor-pointer"
+ />
+ )}
+
+ ) : (
+
setShowIdentityInput(true)}
+ className="bg-[#FFFFFF] text-[#092EEA] text-sm py-2 w-full text-center font-medium cursor-pointer hover:bg-gray-100 rounded-md"
+ >
+ With Identity Secret
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/Locked/index.tsx b/src/pages/Locked/index.tsx
new file mode 100644
index 00000000..2e55c28d
--- /dev/null
+++ b/src/pages/Locked/index.tsx
@@ -0,0 +1,23 @@
+import { useIdentity } from '../../reducers/identity';
+import React, { useEffect, useState } from 'react';
+import Setup from './Setup';
+import Lock from './Lock';
+import NewIdentity from './NewIdentity';
+
+export default function Locked() {
+ const { isSetupCompleted } = useIdentity();
+ const [createNewIdentity, setCreateNewIdentity] = useState(false);
+
+ useEffect(() => {
+ console.log('isSetupCompleted', isSetupCompleted);
+ }, [isSetupCompleted]);
+
+ if (!isSetupCompleted) {
+ if (createNewIdentity) {
+ return ;
+ }
+ return ;
+ }
+
+ return ;
+}
diff --git a/src/pages/Options/index.tsx b/src/pages/Options/index.tsx
index 780a0825..1ad41fdc 100644
--- a/src/pages/Options/index.tsx
+++ b/src/pages/Options/index.tsx
@@ -64,7 +64,7 @@ export default function Options(): ReactElement {
const requests = useUniqueRequests();
const [devMode, setDevMode] = useDevMode();
- const [identity, setIdentity] = useIdentity();
+ const { identity } = useIdentity();
useEffect(() => {
(async () => {
diff --git a/src/reducers/identity.ts b/src/reducers/identity.ts
index 109a3ccd..d5a4cc14 100644
--- a/src/reducers/identity.ts
+++ b/src/reducers/identity.ts
@@ -1,58 +1,123 @@
import { Identity } from '@semaphore-protocol/identity';
-import { sha256 } from '../utils/misc';
-import { Dispatch, SetStateAction, useEffect, useState } from 'react';
-
-export class IdentityManager {
- async getIdentity(): Promise {
- const identityStorageId = await sha256('identity');
- try {
- const storage = await chrome.storage.sync.get(identityStorageId);
- const identity = storage[identityStorageId];
- if (!identity) {
- return this._createIdentity();
- }
- return new Identity(identity);
- } catch (e) {
- return this._createIdentity();
- }
- }
+import { useSelector } from 'react-redux';
+import { AppRootState } from './index';
+import deepEqual from 'fast-deep-equal';
+import { BackgroundActiontype } from '../entries/Background/rpc';
+
+enum ActionType {
+ '/identity/setIdentity' = '/identity/setIdentity',
+ '/identity/setLoading' = '/identity/setLoading',
+}
+
+type Action = {
+ type: ActionType;
+ payload?: payload;
+ error?: boolean;
+ meta?: any;
+};
+
+type State = {
+ loading: boolean;
+ identity: Identity | null;
+ isSetupCompleted: boolean;
+};
- async _saveIdentity(identity: Identity): Promise {
- const identityStorageId = await sha256('identity');
- try {
- await chrome.storage.sync.set({
- [identityStorageId]: identity.privateKey.toString(), // Only PRIVATE KEY is enough to reconstruct the identity
- });
- } catch (e) {
- console.error('Error saving identity', e);
- }
+const initState: State = {
+ loading: true,
+ identity: null,
+ isSetupCompleted: false,
+};
+
+export const setIdentity = async (
+ userId: string | null,
+): Promise<
+ Action<{ identity: Identity | null; isSetupCompleted?: boolean }>
+> => {
+ if (!userId) {
+ return {
+ type: ActionType['/identity/setIdentity'],
+ payload: {
+ identity: null,
+ },
+ };
}
- async _createIdentity(): Promise {
- console.log('creating identity');
- const identity = new Identity();
- await this._saveIdentity(identity);
- return identity;
+ const identity = new Identity(userId);
+ const publicKey = [
+ identity.publicKey[0].toString(),
+ identity.publicKey[1].toString(),
+ ];
+
+ const storage = await chrome.storage.sync.get('isSetupCompleted');
+ const isSetupCompleted = storage?.isSetupCompleted ?? false;
+ if (!isSetupCompleted) {
+ await chrome.storage.sync.set({
+ isSetupCompleted: true,
+ identityPublicKey: JSON.stringify(publicKey),
+ });
}
- async loadIdentity(privateKey: string): Promise {
- const identity = new Identity(privateKey);
- await this._saveIdentity(identity);
- return identity;
+ // send message to background to update identity
+ await chrome.runtime.sendMessage({
+ type: BackgroundActiontype.identity_updated,
+ data: {
+ identitySecret: userId,
+ },
+ });
+ return {
+ type: ActionType['/identity/setIdentity'],
+ payload: {
+ identity,
+ isSetupCompleted: true,
+ },
+ };
+};
+
+export const initIdentity = async () => {
+ // await chrome.storage.sync.remove('isSetupCompleted');
+ const storage = await chrome.storage.sync.get('isSetupCompleted');
+ console.log('storage', storage);
+ const isSetupCompleted = storage?.isSetupCompleted ?? false;
+ return {
+ type: ActionType['/identity/setLoading'],
+ payload: {
+ loading: false,
+ isSetupCompleted,
+ },
+ };
+};
+
+export default function identity(
+ state = initState,
+ action: Action,
+): State {
+ switch (action.type) {
+ case ActionType['/identity/setIdentity']:
+ return {
+ ...state,
+ ...action.payload,
+ };
+
+ case ActionType['/identity/setLoading']:
+ return {
+ ...state,
+ ...action.payload,
+ };
+ default:
+ return state;
}
}
-export const useIdentity = (): [
- Identity | null,
- Dispatch>,
-] => {
- const [identity, setIdentity] = useState(null);
- useEffect(() => {
- (async () => {
- const identityManager = new IdentityManager();
- const identity = await identityManager.getIdentity();
- setIdentity(identity);
- })();
- }, []);
- return [identity, setIdentity];
+export const useIdentity = (): {
+ loading: boolean;
+ identity: Identity | null;
+ isSetupCompleted: boolean;
+} => {
+ return useSelector((state: AppRootState) => {
+ return {
+ loading: state.identity.loading,
+ identity: state.identity.identity ?? null,
+ isSetupCompleted: state.identity.isSetupCompleted ?? false,
+ };
+ }, deepEqual);
};
diff --git a/src/reducers/index.tsx b/src/reducers/index.tsx
index 42b15667..ddd8d656 100644
--- a/src/reducers/index.tsx
+++ b/src/reducers/index.tsx
@@ -2,11 +2,13 @@ import { combineReducers } from 'redux';
import requests from './requests';
import history from './history';
import plugins from './plugins';
+import identity from './identity';
const rootReducer = combineReducers({
requests,
history,
plugins,
+ identity,
});
export type AppRootState = ReturnType;
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
index cb4e4e8c..149e4759 100644
--- a/src/utils/misc.ts
+++ b/src/utils/misc.ts
@@ -468,3 +468,24 @@ export function urlToRegex(url: string): string {
return regexPattern;
}
+
+export const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
+ const binary = String.fromCharCode(...new Uint8Array(buffer));
+ return window.btoa(binary);
+};
+
+export const base64ToArrayBuffer = (base64: string) => {
+ const binary = window.atob(base64);
+ const buffer = new ArrayBuffer(binary.length);
+ const bytes = new Uint8Array(buffer);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ return buffer;
+};
+
+export const generateRandomArrayBuffer = (size: number) => {
+ const array = new Uint8Array(size);
+ window.crypto.getRandomValues(array);
+ return array;
+};
diff --git a/webpack.config.js b/webpack.config.js
index 128072a9..2795c22c 100755
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -50,6 +50,7 @@ var options = {
contentScript: path.join(__dirname, "src", "entries", "Content", "index.ts"),
content: path.join(__dirname, "src", "entries", "Content", "content.ts"),
offscreen: path.join(__dirname, "src", "entries", "Offscreen", "index.tsx"),
+ auth: path.join(__dirname, "src", "entries", "Popup", "auth.ts"),
},
// chromeExtensionBoilerplate: {
// notHotReload: ["background", "contentScript", "devtools"],
@@ -238,6 +239,12 @@ var options = {
chunks: ["popup"],
cache: false,
}),
+ new HtmlWebpackPlugin({
+ template: path.join(__dirname, "src", "entries", "Popup", "auth.html"),
+ filename: "auth.html",
+ chunks: ["auth"],
+ cache: false,
+ }),
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "entries", "Offscreen", "index.html"),
filename: "offscreen.html",