From 19b665c07219423cbd481ff169dbf62c1d37cd9f Mon Sep 17 00:00:00 2001 From: tsukino <87639218+0xtsukino@users.noreply.github.com> Date: Tue, 31 Oct 2023 07:58:09 -0700 Subject: [PATCH] add json parameter to wasm prover and verifier (#20) * wip * wip; git st * add verifier * wip * finish generic request notarizer --- .eslintrc | 2 + package.json | 2 + pnpm-lock.yaml | 95 +++++++ src/components/History/index.tsx | 131 ++++++++++ src/components/Notarize/index.tsx | 15 ++ src/components/Options/index.tsx | 29 +-- src/components/ProofViewer/index.tsx | 84 +++++++ src/components/RequestDetail/index.tsx | 71 ++++-- src/components/ResponseDetail/index.tsx | 54 ++-- src/pages/Background/actionTypes.ts | 27 +- src/pages/Background/index.ts | 314 ++++++++++++++++++++++-- src/pages/Home/index.tsx | 39 +-- src/pages/Offscreen/Offscreen.tsx | 92 +++++-- src/pages/Offscreen/worker.ts | 105 ++++---- src/pages/Popup/Popup.tsx | 11 + src/pages/Popup/index.tsx | 5 +- src/pages/RequestBuilder/index.tsx | 39 ++- src/pages/Requests/Request.tsx | 3 +- src/reducers/history.tsx | 99 ++++++++ src/reducers/index.tsx | 2 + src/reducers/requests.ts | 41 +++- src/utils/misc.ts | 19 ++ src/utils/storage.ts | 14 ++ utils/bookmark/bookmarks.json | 7 - wasm/prover/Cargo.toml | 5 + wasm/prover/src/lib.rs | 209 ++++++++++++---- wasm/prover/src/requestOpt.rs | 29 +++ 27 files changed, 1286 insertions(+), 257 deletions(-) create mode 100644 src/components/History/index.tsx create mode 100644 src/components/Notarize/index.tsx create mode 100644 src/components/ProofViewer/index.tsx create mode 100644 src/reducers/history.tsx create mode 100644 src/utils/storage.ts create mode 100644 wasm/prover/src/requestOpt.rs diff --git a/.eslintrc b/.eslintrc index a1b3e2e0..8161f363 100644 --- a/.eslintrc +++ b/.eslintrc @@ -24,6 +24,8 @@ "import/resolver": "typescript" }, "ignorePatterns": [ + "node_modules", + "zip", "build", "wasm", "tlsn", diff --git a/package.json b/package.json index bd187b70..603015c1 100755 --- a/package.json +++ b/package.json @@ -18,10 +18,12 @@ "@fortawesome/fontawesome-free": "^6.4.2", "async-mutex": "^0.4.0", "buffer": "^6.0.3", + "charwise": "^3.0.1", "classnames": "^2.3.2", "comlink": "^4.4.1", "fast-deep-equal": "^3.1.3", "fuse.js": "^6.6.2", + "level": "^8.0.0", "node-cache": "^5.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1162326..c8aca5fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: buffer: specifier: ^6.0.3 version: 6.0.3 + charwise: + specifier: ^3.0.1 + version: 3.0.1 classnames: specifier: ^2.3.2 version: 2.3.2 @@ -26,6 +29,9 @@ dependencies: fuse.js: specifier: ^6.6.2 version: 6.6.2 + level: + specifier: ^8.0.0 + version: 8.0.0 node-cache: specifier: ^5.1.2 version: 5.1.2 @@ -2690,6 +2696,19 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: true + /abstract-level@1.0.3: + resolution: {integrity: sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA==} + engines: {node: '>=12'} + dependencies: + buffer: 6.0.3 + catering: 2.1.1 + is-buffer: 2.0.5 + level-supports: 4.0.1 + level-transcoder: 1.0.1 + module-error: 1.0.2 + queue-microtask: 1.2.3 + dev: false + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -3134,6 +3153,15 @@ packages: dependencies: fill-range: 7.0.1 + /browser-level@1.0.1: + resolution: {integrity: sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ==} + dependencies: + abstract-level: 1.0.3 + catering: 2.1.1 + module-error: 1.0.2 + run-parallel-limit: 1.1.0 + dev: false + /browserslist@4.21.10: resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -3205,6 +3233,11 @@ packages: resolution: {integrity: sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==} dev: true + /catering@2.1.1: + resolution: {integrity: sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==} + engines: {node: '>=6'} + dev: false + /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3222,6 +3255,10 @@ packages: supports-color: 7.2.0 dev: true + /charwise@3.0.1: + resolution: {integrity: sha512-RcdumNsM6fJZ5HHbYunqj2bpurVRGsXour3OR+SlLEHFhG6ALm54i6Osnh+OvO7kEoSBzwExpblYFH8zKQiEPw==} + dev: false + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -3241,6 +3278,18 @@ packages: engines: {node: '>=6.0'} dev: true + /classic-level@1.3.0: + resolution: {integrity: sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg==} + engines: {node: '>=12'} + requiresBuild: true + dependencies: + abstract-level: 1.0.3 + catering: 2.1.1 + module-error: 1.0.2 + napi-macros: 2.2.2 + node-gyp-build: 4.6.1 + dev: false + /classnames@2.3.2: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} dev: false @@ -4988,6 +5037,11 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + dev: false + /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -5265,6 +5319,27 @@ packages: shell-quote: 1.8.1 dev: true + /level-supports@4.0.1: + resolution: {integrity: sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==} + engines: {node: '>=12'} + dev: false + + /level-transcoder@1.0.1: + resolution: {integrity: sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==} + engines: {node: '>=12'} + dependencies: + buffer: 6.0.3 + module-error: 1.0.2 + dev: false + + /level@8.0.0: + resolution: {integrity: sha512-ypf0jjAk2BWI33yzEaaotpq7fkOPALKAgDBxggO6Q9HGX2MRXn0wbP1Jn/tJv1gtL867+YOjOB49WaUF3UoJNQ==} + engines: {node: '>=12'} + dependencies: + browser-level: 1.0.1 + classic-level: 1.3.0 + dev: false + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -5430,6 +5505,11 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true + /module-error@1.0.2: + resolution: {integrity: sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==} + engines: {node: '>=10'} + dev: false + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true @@ -5463,6 +5543,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /napi-macros@2.2.2: + resolution: {integrity: sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==} + dev: false + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -5499,6 +5583,11 @@ packages: engines: {node: '>= 6.13.0'} dev: true + /node-gyp-build@4.6.1: + resolution: {integrity: sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==} + hasBin: true + dev: false + /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: true @@ -6683,6 +6772,12 @@ packages: execa: 5.1.1 dev: true + /run-parallel-limit@1.1.0: + resolution: {integrity: sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw==} + dependencies: + queue-microtask: 1.2.3 + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: diff --git a/src/components/History/index.tsx b/src/components/History/index.tsx new file mode 100644 index 00000000..f9ef9001 --- /dev/null +++ b/src/components/History/index.tsx @@ -0,0 +1,131 @@ +import React, { ReactElement, useState, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router'; +import { + useHistoryOrder, + useRequestHistory, + deleteRequestHistory, +} from '../../reducers/history'; +import Icon from '../../components/Icon'; +import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage'; +import { urlify, download } from '../../utils/misc'; +import { BackgroundActiontype } from '../../pages/Background/actionTypes'; + +export default function History(): ReactElement { + const history = useHistoryOrder(); + + + return ( +
+ {history.map((id) => { + return ; + })} +
+ ); +} + +function OneRequestHistory(props: { requestId: string }): ReactElement { + const dispatch = useDispatch(); + const request = useRequestHistory(props.requestId); + const navigate = useNavigate(); + const { proof, status } = request || {}; + const requestUrl = urlify(request.url); + + const onRetry = useCallback(async () => { + const notaryUrl = await get(NOTARY_API_LS_KEY); + const websocketProxyUrl = await get(PROXY_API_LS_KEY); + chrome.runtime.sendMessage({ + type: BackgroundActiontype.retry_prove_request, + data: { + id: props.requestId, + notaryUrl, + websocketProxyUrl, + }, + }); + }, [props.requestId]); + + const onView = useCallback(() => { + chrome.runtime.sendMessage({ + type: BackgroundActiontype.verify_prove_request, + data: request, + }); + navigate('/verify/' + request.id); + }, [request]); + + const onDelete = useCallback(async () => { + dispatch(deleteRequestHistory(props.requestId)); + }, [props.requestId]); + + return ( +
+
+
+
+ {request?.method} +
+
+ {requestUrl?.pathname} +
+
+
+
Host:
+
+ {requestUrl?.host} +
+
+
+
Notary API:
+
{request?.notaryUrl}
+
+
+
TLS Proxy API:
+
+ {request?.websocketProxyUrl} +
+
+
+
+ {status === 'success' && ( + <> +
+ + View Proof +
+
download(`${request?.id}.json`, JSON.stringify(request.proof))} + > + + Download +
+ + )} + {(!status || status === 'error') && ( +
+ + Retry +
+ )} + {status === 'pending' && ( +
+ + Pending +
+ )} +
+ + Delete +
+
+
+ ); +} diff --git a/src/components/Notarize/index.tsx b/src/components/Notarize/index.tsx new file mode 100644 index 00000000..2c59f679 --- /dev/null +++ b/src/components/Notarize/index.tsx @@ -0,0 +1,15 @@ +import React, { ReactElement } from 'react'; +import { useParams } from 'react-router'; +import { useRequestHistory } from '../../reducers/history'; +import RequestBuilder from '../../pages/RequestBuilder'; + +export default function Notarize(): ReactElement { + const params = useParams<{ requestId: string }>(); + const request = useRequestHistory(params.requestId); + + return ( +
+ {request?.id} +
+ ); +} diff --git a/src/components/Options/index.tsx b/src/components/Options/index.tsx index 2ca495bb..86e32a4c 100644 --- a/src/components/Options/index.tsx +++ b/src/components/Options/index.tsx @@ -1,7 +1,10 @@ import React, { ReactElement, useState, useEffect, useCallback } from 'react'; - -const NOTARY_API_LS_KEY = 'notary-api'; -const PROXY_API_LS_KEY = 'proxy-api'; +import { + set, + get, + NOTARY_API_LS_KEY, + PROXY_API_LS_KEY, +} from '../../utils/storage'; export default function Options(): ReactElement { const [notary, setNotary] = useState('http://localhost:7047'); @@ -24,7 +27,7 @@ export default function Options(): ReactElement { return (
- API Settings + Settings
Notary API
@@ -32,7 +35,7 @@ export default function Options(): ReactElement { type="text" className="input border" placeholder="http://localhost:7047" - onChange={e => { + onChange={(e) => { setNotary(e.target.value); setDirty(true); }} @@ -45,7 +48,7 @@ export default function Options(): ReactElement { type="text" className="input border" placeholder="ws://127.0.0.1:55688" - onChange={e => { + onChange={(e) => { setProxy(e.target.value); setDirty(true); }} @@ -53,7 +56,7 @@ export default function Options(): ReactElement { />
-
); -}; - -async function set(key: string, value: string) { - return chrome.storage.sync - .set({ [key]: value }); -} - -async function get(key: string) { - return chrome.storage.sync - .get(key) - .then((json: any) => json[key]) - .catch(() => ''); } diff --git a/src/components/ProofViewer/index.tsx b/src/components/ProofViewer/index.tsx new file mode 100644 index 00000000..e0614bb1 --- /dev/null +++ b/src/components/ProofViewer/index.tsx @@ -0,0 +1,84 @@ +import React, { ReactNode, ReactElement, useState } from 'react'; +import { useParams, useLocation, useNavigate } from 'react-router'; +import c from 'classnames'; +import { useRequestHistory } from '../../reducers/history'; +import RequestBuilder from '../../pages/RequestBuilder'; +import Icon from '../../components/Icon'; +import { download } from '../../utils/misc'; + +export default function ProofViewer(): ReactElement { + const {requestId} = useParams<{ requestId: string }>(); + const request = useRequestHistory(requestId); + const navigate = useNavigate(); + const [ tab, setTab ] = useState('sent'); + const loc = useLocation(); + + return ( +
+
+
+ navigate(-1)} + fa="fa-solid fa-xmark" + /> + setTab('sent')} + active={tab === 'sent'} + > + Sent + + setTab('recv')} + active={tab === 'recv'} + > + Recv + +
+ +
+
+
+
+ {tab === 'sent' && ( + + )} + {tab === 'recv' && ( + + )} +
+
+ ); +}; + +function TabLabel(props: { + children: ReactNode; + onClick: MouseEventHandler; + active?: boolean; +}): ReactElement { + return ( + + ); +} diff --git a/src/components/RequestDetail/index.tsx b/src/components/RequestDetail/index.tsx index df22b5ad..d4eee0c1 100644 --- a/src/components/RequestDetail/index.tsx +++ b/src/components/RequestDetail/index.tsx @@ -9,7 +9,12 @@ import { BackgroundActiontype, RequestLog, } from '../../pages/Background/actionTypes'; +import { + notarizeRequest, + useRequest, +} from '../../reducers/requests'; import classNames from 'classnames'; +import { useDispatch } from 'react-redux'; import { Navigate, Route, @@ -20,16 +25,57 @@ import { } from 'react-router'; import Icon from '../Icon'; import NavigateWithParams from '../NavigateWithParams'; +import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage'; +import { urlify } from '../../utils/misc'; type Props = { - data: RequestLog | null; + requestId: string; }; +const maxTranscriptSize = 16384; + +const authToken = 'a28cae3969369c26c1410f5bded83c3f4f914fbc'; +const accessToken = + 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'; +const csrfToken = + 'b73b3488687683372af2ea77486a444ccaa5327bbabad709df1b5161a6b83c8d7ec19106a82cb8dd5f8569632ee95ab4c6dc2abf5ad2ed7fa11b8340fcbe86a8fc00df28db6c4109a807f7cb12dd19da'; +const userAgent = + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'; + + export default function RequestDetail(props: Props): ReactElement { + const request = useRequest(props.requestId); const navigate = useNavigate(); - const { data } = props; + const dispatch = useDispatch(); + + const notarize = useCallback(async () => { + const notaryUrl = await get(NOTARY_API_LS_KEY); + const websocketProxyUrl = await get(PROXY_API_LS_KEY); + const headers = request + .requestHeaders.reduce((acc, h) => { + if (!(/^(origin|referer|Accept-Language|Accept-EncodingAccept)$|^(sec-|x-twitter-)/i.test(h.name))) { + acc[h.name] = h.value; + } + return acc; + }, { Host: urlify(request.url)?.hostname }); + + //TODO: for some reason, these needs to be override for twitter to work + headers['Accept-Encoding'] = 'identity'; + headers['Connection'] = 'close'; + + dispatch(notarizeRequest({ + url: request.url, + method: request.method, + headers, + body: request.body, + maxTranscriptSize, + notaryUrl, + websocketProxyUrl, + })) + navigate(`/history`); + }, [request]); - if (!data) return <>; + if (!request) return <>; return ( <> @@ -50,20 +96,15 @@ export default function RequestDetail(props: Props): ReactElement {
- } /> - } /> - } /> + } /> + } /> + } /> } /> @@ -92,7 +133,7 @@ function RequestDetailsHeaderTab(props: { } function RequestPayload(props: Props): ReactElement { - const { data } = props; + const data = useRequest(props.requestId); const [url, setUrl] = useState(); const [json, setJson] = useState(); const [formData, setFormData] = useState(null); @@ -216,7 +257,7 @@ function RequestPayload(props: Props): ReactElement { } function WebResponse(props: Props): ReactElement { - const { data } = props; + const data = useRequest(props.requestId); const [response, setResponse] = useState(null); const [json, setJSON] = useState(null); const [text, setText] = useState(null); @@ -387,7 +428,7 @@ function WebResponse(props: Props): ReactElement { } function RequestHeaders(props: Props): ReactElement { - const { data } = props; + const data = useRequest(props.requestId); return (
diff --git a/src/components/ResponseDetail/index.tsx b/src/components/ResponseDetail/index.tsx index b8c02d26..6e98d27a 100644 --- a/src/components/ResponseDetail/index.tsx +++ b/src/components/ResponseDetail/index.tsx @@ -66,33 +66,6 @@ export default function ResponseDetail(props: { )} > - {!!props.response?.headers && ( - <> - - - - - - - {Array.from(props.response.headers.entries()).map( - ([name, value]) => { - return ( - - - - - ); - }, - )} - - - )} {!!json && ( <> @@ -149,6 +122,33 @@ export default function ResponseDetail(props: { )} + {!!props.response?.headers && ( + <> + + + + + + + {Array.from(props.response.headers.entries()).map( + ([name, value]) => { + return ( + + + + + ); + }, + )} + + + )}
- Headers -
- {name} - - {value} -
+ Headers +
+ {name} + + {value} +
); diff --git a/src/pages/Background/actionTypes.ts b/src/pages/Background/actionTypes.ts index 86a16631..56913614 100644 --- a/src/pages/Background/actionTypes.ts +++ b/src/pages/Background/actionTypes.ts @@ -2,7 +2,13 @@ export enum BackgroundActiontype { get_requests = 'get_requests', clear_requests = 'clear_requests', push_action = 'push_action', - test_wasm = 'test_wasm', + get_prove_requests = 'get_prove_requests', + prove_request_start = 'prove_request_start', + process_prove_request = 'process_prove_request', + finish_prove_request = 'finish_prove_request', + verify_prove_request = 'verify_prove_request', + delete_prove_request = 'delete_prove_request', + retry_prove_request = 'retry_prove_request', } export type BackgroundAction = { @@ -26,3 +32,22 @@ export type RequestLog = { }; responseHeaders?: chrome.webRequest.HttpHeader[]; }; + +export type RequestHistory = { + id: string; + url: string; + method: string; + headers: { [key: string]: string }; + body: string; + maxTranscriptSize: string; + notaryUrl: string; + websocketProxyUrl: string; + status: '' | 'pending' | 'success' | 'error'; + error?: any; + proof?: { session: any; substrings: any }; + requestBody?: any; + verification?: { + sent: string; + recv: string; + }; +}; diff --git a/src/pages/Background/index.ts b/src/pages/Background/index.ts index ef1685aa..a6d497fa 100644 --- a/src/pages/Background/index.ts +++ b/src/pages/Background/index.ts @@ -1,7 +1,15 @@ -import { BackgroundActiontype, RequestLog } from './actionTypes'; +import { + BackgroundActiontype, + RequestLog, + type RequestHistory, +} from './actionTypes'; + import { Mutex } from 'async-mutex'; import NodeCache from 'node-cache'; import { addRequest } from '../../reducers/requests'; +import { addRequestHistory } from '../../reducers/history'; +import { Level } from 'level'; +import charwise from 'charwise'; let RequestsLogs: { [tabId: string]: NodeCache; @@ -13,11 +21,7 @@ const cache = new NodeCache({ maxKeys: 1000000, }); -(chrome as any).offscreen.createDocument({ - url: 'offscreen.html', - reasons: ['WORKERS'], - justification: 'workers for multithreading', -}); +let creatingOffscreen; chrome.tabs.onActivated.addListener((tabs) => { RequestsLogs[tabs.tabId] = @@ -33,12 +37,33 @@ chrome.tabs.onRemoved.addListener((tab) => { }); (async () => { + const offscreenUrl = chrome.runtime.getURL('offscreen.html'); + const existingContexts = await chrome.runtime.getContexts({ + contextTypes: ['OFFSCREEN_DOCUMENT'], + documentUrls: [offscreenUrl], + }); + + if (existingContexts.length > 0) { + return; + } + + if (creatingOffscreen) { + await creatingOffscreen; + } else { + creatingOffscreen = (chrome as any).offscreen.createDocument({ + url: 'offscreen.html', + reasons: ['WORKERS'], + justification: 'workers for multithreading', + }); + await creatingOffscreen; + creatingOffscreen = null; + } + chrome.webRequest.onSendHeaders.addListener( (details) => { mutex.runExclusive(async () => { const { method, tabId, requestId } = details; - // console.log('details', details); if (method !== 'OPTIONS') { RequestsLogs[tabId] = RequestsLogs[tabId] || @@ -63,7 +88,7 @@ chrome.tabs.onRemoved.addListener((tab) => { { urls: [''], }, - ['requestHeaders'], + ['requestHeaders', 'extraHeaders'], ); chrome.webRequest.onBeforeRequest.addListener( @@ -151,23 +176,266 @@ chrome.tabs.onRemoved.addListener((tab) => { { urls: [''], }, - ['responseHeaders'], + ['responseHeaders', 'extraHeaders'], ); - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - switch (request.type) { - case BackgroundActiontype.get_requests: { - const keys = RequestsLogs[request.data]?.keys() || []; - const data = keys.map((key) => RequestsLogs[request.data]?.get(key)); + chrome.runtime.onMessage.addListener( + async (request, sender, sendResponse) => { + switch (request.type) { + case BackgroundActiontype.get_requests: { + const keys = RequestsLogs[request.data]?.keys() || []; + const data = keys.map((key) => RequestsLogs[request.data]?.get(key)); + return sendResponse(data); + } + case BackgroundActiontype.clear_requests: { + RequestsLogs = {}; + return sendResponse(); + } + case BackgroundActiontype.get_prove_requests: { + getNotaryRequests().then((reqs) => { + for (const req of reqs) { + chrome.runtime.sendMessage({ + type: BackgroundActiontype.push_action, + data: { + tabId: 'background', + }, + action: addRequestHistory(req), + }); + } + }); + return sendResponse(); + } + case BackgroundActiontype.finish_prove_request: { + const { id, proof, error, verification } = request.data; - return sendResponse(data); - } - case BackgroundActiontype.clear_requests: { - RequestsLogs = {}; - return sendResponse(); + if (proof) { + const newReq = await addNotaryRequestProofs(id, proof); + if (!newReq) return; + + chrome.runtime.sendMessage({ + type: BackgroundActiontype.push_action, + data: { + tabId: 'background', + }, + action: addRequestHistory(await getNotaryRequest(id)), + }); + } + + if (error) { + const newReq = await setNotaryRequestError(id, error); + if (!newReq) return; + + chrome.runtime.sendMessage({ + type: BackgroundActiontype.push_action, + data: { + tabId: 'background', + }, + action: addRequestHistory(await getNotaryRequest(id)), + }); + } + + if (verification) { + const newReq = await setNotaryRequestVerification(id, verification); + if (!newReq) return; + + chrome.runtime.sendMessage({ + type: BackgroundActiontype.push_action, + data: { + tabId: 'background', + }, + action: addRequestHistory(await getNotaryRequest(id)), + }); + } + + return sendResponse(); + } + case BackgroundActiontype.delete_prove_request: { + const id = request.data; + await removeNotaryRequest(id); + return sendResponse(); + } + case BackgroundActiontype.retry_prove_request: { + const { id, notaryUrl, websocketProxyUrl } = request.data; + + await setNotaryRequestStatus(id, 'pending'); + + const req = await getNotaryRequest(id); + + chrome.runtime.sendMessage({ + type: BackgroundActiontype.process_prove_request, + data: { + ...req, + notaryUrl, + websocketProxyUrl, + }, + }); + + return sendResponse(); + } + case BackgroundActiontype.prove_request_start: { + const { + url, + method, + headers, + body, + maxTranscriptSize, + notaryUrl, + websocketProxyUrl, + } = request.data; + + const { id } = await addNotaryRequest(Date.now(), { + url, + method, + headers, + body, + maxTranscriptSize, + notaryUrl, + websocketProxyUrl, + }); + + await setNotaryRequestStatus(id, 'pending'); + + chrome.runtime.sendMessage({ + type: BackgroundActiontype.push_action, + data: { + tabId: 'background', + }, + action: addRequestHistory(await getNotaryRequest(id)), + }); + + chrome.runtime.sendMessage({ + type: BackgroundActiontype.process_prove_request, + data: { + id, + url, + method, + headers, + body, + maxTranscriptSize, + notaryUrl, + websocketProxyUrl, + }, + }); + + return sendResponse(); + } + default: + break; } - default: - break; - } - }); + }, + ); })(); + +const db = new Level('./ext-db', { + valueEncoding: 'json', +}); +const historyDb = db.sublevel('history', { valueEncoding: 'json' }); + +async function addNotaryRequest( + now = Date.now(), + request: RequestHistory, +): Promise { + const id = charwise.encode(now).toString('hex'); + const newReq = { + ...request, + id, + }; + await historyDb.put(id, newReq); + return newReq; +} + +async function addNotaryRequestProofs( + id: string, + proof: { session: any; substrings: any }, +): Promise { + const existing = await historyDb.get(id); + + if (!existing) return null; + + const newReq = { + ...existing, + proof, + status: 'success', + }; + + await historyDb.put(id, newReq); + + return newReq; +} + +async function setNotaryRequestStatus( + id: string, + status: '' | 'pending' | 'success' | 'error', +): Promise { + const existing = await historyDb.get(id); + + if (!existing) return null; + + const newReq = { + ...existing, + status, + }; + + await historyDb.put(id, newReq); + + return newReq; +} + +async function setNotaryRequestError( + id: string, + error: any, +): Promise { + const existing = await historyDb.get(id); + + if (!existing) return null; + + const newReq = { + ...existing, + error, + status: 'error', + }; + + await historyDb.put(id, newReq); + + return newReq; +} + +async function setNotaryRequestVerification( + id: string, + verification: { sent: string; recv: string }, +): Promise { + const existing = await historyDb.get(id); + + if (!existing) return null; + + const newReq = { + ...existing, + verification, + }; + + await historyDb.put(id, newReq); + + return newReq; +} + +async function removeNotaryRequest(id: string): Promise { + const existing = await historyDb.get(id); + + if (!existing) return null; + + await historyDb.del(id); + + return existing; +} + +async function getNotaryRequests(): Promise { + const retVal = []; + for await (const [key, value] of historyDb.iterator()) { + retVal.push(value); + } + return retVal; +} + +async function getNotaryRequest(id: string): Promise { + return historyDb.get(id); +} diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index a0380487..e0d03862 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -11,6 +11,7 @@ import { useNavigate } from 'react-router'; import { useActiveTabUrl, useRequests } from '../../reducers/requests'; import { Link } from 'react-router-dom'; import { filterByBookmarks } from '../../../utils/bookmark'; +import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage'; import { BackgroundActiontype } from '../Background/actionTypes'; export default function Home(): ReactElement { @@ -21,8 +22,8 @@ export default function Home(): ReactElement { const [wasmRes, setWasmRes] = useState(''); return ( - <> -
+
+
navigate('/requests')}> Requests {`(${requests.length})`} @@ -40,41 +41,13 @@ export default function Home(): ReactElement { > Verify - navigate('/history')} - disabled - > + navigate('/history')}> History - navigate('/options')} - > + navigate('/options')}> Options
-
-
- Dev -
-
- -
- {wasmRes &&
{wasmRes}
} -
{!suggestions.length && (
@@ -125,7 +98,7 @@ export default function Home(): ReactElement { } })}
- +
); } diff --git a/src/pages/Offscreen/Offscreen.tsx b/src/pages/Offscreen/Offscreen.tsx index c1182492..8aea1dd0 100644 --- a/src/pages/Offscreen/Offscreen.tsx +++ b/src/pages/Offscreen/Offscreen.tsx @@ -2,31 +2,93 @@ import React, { useEffect } from 'react'; import * as Comlink from 'comlink'; import { BackgroundActiontype } from '../Background/actionTypes'; +const TLSN: any = Comlink.wrap( + new Worker(new URL('./worker.ts', import.meta.url)), +); + +let tlsn: any | null = null; + +async function getTLSN(): Promise { + if (tlsn) return tlsn; + tlsn = await new TLSN(); + return tlsn; +} + const Offscreen = () => { useEffect(() => { - (async function offscreenloaded() { - console.log('offscreen loaded - spawning worker from worker.ts'); - - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + chrome.runtime.onMessage.addListener( + async (request, sender, sendResponse) => { switch (request.type) { - case BackgroundActiontype.test_wasm: { - const TLSN: any = Comlink.wrap( - new Worker(new URL('./worker.ts', import.meta.url)), - ); + case BackgroundActiontype.process_prove_request: { + const { + url, + method, + headers, + body = '', + maxTranscriptSize, + notaryUrl, + websocketProxyUrl, + id, + } = request.data; + + const tlsn = await getTLSN(); - new TLSN().then(async (tlsn: any) => { - const data = await tlsn.prover(); - sendResponse({ data }); - }); + try { + const proof = await tlsn.prover(url, { + method, + headers, + body, + maxTranscriptSize, + notaryUrl, + websocketProxyUrl, + }); + + chrome.runtime.sendMessage({ + type: BackgroundActiontype.finish_prove_request, + data: { + id, + proof, + }, + }); + } catch (error) { + chrome.runtime.sendMessage({ + type: BackgroundActiontype.finish_prove_request, + data: { + id, + error, + }, + }); + } + + break; + } + case BackgroundActiontype.verify_prove_request: { + const tlsn = await getTLSN(); + + const result = await tlsn.verify( + request.data.proof, + `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBv36FI4ZFszJa0DQFJ3wWCXvVLFr\ncRzMG5kaTeHGoSzDu6cFqx3uEWYpFGo6C0EOUgf+mEgbktLrXocv5yHzKg==\n-----END PUBLIC KEY-----`, + ); + if (result) { + chrome.runtime.sendMessage({ + type: BackgroundActiontype.finish_prove_request, + data: { + id: request.data.id, + verification: { + sent: result.sent, + recv: result.recv, + }, + }, + }); + } break; } default: break; } - return true; - }); - })(); + }, + ); }, []); return
; diff --git a/src/pages/Offscreen/worker.ts b/src/pages/Offscreen/worker.ts index b7e7608f..7535babe 100644 --- a/src/pages/Offscreen/worker.ts +++ b/src/pages/Offscreen/worker.ts @@ -1,72 +1,83 @@ import * as Comlink from 'comlink'; +import { urlify, devlog } from '../../utils/misc'; import init, { initThreadPool, prover, + verify, } from '../../../wasm/prover/pkg/tlsn_extension_rs'; class TLSN { + private startPromise: any; + private resolveStart: any; + constructor() { - console.log('worker test module initiated.'); + console.log('worker module initiated.'); + this.startPromise = new Promise((resolve) => { + this.resolveStart = resolve; + }); + this.start(); } - async prover() { - try { - console.log('start'); - const numConcurrency = navigator.hardwareConcurrency; - console.log('!@# navigator.hardwareConcurrency=', numConcurrency); - const res = await init(); - console.log('!@# res.memory=', res.memory); - // 6422528 ~= 6.12 mb - console.log( - '!@# res.memory.buffer.length=', - res.memory.buffer.byteLength, - ); - await initThreadPool(numConcurrency); - - const maxTranscriptSize = 16384; - const notaryHost = "127.0.0.1"; - // const notaryHost : &str = "notary.efprivacyscaling.org"; - const notaryPort = 7047; - - const serverDomain = "api.twitter.com"; - const route = "1.1/account/settings.json"; - - const userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"; + async start() { + devlog('start'); + const numConcurrency = navigator.hardwareConcurrency; + devlog('!@# navigator.hardwareConcurrency=', numConcurrency); + const res = await init(); + devlog('!@# res.memory=', res.memory); + // 6422528 ~= 6.12 mb + devlog('!@# res.memory.buffer.length=', res.memory.buffer.byteLength); + await initThreadPool(numConcurrency); + this.resolveStart(); + } - const authToken = ""; - const accessToken = ""; - const csrfToken = ""; - const twitterId = ""; - const websocketProxyURL = "ws://127.0.0.1:55688"; + async waitForStart() { + return this.startPromise; + } - const resProver = await prover( - maxTranscriptSize, - notaryHost, - notaryPort, - serverDomain, - route, - userAgent, - authToken, - accessToken, - csrfToken, - twitterId, - websocketProxyURL, - ); + async prover( + url: string, + options?: { + method?: string; + headers?: { [key: string]: string }; + body?: string; + maxTranscriptSize?: number; + notaryUrl?: string; + websocketProxyUrl?: string; + }, + ) { + try { + await this.waitForStart(); + console.log('worker', url, { + ...options, + notaryUrl: options.notaryUrl, + websocketProxyUrl: options.websocketProxyUrl, + }); + const resProver = await prover(url, { + ...options, + notaryUrl: options.notaryUrl, + websocketProxyUrl: options.websocketProxyUrl, + }); const resJSON = JSON.parse(resProver); - console.log('!@# resProver=', resProver); - console.log('!@# resAfter.memory=', res.memory); + devlog('!@# resProver,resJSON=', { resProver, resJSON }); + devlog('!@# resAfter.memory=', resJSON.memory); // 1105920000 ~= 1.03 gb - console.log( + devlog( '!@# resAfter.memory.buffer.length=', - res.memory.buffer.byteLength, + resJSON.memory?.buffer?.byteLength, ); return resJSON; } catch (e: any) { - console.log(e); + devlog(e); return e; } } + + async verify(proof: any, pubkey: string) { + await this.waitForStart(); + const raw = await verify(JSON.stringify(proof), pubkey); + return JSON.parse(raw); + } } Comlink.expose(TLSN); diff --git a/src/pages/Popup/Popup.tsx b/src/pages/Popup/Popup.tsx index 3e9eb720..f44cf5d5 100644 --- a/src/pages/Popup/Popup.tsx +++ b/src/pages/Popup/Popup.tsx @@ -14,6 +14,9 @@ import Request from '../Requests/Request'; import Home from '../Home'; import logo from '../../assets/img/icon-128.png'; import RequestBuilder from '../RequestBuilder'; +import Notarize from '../../components/Notarize'; +import ProofViewer from '../../components/ProofViewer'; +import History from '../../components/History'; const Popup = () => { const dispatch = useDispatch(); @@ -36,6 +39,11 @@ const Popup = () => { }); dispatch(setRequests(logs)); + + const history = await chrome.runtime.sendMessage({ + type: BackgroundActiontype.get_prove_requests, + data: tab?.id, + }); })(); }, []); @@ -59,6 +67,9 @@ const Popup = () => {
} /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/pages/Popup/index.tsx b/src/pages/Popup/index.tsx index 003386a3..154687fa 100644 --- a/src/pages/Popup/index.tsx +++ b/src/pages/Popup/index.tsx @@ -14,7 +14,10 @@ const root = createRoot(container!); // createRoot(container!) if you use TypeSc chrome.runtime.onMessage.addListener((request) => { switch (request.type) { case BackgroundActiontype.push_action: { - if (request.data.tabId === store.getState().requests.activeTab?.id) { + if ( + request.data.tabId === store.getState().requests.activeTab?.id || + request.data.tabId === 'background' + ) { store.dispatch(request.action); } break; diff --git a/src/pages/RequestBuilder/index.tsx b/src/pages/RequestBuilder/index.tsx index a651f626..4669aa74 100644 --- a/src/pages/RequestBuilder/index.tsx +++ b/src/pages/RequestBuilder/index.tsx @@ -18,16 +18,31 @@ enum TabType { Body = 'Body', } -export default function RequestBuilder(): ReactElement { +export default function RequestBuilder(props?: { + subpath?: string; + url?: string; + params?: [string, string, boolean?][]; + headers?: [string, string, boolean?][]; + body?: string; + method?: string; + response?: Response; +}): ReactElement { const loc = useLocation(); const navigate = useNavigate(); - const [_url, setUrl] = useState(''); - const [params, setParams] = useState<[string, string, boolean?][]>([]); - const [headers, setHeaders] = useState<[string, string, boolean?][]>([]); - const [body, setBody] = useState(undefined); - const [method, setMethod] = useState('GET'); - const [response, setResponse] = useState(null); + const subpath = props.subpath || '/custom'; + const [_url, setUrl] = useState(props.url || ''); + const [params, setParams] = useState<[string, string, boolean?][]>( + props.params || [], + ); + const [headers, setHeaders] = useState<[string, string, boolean?][]>( + props.headers || [], + ); + const [body, setBody] = useState(props.body); + const [method, setMethod] = useState(props.method || 'GET'); + const [response, setResponse] = useState( + props.response || null, + ); const url = urlify(_url); @@ -101,7 +116,7 @@ export default function RequestBuilder(): ReactElement { setResponse(res); - navigate('/custom/response'); + navigate(subpath + '/response'); }, [href, method, headers, body]); return ( @@ -129,26 +144,26 @@ export default function RequestBuilder(): ReactElement {
navigate('/custom/params')} + onClick={() => navigate(subpath + '/params')} active={loc.pathname.includes('params')} > Params navigate('/custom/headers')} + onClick={() => navigate(subpath + '/headers')} active={loc.pathname.includes('headers')} > Headers navigate('/custom/body')} + onClick={() => navigate(subpath + '/body')} active={loc.pathname.includes('body')} > Body {response && ( navigate('/custom/response')} + onClick={() => navigate(subpath + '/response')} active={loc.pathname.includes('response')} > Response diff --git a/src/pages/Requests/Request.tsx b/src/pages/Requests/Request.tsx index 711d5184..1e2b12f0 100644 --- a/src/pages/Requests/Request.tsx +++ b/src/pages/Requests/Request.tsx @@ -5,11 +5,10 @@ import { useRequest } from '../../reducers/requests'; export default function Request(): ReactElement { const params = useParams<{ requestId: string }>(); - const request = useRequest(params.requestId); return ( <> - + ); } diff --git a/src/reducers/history.tsx b/src/reducers/history.tsx new file mode 100644 index 00000000..7df302b2 --- /dev/null +++ b/src/reducers/history.tsx @@ -0,0 +1,99 @@ +import { + BackgroundActiontype, + RequestHistory, +} from '../pages/Background/actionTypes'; +import { useSelector } from 'react-redux'; +import { AppRootState } from './index'; +import { BackgroundActiontype } from '../Background/actionTypes'; +import deepEqual from 'fast-deep-equal'; + +enum ActionType { + '/history/addRequest' = '/history/addRequest', + '/history/deleteRequest' = '/history/deleteRequest', +} + +type Action = { + type: ActionType; + payload?: payload; + error?: boolean; + meta?: any; +}; + +type State = { + map: { + [requestId: string]: RequestHistory; + }; + order: string[]; +}; + +const initialState: State = { + map: {}, + order: [], +}; + +export const addRequestHistory = (request: RequestHistory) => { + return { + type: ActionType['/history/addRequest'], + payload: request, + }; +}; + +export const deleteRequestHistory = (id: string) => { + chrome.runtime.sendMessage({ + type: BackgroundActiontype.delete_prove_request, + data: id, + }); + + return { + type: ActionType['/history/deleteRequest'], + payload: id, + }; +}; + +export default function history( + state = initialState, + action: Action, +): State { + switch (action.type) { + case ActionType['/history/addRequest']: { + const payload: RequestHistory = action.payload; + const existing = state.map[payload.id]; + const newMap = { + ...state.map, + [payload.id]: payload, + }; + const newOrder = existing ? state.order : state.order.concat(payload.id); + + return { + ...state, + map: newMap, + order: newOrder, + }; + } + case ActionType['/history/deleteRequest']: { + const reqId: string = action.payload; + const newMap = { ...state.map }; + delete newMap[reqId]; + const newOrder = state.order.filter((id) => id !== reqId); + return { + ...state, + map: newMap, + order: newOrder, + }; + } + default: + return state; + } +} + +export const useHistoryOrder = (): RequestHistory[] => { + return useSelector((state: AppRootState) => { + return state.history.order; + }, deepEqual); +}; + +export const useRequestHistory = (id: string): RequestHistory | undefined => { + return useSelector((state: AppRootState) => { + return state.history.map[id]; + }, deepEqual); +}; diff --git a/src/reducers/index.tsx b/src/reducers/index.tsx index 6633f8ac..23a4f1bb 100644 --- a/src/reducers/index.tsx +++ b/src/reducers/index.tsx @@ -1,8 +1,10 @@ import { combineReducers } from 'redux'; import requests from './requests'; +import history from './history'; const rootReducer = combineReducers({ requests, + history, }); export type AppRootState = ReturnType; diff --git a/src/reducers/requests.ts b/src/reducers/requests.ts index eae3ee4e..079f625e 100644 --- a/src/reducers/requests.ts +++ b/src/reducers/requests.ts @@ -1,7 +1,9 @@ -import { RequestLog } from '../pages/Background/actionTypes'; +import { type RequestLog, type RequestHistory } from '../pages/Background/actionTypes'; import { useSelector } from 'react-redux'; import { AppRootState } from './index'; import deepEqual from 'fast-deep-equal'; +import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../utils/storage'; +import { BackgroundActiontype } from '../pages/Background/actionTypes'; enum ActionType { '/requests/setRequests' = '/requests/setRequests', @@ -33,6 +35,37 @@ export const setRequests = (requests: RequestLog[]): Action => ({ payload: requests, }); +export const notarizeRequest = (options: RequestHistory) => async () => { + const notaryUrl = await get(NOTARY_API_LS_KEY); + const websocketProxyUrl = await get(PROXY_API_LS_KEY); + + console.log({ + type: BackgroundActiontype.prove_request_start, + data: { + url: options.url, + method: options.method, + headers: options.headers, + body: options.body, + maxTranscriptSize: options.maxTranscriptSize, + notaryUrl, + websocketProxyUrl, + }, + }) + + chrome.runtime.sendMessage({ + type: BackgroundActiontype.prove_request_start, + data: { + url: options.url, + method: options.method, + headers: options.headers, + body: options.body, + maxTranscriptSize: options.maxTranscriptSize, + notaryUrl, + websocketProxyUrl, + }, + }); +} + export const setActiveTab = ( activeTab: chrome.tabs.Tab | null, ): Action => ({ @@ -54,9 +87,11 @@ export default function requests( return { ...state, map: { - ...action.payload.reduce( + ...(action.payload || []).reduce( (acc: { [requestId: string]: RequestLog }, req: RequestLog) => { - acc[req.requestId] = req; + if (req) { + acc[req.requestId] = req; + } return acc; }, {}, diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 9c1c555c..b4b43172 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -16,3 +16,22 @@ export function urlify( return null; } } + +export function devlog(text: string) { + if (process.env.NODE_ENV === 'development') { + console.log(text); + } +} + +export function download(filename: string, content: string) { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content)); + element.setAttribute('download', filename); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); +} diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 00000000..e534c4b8 --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,14 @@ +export const NOTARY_API_LS_KEY = 'notary-api'; +export const PROXY_API_LS_KEY = 'proxy-api'; +export const HISTORY_LS_KEY = 'history'; + +export async function set(key: string, value: string) { + return chrome.storage.sync.set({ [key]: value }); +} + +export async function get(key: string) { + return chrome.storage.sync + .get(key) + .then((json: any) => json[key]) + .catch(() => ''); +} diff --git a/utils/bookmark/bookmarks.json b/utils/bookmark/bookmarks.json index 6b13f78b..6ef34647 100644 --- a/utils/bookmark/bookmarks.json +++ b/utils/bookmark/bookmarks.json @@ -1,11 +1,4 @@ [ - { - "url": "https://twitter.com/i/api/1.1/dm/conversation", - "method": "GET", - "type": "xmlhttprequest", - "title": "Twitter Conversation History", - "description": "Notarize history of a twitter conversation. To start, open any conversation from twitter.com/messages." - }, { "url": "https://api.twitter.com/1.1/account/settings.json", "method": "GET", diff --git a/wasm/prover/Cargo.toml b/wasm/prover/Cargo.toml index e8220faf..52fce590 100644 --- a/wasm/prover/Cargo.toml +++ b/wasm/prover/Cargo.toml @@ -21,7 +21,12 @@ tokio-util = "0.7" futures = "0.3" serde_json = "1.0" serde = { version = "1.0.147", features = ["derive"] } +serde-wasm-bindgen = "0.4" +url = { version = "2.0", features = ["serde"] } futures-util = "0.3.28" +chrono = "0.4" +elliptic-curve = { version = "0.13.5", features = ["pkcs8"] } +p256 = { version = "0.13", features = ["pem", "ecdsa"] } hyper = { version = "0.14", features = ["client", "http1"] } console_error_panic_hook = "0.1.7" diff --git a/wasm/prover/src/lib.rs b/wasm/prover/src/lib.rs index 918bb1f2..0db315b5 100644 --- a/wasm/prover/src/lib.rs +++ b/wasm/prover/src/lib.rs @@ -1,4 +1,5 @@ mod requests; +mod requestOpt; use std::panic; use std::ops::Range; @@ -7,7 +8,6 @@ use web_time::Instant; use hyper::{body::to_bytes, Body, Request, StatusCode}; use futures::{AsyncWriteExt, TryFutureExt}; use futures::channel::oneshot; -use tlsn_core::proof::TlsProof; use tlsn_prover::{Prover, ProverConfig}; // use tokio::io::AsyncWriteExt as _; @@ -25,6 +25,7 @@ use tracing_subscriber::prelude::*; use ws_stream_wasm::{*}; use crate::requests::{NotarizationSessionRequest, NotarizationSessionResponse, ClientType}; +use crate::requestOpt::{RequestOptions, VerifyResult}; pub use wasm_bindgen_rayon::init_thread_pool; // use rayon::iter::IntoParallelRefIterator; @@ -33,6 +34,11 @@ use rayon::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::{Request as WebsysRequest, RequestInit, Headers, RequestMode, Response}; use js_sys::JSON; +use url::Url; + +use tlsn_core::proof::{SessionProof, TlsProof}; +use std::time::Duration; +use elliptic_curve::pkcs8::DecodePublicKey; // A macro to provide `println!(..)`-style syntax for `console.log` logging. macro_rules! log { @@ -62,30 +68,28 @@ async fn fetch_as_json_string(url: &str, opts: &RequestInit) -> Result Result { - let fmt_layer = tracing_subscriber::fmt::layer() - .with_ansi(false) // Only partially supported across browsers - .with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers - .with_writer(MakeConsoleWriter); // write events to the console - let perf_layer = performance_layer() - .with_details_from_fields(Pretty::default()); - - tracing_subscriber::registry() - .with(tracing_subscriber::filter::LevelFilter::DEBUG) - .with(fmt_layer) - .with(perf_layer) - .init(); // Install these as subscribers to tracing events + log!("target_url: {}", targetUrl); + let target_url = Url::parse(targetUrl).expect("url must be valid"); + + log!("target_url.host: {}", target_url.host().unwrap()); + let options: RequestOptions = serde_wasm_bindgen::from_value(val).unwrap(); + log!("done!"); + log!("options.notary_url: {}", options.notary_url.as_str()); + // let fmt_layer = tracing_subscriber::fmt::layer() + // .with_ansi(false) // Only partially supported across browsers + // .with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers + // .with_writer(MakeConsoleWriter); // write events to the console + // let perf_layer = performance_layer() + // .with_details_from_fields(Pretty::default()); + + // tracing_subscriber::registry() + // .with(tracing_subscriber::filter::LevelFilter::DEBUG) + // .with(fmt_layer) + // .with(perf_layer) + // .init(); // Install these as subscribers to tracing events // https://github.com/rustwasm/console_error_panic_hook panic::set_hook(Box::new(console_error_panic_hook::hook)); @@ -97,32 +101,48 @@ pub async fn prover( */ let mut opts = RequestInit::new(); + log!("method: {}", "POST"); opts.method("POST"); // opts.method("GET"); opts.mode(RequestMode::Cors); // set headers let headers = Headers::new().unwrap(); + let notary_url = Url::parse(options.notary_url.as_str()).expect("url must be valid"); + let notary_ssl = notary_url.scheme() == "https" || notary_url.scheme() == "wss"; + let notary_host = notary_url.authority(); + headers.append("Host", notary_host).unwrap(); headers.append("Content-Type", "application/json").unwrap(); opts.headers(&headers); + log!("notary_host: {}", notary_host); // set body let payload = serde_json::to_string(&NotarizationSessionRequest { client_type: ClientType::Websocket, - max_transcript_size: Some(max_transcript_size), + max_transcript_size: Some(options.max_transcript_size), }) .unwrap(); opts.body(Some(&JsValue::from_str(&payload))); // url - let url = format!("https://{}:{}/session", notary_host, notary_port); + let url = format!( + "{}://{}/session", + if notary_ssl { "https" } else { "http" }, + notary_host + ); + log!("Request: {}", url); let rust_string = fetch_as_json_string(&url, &opts).await.unwrap(); let notarization_response = serde_json::from_str::(&rust_string).unwrap(); log!("Response: {}", rust_string); log!("Notarization response: {:?}", notarization_response,); - let notary_wss_url = format!("wss://{}:{}/notarize?sessionId={}", notary_host, notary_port, notarization_response.session_id); + let notary_wss_url = format!( + "{}://{}/notarize?sessionId={}", + if notary_ssl { "wss" } else { "ws" }, + notary_host, + notarization_response.session_id + ); let (mut notary_ws_meta, mut notary_ws_stream) = WsMeta::connect( notary_wss_url, None @@ -135,17 +155,18 @@ pub async fn prover( */ let (mut client_ws_meta, mut client_ws_stream) = WsMeta::connect( - websocket_proxy_url, + options.websocket_proxy_url, None ).await .expect_throw( "assume the client ws connection succeeds" ); let mut client_ws_stream_into = client_ws_stream.into_io(); log!("!@# 0"); + let target_host = target_url.host_str().unwrap(); // Basic default prover config let config = ProverConfig::builder() .id(notarization_response.session_id) - .server_dns(server_domain) + .server_dns(target_host) .build() .unwrap(); @@ -205,29 +226,34 @@ pub async fn prover( } }; spawn_local(handled_connection_fut); - log!("!@# 9"); - - let request = Request::builder() - .uri(format!("https://{server_domain}/{route}")) - .header("Host", server_domain) - .header("Accept", "*/*") - .header("Accept-Encoding", "identity") - .header("Connection", "close") - .header("User-Agent", user_agent) - .header("Authorization", format!("Bearer {access_token}")) - .header( - "Cookie", - format!("auth_token={auth_token}; ct0={csrf_token}"), - ) - .header("X-Csrf-Token", csrf_token) - .body(Body::empty()) - .unwrap(); + log!("!@# 9 - {} request to {}", options.method.as_str(), targetUrl); + + let mut req_with_header = Request::builder() + .uri(targetUrl) + .method(options.method.as_str()); + + for (key, value) in options.headers { + log!("adding header: {} - {}", key.as_str(), value.as_str()); + req_with_header = req_with_header.header(key.as_str(), value.as_str()); + } + + let req_with_body; + + if options.body.is_empty() { + log!("empty body"); + req_with_body = req_with_header.body(Body::empty()); + } else { + log!("added body - {}", options.body.as_str()); + req_with_body = req_with_header.body(Body::from(options.body)); + } + + let unwrapped_request = req_with_body.unwrap(); log!("Starting an MPC TLS connection with the server"); // Send the request to the Server and get a response via the MPC TLS connection - let response = request_sender.send_request(request).await.unwrap(); + let response = request_sender.send_request(unwrapped_request).await.unwrap(); log!("Got a response from the server"); @@ -260,16 +286,18 @@ pub async fn prover( let (sent_public_ranges, sent_private_ranges) = find_ranges( prover.sent_transcript().data(), &[ - access_token.as_bytes(), - auth_token.as_bytes(), - csrf_token.as_bytes(), + // access_token.as_bytes(), + // auth_token.as_bytes(), + // csrf_token.as_bytes(), ], ); // Identify the ranges in the transcript that contain the only data we want to reveal later - let (recv_private_ranges, recv_public_ranges) = find_ranges( + let (recv_public_ranges, recv_private_ranges) = find_ranges( prover.recv_transcript().data(), - &[format!("\"screen_name\":\"{twitter_id}\"").as_bytes()], + &[ + // format!("\"screen_name\":\"{twitter_id}\"").as_bytes() + ], ); log!("!@# 15"); @@ -330,6 +358,83 @@ pub async fn prover( } +#[wasm_bindgen] +pub async fn verify( + proof: &str, + notary_pubkey_str: &str, +) -> Result { + log!("!@# proof {}", proof); + let proof: TlsProof = serde_json::from_str(proof).unwrap(); + + let TlsProof { + // The session proof establishes the identity of the server and the commitments + // to the TLS transcript. + session, + // The substrings proof proves select portions of the transcript, while redacting + // anything the Prover chose not to disclose. + substrings, + } = proof; + + + log!("!@# notary_pubkey {}, {}", notary_pubkey_str, notary_pubkey_str.len()); + session + .verify_with_default_cert_verifier(get_notary_pubkey(notary_pubkey_str)) + .unwrap(); + + + let SessionProof { + // The session header that was signed by the Notary is a succinct commitment to the TLS transcript. + header, + // This is the server name, checked against the certificate chain shared in the TLS handshake. + server_name, + .. + } = session; + + // The time at which the session was recorded + let time = chrono::DateTime::UNIX_EPOCH + Duration::from_secs(header.time()); + + // Verify the substrings proof against the session header. + // + // This returns the redacted transcripts + let (mut sent, mut recv) = substrings.verify(&header).unwrap(); + + // Replace the bytes which the Prover chose not to disclose with 'X' + sent.set_redacted(b'X'); + recv.set_redacted(b'X'); + + log!("-------------------------------------------------------------------"); + log!( + "Successfully verified that the bytes below came from a session with {:?} at {}.", + server_name, time + ); + log!("Note that the bytes which the Prover chose not to disclose are shown as X."); + log!("Bytes sent:"); + log!("{}", String::from_utf8(sent.data().to_vec()).unwrap()); + log!("Bytes received:"); + log!("{}", String::from_utf8(recv.data().to_vec()).unwrap()); + log!("-------------------------------------------------------------------"); + + let result = VerifyResult { + server_name: String::from(server_name.as_str()), + time: header.time(), + sent: String::from_utf8(sent.data().to_vec()).unwrap(), + recv: String::from_utf8(recv.data().to_vec()).unwrap(), + }; + let res = serde_json::to_string_pretty(&result).unwrap(); + + Ok(res) +} + +fn print_type_of(_: &T) { + log!("{}", std::any::type_name::()) +} + +/// Returns a Notary pubkey trusted by this Verifier +fn get_notary_pubkey(pubkey: &str) -> p256::PublicKey { + // from https://github.com/tlsnotary/notary-server/tree/main/src/fixture/notary/notary.key + // converted with `openssl ec -in notary.key -pubout -outform PEM` + p256::PublicKey::from_public_key_pem(pubkey).unwrap() +} /// Find the ranges of the public and private parts of a sequence. /// diff --git a/wasm/prover/src/requestOpt.rs b/wasm/prover/src/requestOpt.rs new file mode 100644 index 00000000..2114bf99 --- /dev/null +++ b/wasm/prover/src/requestOpt.rs @@ -0,0 +1,29 @@ +use std::{collections::HashMap}; +use serde::{Deserialize, Serialize}; + +/// Requestion Options of Fetch API +// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestOptions { + pub method: String, // *GET, POST, PUT, DELETE, etc. + // pub mode: String, // no-cors, *cors, same-origin + // pub cache: String, // *default, no-cache, reload, force-cache, only-if-cached + // pub credentials: String, // include, *same-origin, omit + pub headers: HashMap, + // pub redirect: String, // manual, *follow, error + // pub referrer_policy: String, // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url + pub body: String, // body data type must match "Content-Type" header + pub max_transcript_size: usize, + pub notary_url: String, + pub websocket_proxy_url: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifyResult { + pub server_name: String, + pub time: u64, + pub sent: String, + pub recv: String, +} \ No newline at end of file