feat(pwa): add base
All checks were successful
/ build-web (push) Successful in 5m35s
/ build-desktop (linux) (push) Successful in 9m41s
/ build-mobile (push) Successful in 20m12s
/ release (push) Successful in 1m51s
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
All checks were successful
/ build-web (push) Successful in 5m35s
/ build-desktop (linux) (push) Successful in 9m41s
/ build-mobile (push) Successful in 20m12s
/ release (push) Successful in 1m51s
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
This commit is contained in:
parent
6e977bca7e
commit
7b36218ffa
40 changed files with 4014 additions and 922 deletions
40
packages/storage/src/browserSecure.ts
Normal file
40
packages/storage/src/browserSecure.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { getDatabaseEntry } from "@tensamin/shared/indexedDb";
|
||||
|
||||
type SecureEnvelope = {
|
||||
__tensaminSecure: 1;
|
||||
version: 1;
|
||||
iv: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
function base64ToBytes(value: string) {
|
||||
const binary = atob(value);
|
||||
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
||||
}
|
||||
|
||||
function isSecureEnvelope(value: unknown): value is SecureEnvelope {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const envelope = value as Partial<SecureEnvelope>;
|
||||
return (
|
||||
envelope.__tensaminSecure === 1 &&
|
||||
envelope.version === 1 &&
|
||||
typeof envelope.iv === "string" &&
|
||||
typeof envelope.data === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadSecureBrowserValue<T>(key: string) {
|
||||
const stored = await getDatabaseEntry<unknown>("storage", key);
|
||||
if (stored === undefined || !isSecureEnvelope(stored)) {
|
||||
return stored as T | undefined;
|
||||
}
|
||||
|
||||
const masterKey = await getDatabaseEntry<CryptoKey>("keys", "master-v1");
|
||||
if (!masterKey) throw new Error("Secure storage key is unavailable.");
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: base64ToBytes(stored.iv) },
|
||||
masterKey,
|
||||
base64ToBytes(stored.data),
|
||||
);
|
||||
return JSON.parse(new TextDecoder().decode(plaintext)) as T;
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ import {
|
|||
|
||||
export type SaveOptions = { secure?: boolean };
|
||||
|
||||
interface StorageContextValue {
|
||||
export interface StorageContextValue {
|
||||
load<K extends keyof StorageSchema>(key: K): Promise<StorageSchema[K]>;
|
||||
save<K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
|
|
|
|||
68
packages/storage/src/credentials.ts
Normal file
68
packages/storage/src/credentials.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
import type { StorageContextValue } from "./context";
|
||||
|
||||
export function parseTuFileContent(rawFileContent: string): {
|
||||
userId: number;
|
||||
privateKey: string;
|
||||
domain: string | null;
|
||||
} {
|
||||
const content = rawFileContent.trim();
|
||||
const separator = content.indexOf("::");
|
||||
if (separator <= 0 || separator !== content.lastIndexOf("::")) {
|
||||
throw new Error("Invalid file");
|
||||
}
|
||||
|
||||
const identity = content.slice(0, separator);
|
||||
const privateKey = content.slice(separator + 2).trim();
|
||||
const [userIdValue, domain, ...extraDomainParts] = identity.split("@");
|
||||
const userId = Number(userIdValue);
|
||||
if (
|
||||
!Number.isSafeInteger(userId) ||
|
||||
userId <= 0 ||
|
||||
!privateKey ||
|
||||
extraDomainParts.length > 0 ||
|
||||
(identity.includes("@") && !domain)
|
||||
) {
|
||||
throw new Error("Invalid file");
|
||||
}
|
||||
|
||||
return { userId, privateKey, domain: domain ?? null };
|
||||
}
|
||||
|
||||
export async function persistMtpCredentials({
|
||||
storage,
|
||||
userId,
|
||||
keyring,
|
||||
domain,
|
||||
}: {
|
||||
storage: Pick<StorageContextValue, "load" | "save">;
|
||||
userId: number;
|
||||
keyring: string;
|
||||
domain?: string | null;
|
||||
}) {
|
||||
const omegaUrl = domain
|
||||
? `https://${domain}/`
|
||||
: await storage.load("omega_url");
|
||||
if (domain) await storage.save("omega_url", omegaUrl);
|
||||
|
||||
if (isTauri()) {
|
||||
const [forcedOmikronUrl, forcedOmikronPublicKey] = await Promise.all([
|
||||
storage.load("forced_omikron_url"),
|
||||
storage.load("forced_omikron_public_key"),
|
||||
]);
|
||||
await invoke("mtp_store_credentials", {
|
||||
config: {
|
||||
userId,
|
||||
keyring,
|
||||
omegaUrl,
|
||||
forcedOmikronUrl,
|
||||
forcedOmikronPublicKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await storage.save("mtp_keyring", keyring, { secure: true });
|
||||
await storage.save("session_id", Date.now());
|
||||
await storage.save("user_id", userId);
|
||||
}
|
||||
|
|
@ -4,10 +4,7 @@ import { getDatabaseEntry, setDatabaseEntry } from "@tensamin/shared/indexedDb";
|
|||
|
||||
export type SecureStorageStatus = {
|
||||
backend:
|
||||
| "electron-keyring"
|
||||
| "application-storage"
|
||||
| "webcrypto"
|
||||
| "indexeddb";
|
||||
"electron-keyring" | "application-storage" | "webcrypto" | "indexeddb";
|
||||
secure: boolean;
|
||||
reason?: string;
|
||||
};
|
||||
|
|
@ -71,7 +68,12 @@ async function getKey() {
|
|||
if (!globalThis.crypto?.subtle || typeof indexedDB === "undefined") {
|
||||
return null;
|
||||
}
|
||||
if (window.tensaminDesktop?.secureStorage) return loadElectronKey();
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
window.tensaminDesktop?.secureStorage
|
||||
) {
|
||||
return loadElectronKey();
|
||||
}
|
||||
return loadBrowserKey();
|
||||
})().catch(() => null);
|
||||
return keyPromise;
|
||||
|
|
@ -120,7 +122,10 @@ export async function decodeSecureValue(value: unknown): Promise<unknown> {
|
|||
}
|
||||
|
||||
export async function getSecureStorageStatus(): Promise<SecureStorageStatus> {
|
||||
const desktop = window.tensaminDesktop?.secureStorage;
|
||||
const desktop =
|
||||
typeof window === "undefined"
|
||||
? undefined
|
||||
: window.tensaminDesktop?.secureStorage;
|
||||
if (desktop?.getStatus) {
|
||||
const status = await desktop.getStatus();
|
||||
if (status.available) return { backend: "electron-keyring", secure: true };
|
||||
|
|
|
|||
Loading…
Reference in a new issue