Skip to content

Commit

Permalink
Merge pull request #1244 from buttercup/feat/browser_api
Browse files Browse the repository at this point in the history
Browser extension (v3) API
  • Loading branch information
perry-mitchell authored Nov 3, 2023
2 parents 092d352 + dd13b60 commit cd5d487
Show file tree
Hide file tree
Showing 39 changed files with 5,436 additions and 3,250 deletions.
7,628 changes: 4,447 additions & 3,181 deletions package-lock.json

Large diffs are not rendered by default.

69 changes: 39 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
"main": "build/main/index.js",
"scripts": {
"build": "run-s clean build:app",
"build:app": "webpack --config webpack.config.js --mode production",
"build:app": "npm run set-version && webpack --config webpack.config.js --mode production",
"clean": "rimraf build dist",
"format": "prettier --write '{{resources,source,test}/**/*.{js,ts},webpack.config.js}'",
"package": "electron-builder --mac --win --linux -p always",
"package:linux": "electron-builder --linux",
"package:mac": "electron-builder --mac",
"package:win": "electron-builder --win",
"release": "run-s build package",
"set-version": "node ./resources/scripts/set-version.js",
"start": "npm run start:main",
"start:build": "npm run clean && webpack --mode development --watch",
"start:build": "npm run clean && npm run set-version && webpack --mode development --watch",
"start:main": "electron .",
"test": "run-s build test:specs test:format",
"test:format": "prettier --check '{{resources,source,test}/**/*.{js,ts},webpack.config.js}'",
Expand Down Expand Up @@ -186,15 +187,15 @@
},
"dependencies": {
"@buttercup/channel-queue": "^1.2.0",
"@buttercup/dropbox-client": "^2.1.1",
"@buttercup/dropbox-client": "^2.1.2",
"@buttercup/exporter": "^1.1.0",
"@buttercup/file-interface": "^3.0.0",
"@buttercup/google-oauth2-client": "^2.1.1",
"@buttercup/googledrive-client": "^2.2.1",
"@buttercup/file-interface": "^3.0.1",
"@buttercup/google-oauth2-client": "^2.1.2",
"@buttercup/googledrive-client": "^2.2.2",
"@buttercup/importer": "^3.1.0",
"@buttercup/secure-file-host": "^0.3.0",
"@electron/remote": "^2.0.8",
"buttercup": "^7.2.3",
"buttercup": "^7.2.4",
"debounce": "^1.2.1",
"debounce-promise": "^3.1.2",
"delayable-setinterval": "^0.1.1",
Expand All @@ -203,32 +204,38 @@
"env-paths": "^2.2.1",
"eventemitter3": "^4.0.7",
"execution-time": "^1.4.1",
"i18next": "^21.9.1",
"express": "^4.18.2",
"express-promise-router": "^4.1.1",
"i18next": "^23.6.0",
"iocane": "^5.1.1",
"keytar": "^7.9.0",
"layerr": "^2.0.0",
"layerr": "^2.0.1",
"log-rotate": "^0.2.8",
"ms": "^2.1.3",
"nested-property": "^4.0.0",
"os-locale": "^5.0.0",
"pify": "^5.0.0",
"stacktracey": "^2.1.7",
"webdav": "^5.0.0-r4"
"statuses": "^2.0.1",
"webdav": "^5.3.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@babel/core": "^7.21.4",
"@babel/preset-env": "^7.21.4",
"@babel/preset-typescript": "^7.21.4",
"@blueprintjs/core": "^4.13.0",
"@blueprintjs/popover2": "^1.10.2",
"@blueprintjs/select": "^4.8.12",
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.23.2",
"@babel/preset-typescript": "^7.23.2",
"@blueprintjs/core": "^4.20.2",
"@blueprintjs/popover2": "^1.14.11",
"@blueprintjs/select": "^4.9.24",
"@buttercup/ui": "^6.2.2",
"@hookstate/core": "^3.0.13",
"@types/jest": "^29.5.0",
"@types/node": "^18.11.17",
"@types/express": "^4.17.20",
"@types/jest": "^29.5.7",
"@types/node": "^20.8.10",
"@types/react": "^17.0.52",
"@types/react-dom": "^17.0.18",
"@types/styled-components": "^5.1.26",
"babel-jest": "^29.5.0",
"@types/styled-components": "^5.1.29",
"babel-jest": "^29.7.0",
"babel-loader": "^9.1.2",
"classnames": "^2.3.2",
"concurrently": "^6.3.0",
Expand All @@ -238,11 +245,12 @@
"electron-builder": "^23.6.0",
"electron-builder-notarize": "^1.5.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.2",
"html-webpack-plugin": "^5.5.3",
"husky": "^4.3.8",
"jest": "^29.5.0",
"jest": "^29.7.0",
"lint-staged": "^13.1.0",
"npm-run-all": "^4.1.5",
"obstate": "^0.1.4",
"path-posix": "^1.0.0",
"prettier": "^2.8.1",
"pretty-ms": "^7.0.1",
Expand All @@ -252,16 +260,17 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^17.0.2",
"react-obstate": "^0.1.3",
"react-router-dom": "^5.3.3",
"rimraf": "^3.0.2",
"sass": "^1.62.1",
"sass-loader": "^13.2.2",
"rimraf": "^5.0.5",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"spectron": "^15.0.0",
"style-loader": "^3.3.2",
"styled-components": "^5.3.6",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.82.1",
"webpack-cli": "^5.1.1"
"styled-components": "^6.1.0",
"ts-loader": "^9.5.0",
"typescript": "^5.2.2",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}
12 changes: 12 additions & 0 deletions resources/scripts/set-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const fs = require("node:fs");
const path = require("node:path");

const packageJson = require("../../package.json");

fs.writeFileSync(
path.resolve(__dirname, "../../source/main/library/build.ts"),
`// This file updated automatically: changes made here will be overwritten!
export const VERSION = "${packageJson.version}";
`
);
4 changes: 2 additions & 2 deletions source/main/actions/appMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async function getContextMenu(): Promise<Menu> {
const sources = getSourceDescriptions();
const lastSourceID = getLastSourceID();
const lastSource = sources.find((source) => source.id === lastSourceID) || null;
const preferences = await getConfigValue<Preferences>("preferences");
const preferences = await getConfigValue("preferences");
const currentVaultPrefix = [];
const biometricsSupported = await supportsBiometricUnlock();
let biometricsEnabled = false,
Expand Down Expand Up @@ -194,7 +194,7 @@ async function getContextMenu(): Promise<Menu> {
type: "checkbox",
checked: preferences.fileHostEnabled,
click: async () => {
const prefs = await getConfigValue<Preferences>("preferences");
const prefs = await getConfigValue("preferences");
prefs.fileHostEnabled = !prefs.fileHostEnabled;
await setConfigValue("preferences", prefs);
await updateAppMenu();
Expand Down
3 changes: 3 additions & 0 deletions source/main/actions/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getOSLocale } from "../services/locale";
import { changeLanguage } from "../../shared/i18n/trans";
import { getLanguage } from "../../shared/library/i18n";
import { startFileHost, stopFileHost } from "../services/fileHost";
import { start as startBrowserAPI, stop as stopBrowserAPI } from "../services/browser/index";
import { Preferences } from "../types";

export async function handleConfigUpdate(preferences: Preferences) {
Expand All @@ -18,8 +19,10 @@ export async function handleConfigUpdate(preferences: Preferences) {
logInfo(` - Auto clear clipboard: ${preferences.autoClearClipboard}s`);
logInfo(` - Lock vaults after: ${preferences.lockVaultsAfterTime}s`);
if (preferences.fileHostEnabled) {
await startBrowserAPI();
await startFileHost();
} else {
await stopBrowserAPI();
await stopFileHost();
}
}
9 changes: 7 additions & 2 deletions source/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { startAutoVaultLockTimer } from "./services/autoLock";
import { log as logRaw, logInfo, logErr } from "./library/log";
import { isPortable } from "./library/portability";
import { convertVaultFormat } from "./services/format";
import { clearCode } from "./services/browser/interaction";
import {
AppEnvironmentFlags,
AddVaultPayload,
Expand Down Expand Up @@ -98,7 +99,7 @@ ipcMain.on("get-empty-vault", async (evt, payload) => {
});

ipcMain.on("get-preferences", async (evt, sourceID) => {
const prefs = await getConfigValue<Preferences>("preferences");
const prefs = await getConfigValue("preferences");
evt.reply("get-preferences:reply", JSON.stringify(prefs));
});

Expand Down Expand Up @@ -196,6 +197,10 @@ ipcMain.handle(
}
);

ipcMain.handle("browser-access-code-clear", async (_) => {
await clearCode();
});

ipcMain.handle("check-source-biometrics", async (_, sourceID: VaultSourceID) => {
const supportsBiometrics = await supportsBiometricUnlock();
if (!supportsBiometrics) return false;
Expand Down Expand Up @@ -250,7 +255,7 @@ ipcMain.handle("get-new-vault-filename", async (evt) => {
ipcMain.handle("get-ready-update", getReadyUpdate);

ipcMain.handle("get-selected-source", async () => {
const sourceID = await getConfigValue<VaultSourceID>("selectedSource");
const sourceID = await getConfigValue("selectedSource");
return sourceID;
});

Expand Down
3 changes: 3 additions & 0 deletions source/main/library/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file updated automatically: changes made here will be overwritten!

export const VERSION = "2.20.3";
21 changes: 21 additions & 0 deletions source/main/library/otp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Entry, EntryPropertyValueType, EntryURLType, getEntryURLs, VaultSource } from "buttercup";
import { OTP } from "../types";

export function extractVaultOTPItems(source: VaultSource): Array<OTP> {
return source.vault.getAllEntries().reduce((output: Array<OTP>, entry: Entry) => {
const properties = entry.getProperties();
const loginURLs = getEntryURLs(properties, EntryURLType.Login);
for (const key in properties) {
if (entry.getPropertyValueType(key) !== EntryPropertyValueType.OTP) continue;
output.push({
sourceID: source.id,
entryID: entry.id,
entryProperty: key,
entryTitle: properties.title,
loginURL: loginURLs.length > 0 ? loginURLs[0] : null,
otpURL: properties[key]
});
}
return output;
}, []);
}
2 changes: 1 addition & 1 deletion source/main/services/autoClearClipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let lastCopiedText = "";

export async function restartAutoClearClipboardTimer(text: string) {
lastCopiedText = text;
const { autoClearClipboard } = await getConfigValue<Preferences>("preferences");
const { autoClearClipboard } = await getConfigValue("preferences");
if (!autoClearClipboard) {
return;
}
Expand Down
2 changes: 1 addition & 1 deletion source/main/services/autoLock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function setAutoLockEnabled(value: boolean) {
export async function startAutoVaultLockTimer() {
stopAutoVaultLockTimer();
if (!__autoLockEnabled) return;
const { lockVaultsAfterTime } = await getConfigValue<Preferences>("preferences");
const { lockVaultsAfterTime } = await getConfigValue("preferences");
if (!lockVaultsAfterTime) return;
__autoVaultLockTimeout = setTimeout(() => {
if (getUnlockedSourcesCount() === 0) return;
Expand Down
45 changes: 45 additions & 0 deletions source/main/services/browser/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import express, { Request, Response, NextFunction } from "express";
import createRouter from "express-promise-router";
import { VERSION } from "../../library/build";
import { handleAuthPing, processAuthRequest, processAuthResponse } from "./controllers/auth";
import { searchEntries } from "./controllers/entries";
import { getAllOTPs } from "./controllers/otp";
import { getVaults, getVaultsTree, promptVaultLock, promptVaultUnlock } from "./controllers/vaults";
import { handleError } from "./error";
import { requireClient, requireKeyAuth } from "./middleware";
import { saveExistingEntry, saveNewEntry } from "./controllers/save";

export function buildApplication(): express.Application {
const app = express();
app.disable("x-powered-by");
app.use(express.text());
app.use(express.json());
app.use((req: Request, res: Response, next: NextFunction) => {
res.set("Server", `ButtercupDesktop/${VERSION}`);
next();
});
createRoutes(app);
app.use(handleError);
return app;
}

function createRoutes(app: express.Application): void {
const router = createRouter();
router.post("/auth/request", processAuthRequest);
router.post("/auth/response", processAuthResponse);
router.post("/auth/test", requireClient, requireKeyAuth, handleAuthPing);
router.get("/entries", requireClient, searchEntries);
router.get("/otps", requireClient, getAllOTPs);
router.get("/vaults", requireClient, getVaults);
router.get("/vaults-tree", requireClient, getVaultsTree);
router.patch(
"/vaults/:id/group/:gid/entry/:eid",
requireClient,
requireKeyAuth,
saveExistingEntry
);
router.post("/vaults/:id/group/:gid/entry", requireClient, requireKeyAuth, saveNewEntry);
router.post("/vaults/:id/lock", requireClient, requireKeyAuth, promptVaultLock);
router.post("/vaults/:id/unlock", requireClient, requireKeyAuth, promptVaultUnlock);
app.use("/v1", router);
}
60 changes: 60 additions & 0 deletions source/main/services/browser/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Layerr } from "layerr";
import { decryptPayload, encryptPayload, getBrowserPublicKeyString } from "../browserAuth";
import { getConfigValue, setConfigValue } from "../config";
import { BrowserAPIErrorType } from "../../types";

export async function decryptAPIPayload(clientID: string, payload: string): Promise<string> {
// Check that the client is registered, we don't actually
// use their key for decryption..
const clients = await getConfigValue("browserClients");
const clientConfig = clients[clientID];
if (!clientConfig) {
throw new Layerr(
{
info: {
clientID,
code: BrowserAPIErrorType.NoAPIKey
}
},
"No client key registered for decryption"
);
}
// Private key for decryption
const browserPrivateKey = await getConfigValue("browserPrivateKey");
// Decrypt
return decryptPayload(payload, clientConfig.publicKey, browserPrivateKey);
}

export async function encryptAPIPayload(clientID: string, payload: string): Promise<string> {
// Check that the client is registered, we don't actually
// use their key for decryption..
const clients = await getConfigValue("browserClients");
const clientConfig = clients[clientID];
if (!clientConfig) {
throw new Layerr(
{
info: {
clientID,
code: BrowserAPIErrorType.NoAPIKey
}
},
"No client key registered for encryption"
);
}
// Private key for decryption
const browserPrivateKey = await getConfigValue("browserPrivateKey");
// Encrypt
return encryptPayload(payload, browserPrivateKey, clientConfig.publicKey);
}

export async function registerPublicKey(id: string, publicKey: string): Promise<string> {
const clients = await getConfigValue("browserClients");
await setConfigValue("browserClients", {
...clients,
[id]: {
publicKey
}
});
const serverPublicKey = await getBrowserPublicKeyString();
return serverPublicKey;
}
Loading

0 comments on commit cd5d487

Please sign in to comment.