1 | import localforage from 'localforage';
|
2 | import * as common from "./common/index.js";
|
3 | import * as identifiers from "./common/identifiers.js";
|
4 | import * as ipfs from "./ipfs/index.js";
|
5 | import * as pathing from "./path.js";
|
6 | import * as crypto from "./crypto/index.js";
|
7 | import * as storage from "./storage/index.js";
|
8 | import * as ucan from "./ucan/internal.js";
|
9 | import * as ucanPermissions from "./ucan/permissions.js";
|
10 | import { setup } from "./setup/internal.js";
|
11 | import * as did from "./did/index.js";
|
12 | import { USERNAME_STORAGE_KEY } from "./common/index.js";
|
13 | import { loadFileSystem } from "./filesystem.js";
|
14 | import FileSystem from "./fs/index.js";
|
15 |
|
16 | export var Scenario;
|
17 | (function (Scenario) {
|
18 | Scenario["NotAuthorised"] = "NOT_AUTHORISED";
|
19 | Scenario["AuthSucceeded"] = "AUTH_SUCCEEDED";
|
20 | Scenario["AuthCancelled"] = "AUTH_CANCELLED";
|
21 | Scenario["Continuation"] = "CONTINUATION";
|
22 | })(Scenario || (Scenario = {}));
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | export var InitialisationError;
|
28 | (function (InitialisationError) {
|
29 | InitialisationError["InsecureContext"] = "INSECURE_CONTEXT";
|
30 | InitialisationError["UnsupportedBrowser"] = "UNSUPPORTED_BROWSER";
|
31 | })(InitialisationError || (InitialisationError = {}));
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | export async function initialise(options) {
|
41 | options = options || {};
|
42 | const permissions = options.permissions || null;
|
43 | const { autoRemoveUrlParams = true, rootKey } = options;
|
44 | const maybeLoadFs = async (username) => {
|
45 | return options.loadFileSystem === false
|
46 | ? undefined
|
47 | : await loadFileSystem(permissions, username, rootKey);
|
48 | };
|
49 |
|
50 | if (globalThis.isSecureContext === false)
|
51 | throw InitialisationError.InsecureContext;
|
52 | if (await isSupported() === false)
|
53 | throw InitialisationError.UnsupportedBrowser;
|
54 |
|
55 | const url = new URL(window.location.href);
|
56 | const authorised = url.searchParams.get("authorised");
|
57 | const cancellation = url.searchParams.get("cancelled");
|
58 |
|
59 | if (authorised) {
|
60 | const newUser = url.searchParams.get("newUser") === "t";
|
61 | const username = url.searchParams.get("username") || "";
|
62 | await importClassifiedInfo(authorised === "via-postmessage"
|
63 | ? await getClassifiedViaPostMessage()
|
64 | : await ipfs.cat(authorised)
|
65 | );
|
66 | await storage.setItem(USERNAME_STORAGE_KEY, username);
|
67 | if (autoRemoveUrlParams) {
|
68 | url.searchParams.delete("authorised");
|
69 | url.searchParams.delete("newUser");
|
70 | url.searchParams.delete("username");
|
71 | history.replaceState(null, document.title, url.toString());
|
72 | }
|
73 | if (permissions && await validateSecrets(permissions) === false) {
|
74 | console.warn("Unable to validate filesystem secrets");
|
75 | return scenarioNotAuthorised(permissions);
|
76 | }
|
77 | if (permissions && ucan.validatePermissions(permissions, username) === false) {
|
78 | console.warn("Unable to validate UCAN permissions");
|
79 | return scenarioNotAuthorised(permissions);
|
80 | }
|
81 | return scenarioAuthSucceeded(permissions, newUser, username, await maybeLoadFs(username));
|
82 | }
|
83 | else if (cancellation) {
|
84 | const c = (() => {
|
85 | switch (cancellation) {
|
86 | case "DENIED": return "User denied authorisation";
|
87 | default: return "Unknown reason";
|
88 | }
|
89 | })();
|
90 | return scenarioAuthCancelled(permissions, c);
|
91 | }
|
92 | else {
|
93 |
|
94 | await ucan.store([]);
|
95 | }
|
96 | const authedUsername = await common.authenticatedUsername();
|
97 | if (authedUsername && permissions) {
|
98 | const validSecrets = await validateSecrets(permissions);
|
99 | const validUcans = ucan.validatePermissions(permissions, authedUsername);
|
100 | if (validSecrets && validUcans) {
|
101 | return scenarioContinuation(permissions, authedUsername, await maybeLoadFs(authedUsername));
|
102 | }
|
103 | else {
|
104 | return scenarioNotAuthorised(permissions);
|
105 | }
|
106 | }
|
107 | else if (authedUsername) {
|
108 | return scenarioContinuation(permissions, authedUsername, await maybeLoadFs(authedUsername));
|
109 | }
|
110 | else {
|
111 | return scenarioNotAuthorised(permissions);
|
112 | }
|
113 | }
|
114 |
|
115 |
|
116 |
|
117 | export { initialise as initialize };
|
118 |
|
119 | export async function isSupported() {
|
120 | return localforage.supports(localforage.INDEXEDDB)
|
121 |
|
122 |
|
123 | && await (() => new Promise(resolve => {
|
124 | const db = indexedDB.open("testDatabase");
|
125 | db.onsuccess = () => resolve(true);
|
126 | db.onerror = () => resolve(false);
|
127 | }))();
|
128 | }
|
129 |
|
130 | export * from "./auth.js";
|
131 | export * from "./filesystem.js";
|
132 | export * from "./common/version.js";
|
133 | export const fs = FileSystem;
|
134 | export * as apps from "./apps/index.js";
|
135 | export * as dataRoot from "./data-root.js";
|
136 | export * as did from "./did/index.js";
|
137 | export * as errors from "./errors.js";
|
138 | export * as lobby from "./lobby/index.js";
|
139 | export * as path from "./path.js";
|
140 | export * as setup from "./setup.js";
|
141 | export * as ucan from "./ucan/index.js";
|
142 | export * as dns from "./dns/index.js";
|
143 | export * as ipfs from "./ipfs/index.js";
|
144 | export * as keystore from "./keystore.js";
|
145 | export * as machinery from "./common/index.js";
|
146 | export * as crypto from "./crypto/index.js";
|
147 | export * as cbor from 'cborg';
|
148 |
|
149 | function scenarioAuthSucceeded(permissions, newUser, username, fs) {
|
150 | return {
|
151 | scenario: Scenario.AuthSucceeded,
|
152 | permissions,
|
153 | authenticated: true,
|
154 | throughLobby: true,
|
155 | fs,
|
156 | newUser,
|
157 | username
|
158 | };
|
159 | }
|
160 | function scenarioAuthCancelled(permissions, cancellationReason) {
|
161 | return {
|
162 | scenario: Scenario.AuthCancelled,
|
163 | permissions,
|
164 | authenticated: false,
|
165 | throughLobby: true,
|
166 | cancellationReason
|
167 | };
|
168 | }
|
169 | function scenarioContinuation(permissions, username, fs) {
|
170 | return {
|
171 | scenario: Scenario.Continuation,
|
172 | permissions,
|
173 | authenticated: true,
|
174 | newUser: false,
|
175 | throughLobby: false,
|
176 | fs,
|
177 | username
|
178 | };
|
179 | }
|
180 | function scenarioNotAuthorised(permissions) {
|
181 | return {
|
182 | scenario: Scenario.NotAuthorised,
|
183 | permissions,
|
184 | authenticated: false
|
185 | };
|
186 | }
|
187 | async function importClassifiedInfo(classified) {
|
188 | const info = JSON.parse(classified);
|
189 |
|
190 | const rawSessionKey = await crypto.keystore.decrypt(info.sessionKey);
|
191 |
|
192 | const secretsStr = await crypto.aes.decryptGCM(info.secrets, rawSessionKey, info.iv);
|
193 | const secrets = JSON.parse(secretsStr);
|
194 | const fsSecrets = secrets.fs;
|
195 | const ucans = secrets.ucans;
|
196 |
|
197 | await Promise.all(Object.entries(fsSecrets).map(async ([posixPath, { bareNameFilter, key }]) => {
|
198 | const path = pathing.fromPosix(posixPath);
|
199 | const readKeyId = await identifiers.readKey({ path });
|
200 | const bareNameFilterId = await identifiers.bareNameFilter({ path });
|
201 | await crypto.keystore.importSymmKey(key, readKeyId);
|
202 | await storage.setItem(bareNameFilterId, bareNameFilter);
|
203 | }));
|
204 |
|
205 | await ucan.store(ucans);
|
206 | }
|
207 | async function getClassifiedViaPostMessage() {
|
208 | const iframe = await new Promise(resolve => {
|
209 | const iframe = document.createElement("iframe");
|
210 | iframe.id = "webnative-secret-exchange";
|
211 | iframe.style.width = "0";
|
212 | iframe.style.height = "0";
|
213 | iframe.style.border = "none";
|
214 | iframe.style.display = "none";
|
215 | document.body.appendChild(iframe);
|
216 | iframe.onload = () => {
|
217 | resolve(iframe);
|
218 | };
|
219 | iframe.src = `${setup.endpoints.lobby}/exchange.html`;
|
220 | });
|
221 | try {
|
222 | const answer = new Promise((resolve, reject) => {
|
223 | window.addEventListener("message", listen);
|
224 | function listen(event) {
|
225 | window.removeEventListener("message", listen);
|
226 | if (event.data) {
|
227 | resolve(event.data);
|
228 | }
|
229 | else {
|
230 | reject(new Error("Can't import UCANs & readKey(s): Missing data"));
|
231 | }
|
232 | }
|
233 | });
|
234 | if (iframe.contentWindow == null)
|
235 | throw new Error("Can't import UCANs & readKey(s): No access to its contentWindow");
|
236 | const message = {
|
237 | webnative: "exchange-secrets",
|
238 | didExchange: await did.exchange()
|
239 | };
|
240 | iframe.contentWindow.postMessage(message, iframe.src);
|
241 | return await answer;
|
242 | }
|
243 | finally {
|
244 | document.body.removeChild(iframe);
|
245 | }
|
246 | }
|
247 | async function validateSecrets(permissions) {
|
248 | return ucanPermissions.paths(permissions).reduce((acc, path) => acc.then(async (bool) => {
|
249 | if (bool === false)
|
250 | return bool;
|
251 | if (pathing.isBranch(pathing.Branch.Public, path))
|
252 | return bool;
|
253 | const keyName = await identifiers.readKey({ path });
|
254 | return await crypto.keystore.keyExists(keyName);
|
255 | }), Promise.resolve(true));
|
256 | }
|
257 |
|
\ | No newline at end of file |