diff --git a/Cargo.lock b/Cargo.lock index 4889bca6..41c1de96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,12 +78,6 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - [[package]] name = "bitflags" version = "1.3.2" @@ -117,12 +111,6 @@ version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.5.0" @@ -150,12 +138,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "contrast" version = "0.1.0" @@ -246,17 +228,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "der" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - [[package]] name = "derive_builder" version = "0.12.0" @@ -295,7 +266,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", ] @@ -638,9 +608,6 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin", -] [[package]] name = "libc" @@ -658,12 +625,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -711,7 +672,6 @@ name = "matrix-hookshot" version = "5.2.1" dependencies = [ "atom_syndication", - "base64ct", "contrast", "hex", "md-5", @@ -721,7 +681,6 @@ dependencies = [ "rand", "reqwest", "rgb", - "rsa", "rss", "ruma", "serde", @@ -862,43 +821,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.18" @@ -906,7 +828,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -1001,15 +922,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -1108,27 +1020,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.30" @@ -1302,26 +1193,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "rsa" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rss" version = "2.0.7" @@ -1579,16 +1450,6 @@ dependencies = [ "serde", ] -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - [[package]] name = "siphasher" version = "0.3.11" @@ -1620,22 +1481,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - [[package]] name = "string_cache" version = "0.8.7" @@ -1668,12 +1513,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - [[package]] name = "syn" version = "1.0.109" @@ -2207,9 +2046,3 @@ dependencies = [ "cfg-if", "windows-sys 0.48.0", ] - -[[package]] -name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index 0711159f..5475bb53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ atom_syndication = "0.12" ruma = { version = "0.9", features = ["events", "html"] } reqwest = "0.11" rand = "0.8.5" -rsa = "0.9.6" -base64ct = { version = "1.6.0", features = ["alloc"] } + [build-dependencies] napi-build = "2" diff --git a/changelog.d/915.misc b/changelog.d/915.misc deleted file mode 100644 index 7261adb4..00000000 --- a/changelog.d/915.misc +++ /dev/null @@ -1 +0,0 @@ -Track which key was used to encrypt secrets in storage, and encrypt/decrypt secrets in Rust. diff --git a/config.sample.yml b/config.sample.yml index 00201678..9c3aa289 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -10,7 +10,7 @@ bridge: passFile: # A passkey used to encrypt tokens stored inside the bridge. # Run openssl genpkey -out passkey.pem -outform PEM -algorithm RSA -pkeyopt rsa_keygen_bits:4096 to generate - ./passkey.pem + passkey.pem logging: # Logging settings. You can have a severity debug,info,warn,error level: info diff --git a/scripts/build-app.sh b/scripts/build-app.sh index 9b25f720..6a8fdce4 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # exit when any command fails set -e diff --git a/spec/basic.spec.ts b/spec/basic.spec.ts index e375a18a..ec36ddf0 100644 --- a/spec/basic.spec.ts +++ b/spec/basic.spec.ts @@ -19,12 +19,12 @@ describe('Basic test setup', () => { const user = testEnv.getUser('user'); const roomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid] }); await user.waitForRoomJoin({sender: testEnv.botMxid, roomId }); - const msg = user.waitForRoomEvent({ + await user.sendText(roomId, "!hookshot help"); + const msg = await user.waitForRoomEvent({ eventType: 'm.room.message', sender: testEnv.botMxid, roomId }); - await user.sendText(roomId, "!hookshot help"); // Expect help text. - expect((await msg).data.content.body).to.include('!hookshot help` - This help text\n'); + expect(msg.data.content.body).to.include('!hookshot help` - This help text\n'); }); // TODO: Move test to it's own generic connections file. diff --git a/src/AdminRoom.ts b/src/AdminRoom.ts index 69155163..efa2c130 100644 --- a/src/AdminRoom.ts +++ b/src/AdminRoom.ts @@ -16,7 +16,7 @@ import { Intent } from "matrix-bot-sdk"; import { JiraBotCommands } from "./jira/AdminCommands"; import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters"; import { ProjectsListResponseData } from "./github/Types"; -import { UserTokenStore } from "./tokens/UserTokenStore"; +import { UserTokenStore } from "./UserTokenStore"; import { Logger } from "matrix-appservice-bridge"; import markdown from "markdown-it"; type ProjectsListForRepoResponseData = Endpoints["GET /repos/{owner}/{repo}/projects"]["response"]; diff --git a/src/AdminRoomCommandHandler.ts b/src/AdminRoomCommandHandler.ts index 6840729e..10102e7e 100644 --- a/src/AdminRoomCommandHandler.ts +++ b/src/AdminRoomCommandHandler.ts @@ -1,7 +1,7 @@ import EventEmitter from "events"; import { Intent } from "matrix-bot-sdk"; import { BridgeConfig } from "./config/Config"; -import { UserTokenStore } from "./tokens/UserTokenStore"; +import { UserTokenStore } from "./UserTokenStore"; export enum Category { diff --git a/src/App/BridgeApp.ts b/src/App/BridgeApp.ts index 83f90957..1c40b23e 100644 --- a/src/App/BridgeApp.ts +++ b/src/App/BridgeApp.ts @@ -9,7 +9,6 @@ import { getAppservice } from "../appservice"; import BotUsersManager from "../Managers/BotUsersManager"; import * as Sentry from '@sentry/node'; import { GenericHookConnection } from "../Connections"; -import { UserTokenStore } from "../tokens/UserTokenStore"; Logger.configure({console: "info"}); const log = new Logger("App"); @@ -51,8 +50,7 @@ export async function start(config: BridgeConfig, registration: IAppserviceRegis const botUsersManager = new BotUsersManager(config, appservice); - const tokenStore = await UserTokenStore.fromKeyPath(config.passFile , appservice.botIntent, config); - const bridgeApp = new Bridge(config, tokenStore, listener, appservice, storage, botUsersManager); + const bridgeApp = new Bridge(config, listener, appservice, storage, botUsersManager); process.once("SIGTERM", () => { log.error("Got SIGTERM"); diff --git a/src/Bridge.ts b/src/Bridge.ts index 311bb827..c9123b4d 100644 --- a/src/Bridge.ts +++ b/src/Bridge.ts @@ -23,7 +23,7 @@ import { NotificationsEnableEvent, NotificationsDisableEvent, Webhooks } from ". import { GitHubOAuthToken, GitHubOAuthTokenResponse, ProjectsGetResponseData } from "./github/Types"; import { retry } from "./PromiseUtil"; import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher"; -import { UserTokenStore } from "./tokens/UserTokenStore"; +import { UserTokenStore } from "./UserTokenStore"; import * as GitHubWebhookTypes from "@octokit/webhooks-types"; import { Logger } from "matrix-appservice-bridge"; import { Provisioner } from "./provisioning/provisioner"; @@ -51,9 +51,11 @@ export class Bridge { private readonly queue: MessageQueue; private readonly commentProcessor: CommentProcessor; private readonly notifProcessor: NotificationProcessor; + private readonly tokenStore: UserTokenStore; private connectionManager?: ConnectionManager; private github?: GithubInstance; private adminRooms: Map = new Map(); + private widgetApi?: BridgeWidgetApi; private feedReader?: FeedReader; private houndReader?: HoundReader; private provisioningApi?: Provisioner; @@ -63,7 +65,6 @@ export class Bridge { constructor( private config: BridgeConfig, - private readonly tokenStore: UserTokenStore, private readonly listener: ListenerService, private readonly as: Appservice, private readonly storage: IBridgeStorageProvider, @@ -73,6 +74,8 @@ export class Bridge { this.messageClient = new MessageSenderClient(this.queue); this.commentProcessor = new CommentProcessor(this.as, this.config.bridge.mediaUrl || this.config.bridge.url); this.notifProcessor = new NotificationProcessor(this.storage, this.messageClient); + this.tokenStore = new UserTokenStore(this.config.passFile || "./passkey.pem", this.as.botIntent, this.config); + this.tokenStore.on("onNewToken", this.onTokenUpdated.bind(this)); // Legacy routes, to be removed. this.as.expressAppInstance.get("/live", (_, res) => res.send({ok: true})); @@ -88,8 +91,8 @@ export class Bridge { } public async start() { - this.tokenStore.on("onNewToken", this.onTokenUpdated.bind(this)); log.info('Starting up'); + await this.tokenStore.load(); await this.storage.connect?.(); await this.queue.connect?.(); @@ -762,7 +765,7 @@ export class Bridge { if (apps.length > 1) { throw Error('You may only bind `widgets` to one listener.'); } - new BridgeWidgetApi( + this.widgetApi = new BridgeWidgetApi( this.adminRooms, this.config, this.storage, diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts index 5f8508c8..75065b99 100644 --- a/src/ConnectionManager.ts +++ b/src/ConnectionManager.ts @@ -18,7 +18,7 @@ import { IBridgeStorageProvider } from "./Stores/StorageProvider"; import { JiraProject, JiraVersion } from "./jira/Types"; import { Logger } from "matrix-appservice-bridge"; import { MessageSenderClient } from "./MatrixSender"; -import { UserTokenStore } from "./tokens/UserTokenStore"; +import { UserTokenStore } from "./UserTokenStore"; import BotUsersManager from "./Managers/BotUsersManager"; import { retry, retryMatrixErrorFilter } from "./PromiseUtil"; import Metrics from "./Metrics"; diff --git a/src/Connections/GithubDiscussion.ts b/src/Connections/GithubDiscussion.ts index d3787d1d..5529118c 100644 --- a/src/Connections/GithubDiscussion.ts +++ b/src/Connections/GithubDiscussion.ts @@ -1,6 +1,6 @@ import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection"; import { Appservice, Intent, StateEvent } from "matrix-bot-sdk"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import { CommentProcessor } from "../CommentProcessor"; import { MessageSenderClient } from "../MatrixSender"; import { ensureUserIsInRoom, getIntentForUser } from "../IntentUtils"; diff --git a/src/Connections/GithubIssue.ts b/src/Connections/GithubIssue.ts index 4c0a728d..449349d4 100644 --- a/src/Connections/GithubIssue.ts +++ b/src/Connections/GithubIssue.ts @@ -2,7 +2,7 @@ import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnectio import { Appservice, Intent, StateEvent } from "matrix-bot-sdk"; import { MatrixMessageContent, MatrixEvent } from "../MatrixEvent"; import markdown from "markdown-it"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import { Logger } from "matrix-appservice-bridge"; import { CommentProcessor } from "../CommentProcessor"; import { MessageSenderClient } from "../MatrixSender"; diff --git a/src/Connections/GithubRepo.ts b/src/Connections/GithubRepo.ts index dc4e452d..6e77191d 100644 --- a/src/Connections/GithubRepo.ts +++ b/src/Connections/GithubRepo.ts @@ -13,7 +13,7 @@ import { MatrixMessageContent, MatrixEvent, MatrixReactionContent } from "../Mat import { MessageSenderClient } from "../MatrixSender"; import { CommandError, NotLoggedInError } from "../errors"; import { ReposGetResponseData } from "../github/Types"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import axios, { AxiosError } from "axios"; import { emojify } from "node-emoji"; import { Logger } from "matrix-appservice-bridge"; diff --git a/src/Connections/GitlabIssue.ts b/src/Connections/GitlabIssue.ts index 7d2df87d..6a19cdd6 100644 --- a/src/Connections/GitlabIssue.ts +++ b/src/Connections/GitlabIssue.ts @@ -1,7 +1,7 @@ import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection"; import { Appservice, Intent, StateEvent } from "matrix-bot-sdk"; import { MatrixMessageContent, MatrixEvent } from "../MatrixEvent"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import { Logger } from "matrix-appservice-bridge"; import { CommentProcessor } from "../CommentProcessor"; import { MessageSenderClient } from "../MatrixSender"; diff --git a/src/Connections/GitlabRepo.ts b/src/Connections/GitlabRepo.ts index 29e7f3e1..c3936a4c 100644 --- a/src/Connections/GitlabRepo.ts +++ b/src/Connections/GitlabRepo.ts @@ -1,4 +1,4 @@ -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import { Appservice, Intent, StateEvent } from "matrix-bot-sdk"; import { BotCommands, botCommand, compileBotCommands } from "../BotCommands"; import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent"; diff --git a/src/Connections/IConnection.ts b/src/Connections/IConnection.ts index c01c8878..9fe63a9a 100644 --- a/src/Connections/IConnection.ts +++ b/src/Connections/IConnection.ts @@ -3,7 +3,7 @@ import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types"; import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api"; import { Appservice, Intent, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk"; import { BridgeConfig, BridgePermissionLevel } from "../config/Config"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import { CommentProcessor } from "../CommentProcessor"; import { MessageSenderClient } from "../MatrixSender"; import { IBridgeStorageProvider } from "../Stores/StorageProvider"; diff --git a/src/Connections/JiraProject.ts b/src/Connections/JiraProject.ts index 05aabec1..a1aa2acd 100644 --- a/src/Connections/JiraProject.ts +++ b/src/Connections/JiraProject.ts @@ -9,7 +9,7 @@ import { JiraProject, JiraVersion } from "../jira/Types"; import { botCommand, BotCommands, compileBotCommands } from "../BotCommands"; import { MatrixMessageContent } from "../MatrixEvent"; import { CommandConnection } from "./CommandConnection"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import { CommandError, NotLoggedInError } from "../errors"; import { ApiError, ErrCode } from "../api"; import JiraApi from "jira-client"; diff --git a/src/Gitlab/GrantChecker.ts b/src/Gitlab/GrantChecker.ts index de759fc6..66bd7bea 100644 --- a/src/Gitlab/GrantChecker.ts +++ b/src/Gitlab/GrantChecker.ts @@ -3,7 +3,7 @@ import { Appservice } from "matrix-bot-sdk"; import { BridgeConfigGitLab } from "../config/Config"; import { GitLabRepoConnection } from "../Connections"; import { GrantChecker } from "../grants/GrantCheck"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; const log = new Logger('GitLabGrantChecker'); diff --git a/src/tokens/UserTokenStore.ts b/src/UserTokenStore.ts similarity index 83% rename from src/tokens/UserTokenStore.ts rename to src/UserTokenStore.ts index 7ea2ce3b..afd6520d 100644 --- a/src/tokens/UserTokenStore.ts +++ b/src/UserTokenStore.ts @@ -1,22 +1,22 @@ -import { GithubInstance } from "../github/GithubInstance"; -import { GitLabClient } from "../Gitlab/Client"; +import { GithubInstance } from "./github/GithubInstance"; +import { GitLabClient } from "./Gitlab/Client"; import { Intent } from "matrix-bot-sdk"; import { promises as fs } from "fs"; +import { publicEncrypt, privateDecrypt } from "crypto"; import { Logger } from "matrix-appservice-bridge"; -import { isJiraCloudInstance, JiraClient } from "../jira/Client"; -import { JiraStoredToken } from "../jira/Types"; -import { BridgeConfig, BridgeConfigJira, BridgeConfigJiraOnPremOAuth, BridgePermissionLevel } from "../config/Config"; +import { isJiraCloudInstance, JiraClient } from "./jira/Client"; +import { JiraStoredToken } from "./jira/Types"; +import { BridgeConfig, BridgeConfigJira, BridgeConfigJiraOnPremOAuth, BridgePermissionLevel } from "./config/Config"; import { randomUUID } from 'node:crypto'; -import { GitHubOAuthToken } from "../github/Types"; -import { ApiError, ErrCode } from "../api"; -import { JiraOAuth } from "../jira/OAuth"; -import { JiraCloudOAuth } from "../jira/oauth/CloudOAuth"; -import { JiraOnPremOAuth } from "../jira/oauth/OnPremOAuth"; -import { JiraOnPremClient } from "../jira/client/OnPremClient"; -import { JiraCloudClient } from "../jira/client/CloudClient"; -import { TokenError, TokenErrorCode } from "../errors"; +import { GitHubOAuthToken } from "./github/Types"; +import { ApiError, ErrCode } from "./api"; +import { JiraOAuth } from "./jira/OAuth"; +import { JiraCloudOAuth } from "./jira/oauth/CloudOAuth"; +import { JiraOnPremOAuth } from "./jira/oauth/OnPremOAuth"; +import { JiraOnPremClient } from "./jira/client/OnPremClient"; +import { JiraCloudClient } from "./jira/client/CloudClient"; +import { TokenError, TokenErrorCode } from "./errors"; import { TypedEmitter } from "tiny-typed-emitter"; -import { hashId, TokenEncryption } from "../libRs"; const ACCOUNT_DATA_TYPE = "uk.half-shot.matrix-hookshot.github.password-store:"; const ACCOUNT_DATA_GITLAB_TYPE = "uk.half-shot.matrix-hookshot.gitlab.password-store:"; @@ -31,8 +31,6 @@ export const AllowedTokenTypes = ["github", "gitlab", "jira"]; interface StoredTokenData { encrypted: string|string[]; - keyId: string; - algorithm: 'rsa'; instance?: string; } @@ -53,29 +51,20 @@ function tokenKey(type: TokenType, userId: string, legacy = false, instanceUrl?: return `${legacy ? LEGACY_ACCOUNT_DATA_GITLAB_TYPE : ACCOUNT_DATA_GITLAB_TYPE}${instanceUrl}${userId}`; } +const MAX_TOKEN_PART_SIZE = 128; const OAUTH_TIMEOUT_MS = 1000 * 60 * 30; interface Emitter { onNewToken: (type: TokenType, userId: string, token: string, instanceUrl?: string) => void, } export class UserTokenStore extends TypedEmitter { - - public static async fromKeyPath(keyPath: string, intent: Intent, config: BridgeConfig) { - log.info(`Loading token key file ${keyPath}`); - const key = await fs.readFile(keyPath); - return new UserTokenStore(key, intent, config); - } - + private key!: Buffer; private oauthSessionStore: Map = new Map(); private userTokens: Map; public readonly jiraOAuth?: JiraOAuth; - private tokenEncryption: TokenEncryption; - private readonly keyId: string; - constructor(key: Buffer, private readonly intent: Intent, private readonly config: BridgeConfig) { + constructor(private keyPath: string, private intent: Intent, private config: BridgeConfig) { super(); - this.tokenEncryption = new TokenEncryption(key); this.userTokens = new Map(); - this.keyId = hashId(key.toString('utf-8')); if (config.jira?.oauth) { if ("client_id" in config.jira.oauth) { this.jiraOAuth = new JiraCloudOAuth(config.jira.oauth); @@ -87,6 +76,11 @@ export class UserTokenStore extends TypedEmitter { } } + public async load() { + log.info(`Loading token key file ${this.keyPath}`); + this.key = await fs.readFile(this.keyPath); + } + public stop() { for (const session of this.oauthSessionStore.values()) { clearTimeout(session.timeout); @@ -98,16 +92,21 @@ export class UserTokenStore extends TypedEmitter { throw new ApiError('User does not have permission to log in to service', ErrCode.ForbiddenUser); } const key = tokenKey(type, userId, false, instanceUrl); - const tokenParts: string[] = this.tokenEncryption.encrypt(token); + const tokenParts: string[] = []; + let tokenSource = token; + while (tokenSource && tokenSource.length > 0) { + const part = tokenSource.slice(0, MAX_TOKEN_PART_SIZE); + tokenSource = tokenSource.substring(MAX_TOKEN_PART_SIZE); + tokenParts.push(publicEncrypt(this.key, Buffer.from(part)).toString("base64")); + } const data: StoredTokenData = { encrypted: tokenParts, - keyId: this.keyId, - algorithm: "rsa", instance: instanceUrl, }; await this.intent.underlyingClient.setAccountData(key, data); this.userTokens.set(key, token); log.info(`Stored new ${type} token for ${userId}`); + log.debug(`Stored`, data); this.emit("onNewToken", type, userId, token, instanceUrl); } @@ -147,19 +146,8 @@ export class UserTokenStore extends TypedEmitter { if (!obj || "deleted" in obj) { return null; } - // For legacy we just assume it's the current configured key. - const algorithm = obj.algorithm ?? "rsa"; - const keyId = obj.keyId ?? this.keyId; - - if (algorithm !== 'rsa') { - throw new Error(`Algorithm for stored data is '${algorithm}', but we only support RSA`); - } - if (keyId !== this.keyId) { - throw new Error(`Stored data was encrypted with a different key to the one currently configured`); - } - const encryptedParts = typeof obj.encrypted === "string" ? [obj.encrypted] : obj.encrypted; - const token = this.tokenEncryption.decrypt(encryptedParts); + const token = encryptedParts.map((t) => privateDecrypt(this.key, Buffer.from(t, "base64")).toString("utf-8")).join(""); this.userTokens.set(key, token); return token; } catch (ex) { diff --git a/src/Widgets/BridgeWidgetApi.ts b/src/Widgets/BridgeWidgetApi.ts index 0962d5d0..38555789 100644 --- a/src/Widgets/BridgeWidgetApi.ts +++ b/src/Widgets/BridgeWidgetApi.ts @@ -11,7 +11,7 @@ import BotUsersManager, {BotUser} from "../Managers/BotUsersManager"; import { assertUserPermissionsInRoom, GetConnectionsResponseItem } from "../provisioning/api"; import { Appservice, PowerLevelsEvent } from "matrix-bot-sdk"; import { GithubInstance } from '../github/GithubInstance'; -import { AllowedTokenTypes, TokenType, UserTokenStore } from '../tokens/UserTokenStore'; +import { AllowedTokenTypes, TokenType, UserTokenStore } from '../UserTokenStore'; const log = new Logger("BridgeWidgetApi"); diff --git a/src/config/Config.ts b/src/config/Config.ts index 83288f97..50c6c55b 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -560,7 +560,7 @@ export class BridgeConfig { this.generic = configData.generic && new BridgeConfigGenericWebhooks(configData.generic); this.feeds = configData.feeds && new BridgeConfigFeeds(configData.feeds); this.provisioning = configData.provisioning; - this.passFile = configData.passFile ?? "./passkey.pem"; + this.passFile = configData.passFile; this.bot = configData.bot; this.serviceBots = configData.serviceBots; this.metrics = configData.metrics; diff --git a/src/config/Defaults.ts b/src/config/Defaults.ts index 2ea9e1f5..190f47b7 100644 --- a/src/config/Defaults.ts +++ b/src/config/Defaults.ts @@ -34,7 +34,7 @@ export const DefaultConfigRoot: BridgeConfigRoot = { level: "admin" }], }], - passFile: "./passkey.pem", + passFile: "passkey.pem", widgets: { publicUrl: `${hookshotWebhooksUrl}/widgetapi/v1/static`, addToAdminRooms: false, diff --git a/src/github/GrantChecker.ts b/src/github/GrantChecker.ts index e3eb27ee..a1497f30 100644 --- a/src/github/GrantChecker.ts +++ b/src/github/GrantChecker.ts @@ -1,7 +1,7 @@ import { Appservice } from "matrix-bot-sdk"; import { GitHubRepoConnection } from "../Connections"; import { GrantChecker } from "../grants/GrantCheck"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import { Logger } from 'matrix-appservice-bridge'; const log = new Logger('GitHubGrantChecker'); diff --git a/src/github/Router.ts b/src/github/Router.ts index fa05c57b..94ba2ce9 100644 --- a/src/github/Router.ts +++ b/src/github/Router.ts @@ -1,7 +1,7 @@ import { Router, Request, Response, NextFunction } from "express"; import { BridgeConfigGitHub } from "../config/Config"; import { ApiError, ErrCode } from "../api"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import { Logger } from "matrix-appservice-bridge"; import { GithubInstance } from "./GithubInstance"; import { NAMELESS_ORG_PLACEHOLDER } from "./Types"; diff --git a/src/jira/GrantChecker.ts b/src/jira/GrantChecker.ts index 1fc19b14..211fc821 100644 --- a/src/jira/GrantChecker.ts +++ b/src/jira/GrantChecker.ts @@ -1,7 +1,7 @@ import { Appservice } from "matrix-bot-sdk"; import { JiraProjectConnection } from "../Connections"; import { GrantChecker } from "../grants/GrantCheck"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; interface JiraGrantConnectionId{ url: string; diff --git a/src/jira/Router.ts b/src/jira/Router.ts index 7771e5f7..2bd6815b 100644 --- a/src/jira/Router.ts +++ b/src/jira/Router.ts @@ -1,7 +1,7 @@ import { BridgeConfigJira } from "../config/Config"; import { MessageQueue } from "../MessageQueue"; import { Router, Request, Response, NextFunction, json } from "express"; -import { UserTokenStore } from "../tokens/UserTokenStore"; +import { UserTokenStore } from "../UserTokenStore"; import { Logger } from "matrix-appservice-bridge"; import { ApiError, ErrCode } from "../api"; import { JiraOAuthRequestCloud, JiraOAuthRequestOnPrem, JiraOAuthRequestResult } from "./OAuth"; diff --git a/src/lib.rs b/src/lib.rs index 4e4e1bfe..3a15bd65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ pub mod feeds; pub mod format_util; pub mod github; pub mod jira; -pub mod tokens; pub mod util; #[macro_use] diff --git a/src/tokens/mod.rs b/src/tokens/mod.rs deleted file mode 100644 index 6aa0cc17..00000000 --- a/src/tokens/mod.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::string::FromUtf8Error; - -use base64ct::{Base64, Encoding}; -use napi::bindgen_prelude::Buffer; -use napi::Error; -use rsa::pkcs8::DecodePrivateKey; -use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey}; - -static MAX_TOKEN_PART_SIZE: usize = 128; - -struct TokenEncryption { - pub private_key: RsaPrivateKey, - pub public_key: RsaPublicKey, -} - -#[derive(Debug)] -#[allow(dead_code)] -enum TokenEncryptionError { - FromUtf8(FromUtf8Error), - PrivateKey(rsa::pkcs8::Error), -} - -#[derive(Debug)] -#[allow(dead_code)] -enum DecryptError { - Base64(base64ct::Error), - Decryption(rsa::Error), - FromUtf8(FromUtf8Error), -} - -impl TokenEncryption { - pub fn new(private_key_data: Vec) -> Result { - let data = String::from_utf8(private_key_data).map_err(TokenEncryptionError::FromUtf8)?; - let private_key = RsaPrivateKey::from_pkcs8_pem(data.as_str()) - .map_err(TokenEncryptionError::PrivateKey)?; - let public_key = private_key.to_public_key(); - Ok(TokenEncryption { - private_key, - public_key, - }) - } -} - -#[napi(js_name = "TokenEncryption")] -pub struct JsTokenEncryption { - inner: TokenEncryption, -} - -#[napi] -impl JsTokenEncryption { - #[napi(constructor)] - pub fn new(private_key_data: Buffer) -> Result { - let buf: Vec = private_key_data.into(); - match TokenEncryption::new(buf) { - Ok(inner) => Ok(JsTokenEncryption { inner }), - Err(err) => Err(Error::new( - napi::Status::GenericFailure, - format!("Error reading private key: {:?}", err).to_string(), - )), - } - } - - #[napi] - pub fn decrypt(&self, parts: Vec) -> Result { - let mut result = String::new(); - - for v in parts { - match self.decrypt_value(v) { - Ok(new_value) => { - result += &new_value; - Ok(()) - } - Err(err) => Err(Error::new( - napi::Status::GenericFailure, - format!("Could not decrypt string: {:?}", err).to_string(), - )), - }? - } - Ok(result) - } - - fn decrypt_value(&self, value: String) -> Result { - let raw_value = Base64::decode_vec(&value).map_err(DecryptError::Base64)?; - let decrypted_value = self - .inner - .private_key - .decrypt(Pkcs1v15Encrypt, &raw_value) - .map_err(DecryptError::Decryption)?; - let utf8_value = String::from_utf8(decrypted_value).map_err(DecryptError::FromUtf8)?; - Ok(utf8_value) - } - - #[napi] - pub fn encrypt(&self, input: String) -> Result, Error> { - let mut rng = rand::thread_rng(); - let mut parts: Vec = Vec::new(); - for part in input.into_bytes().chunks(MAX_TOKEN_PART_SIZE) { - match self - .inner - .public_key - .encrypt(&mut rng, Pkcs1v15Encrypt, part) - { - Ok(encrypted) => { - let b64 = Base64::encode_string(encrypted.as_slice()); - parts.push(b64); - Ok(()) - } - Err(err) => Err(Error::new( - napi::Status::GenericFailure, - format!("Could not encrypt string: {:?}", err).to_string(), - )), - }? - } - Ok(parts) - } -} diff --git a/tests/AdminRoomTest.ts b/tests/AdminRoomTest.ts index 658c0cfc..ee171e17 100644 --- a/tests/AdminRoomTest.ts +++ b/tests/AdminRoomTest.ts @@ -4,7 +4,7 @@ import { AdminRoom } from "../src/AdminRoom"; import { DefaultConfig } from "../src/config/Defaults"; import { ConnectionManager } from "../src/ConnectionManager"; import { NotifFilter } from "../src/NotificationFilters"; -import { UserTokenStore } from "../src/tokens/UserTokenStore"; +import { UserTokenStore } from "../src/UserTokenStore"; import { IntentMock } from "./utils/IntentMock"; const ROOM_ID = "!foo:bar"; @@ -14,8 +14,9 @@ function createAdminRoom(data: any = {admin_user: "@admin:bar"}): [AdminRoom, In if (!data.admin_user) { data.admin_user = "@admin:bar"; } - return [new AdminRoom(ROOM_ID, data, NotifFilter.getDefaultContent(), intent, {} as UserTokenStore, DefaultConfig, {} as ConnectionManager), intent]; -} + const tokenStore = new UserTokenStore("notapath", intent, DefaultConfig); + return [new AdminRoom(ROOM_ID, data, NotifFilter.getDefaultContent(), intent, tokenStore, DefaultConfig, {} as ConnectionManager), intent]; +} describe("AdminRoom", () => { it("will present help text", async () => { diff --git a/tests/connections/GithubRepoTest.ts b/tests/connections/GithubRepoTest.ts index 7359e55b..8c1728d4 100644 --- a/tests/connections/GithubRepoTest.ts +++ b/tests/connections/GithubRepoTest.ts @@ -1,7 +1,7 @@ import { GitHubRepoConnection, GitHubRepoConnectionState } from "../../src/Connections/GithubRepo" import { GithubInstance } from "../../src/github/GithubInstance"; import { createMessageQueue } from "../../src/MessageQueue"; -import { UserTokenStore } from "../../src/tokens/UserTokenStore"; +import { UserTokenStore } from "../../src/UserTokenStore"; import { DefaultConfig } from "../../src/config/Defaults"; import { AppserviceMock } from "../utils/AppserviceMock"; import { ApiError, ErrCode, ValidatorApiError } from "../../src/api"; diff --git a/tests/connections/GitlabRepoTest.ts b/tests/connections/GitlabRepoTest.ts index af438e61..b2376fe2 100644 --- a/tests/connections/GitlabRepoTest.ts +++ b/tests/connections/GitlabRepoTest.ts @@ -1,5 +1,5 @@ import { createMessageQueue } from "../../src/MessageQueue"; -import { UserTokenStore } from "../../src/tokens/UserTokenStore"; +import { UserTokenStore } from "../../src/UserTokenStore"; import { AppserviceMock } from "../utils/AppserviceMock"; import { ApiError, ErrCode, ValidatorApiError } from "../../src/api"; import { GitLabRepoConnection, GitLabRepoConnectionState } from "../../src/Connections"; diff --git a/tests/tokens/tokenencryption.spec.ts b/tests/tokens/tokenencryption.spec.ts deleted file mode 100644 index bd969447..00000000 --- a/tests/tokens/tokenencryption.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { TokenEncryption } from "../../src/libRs"; -import { RSAKeyPairOptions, generateKeyPair } from "node:crypto"; -import { expect } from "chai"; - -describe("TokenEncryption", () => { - let keyPromise: Promise; - async function createTokenEncryption() { - return new TokenEncryption(await keyPromise); - } - - before('generate RSA key', () => { - // Generate this once since it will take an age. - keyPromise = new Promise((resolve, reject) => generateKeyPair("rsa", { - // Deliberately shorter length to speed up test - modulusLength: 2048, - privateKeyEncoding: { - type: "pkcs8", - format: "pem", - }, - publicKeyEncoding: { - format: "pem", - type: "pkcs1", - } - } satisfies RSAKeyPairOptions<"pem", "pem">, (err, _, privateKey) => { - if (err) { reject(err) } else { resolve(Buffer.from(privateKey)) } - })); - }, ); - it('should be able to encrypt a string into a single part', async() => { - const tokenEncryption = await createTokenEncryption(); - const result = tokenEncryption.encrypt('hello world'); - expect(result).to.have.lengthOf(1); - }); - it('should be able to decrypt from a single part into a string', async() => { - const tokenEncryption = await createTokenEncryption(); - const value = tokenEncryption.encrypt('hello world'); - const result = tokenEncryption.decrypt(value); - expect(result).to.equal('hello world'); - }); - it('should be able to decrypt from many parts into string', async() => { - const plaintext = 'This is a very long string that needs to be encoded into multiple parts in order for us to store it properly. This ' + - ' should end up as multiple encrypted values in base64.'; - const tokenEncryption = await createTokenEncryption(); - const value = tokenEncryption.encrypt(plaintext); - expect(value).to.have.lengthOf(2); - const result = tokenEncryption.decrypt(value); - expect(result).to.equal(plaintext); - }); -});