mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 13:17:08 +00:00
Handle RSA token handling in Rust (#915)
* Split out queue and cache config * Update usages of cache config, * Update default * Cleanup * Make queue optional. * config updates. * changelog * update spec config * Update tests * tweak import * Update default config. * fixup test * Update config.sample.yml Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Signed-off-by: Will Hunt <will@half-shot.uk> * Update encryption.md Signed-off-by: Will Hunt <will@half-shot.uk> * Clear up worker config Signed-off-by: Will Hunt <will@half-shot.uk> * Update src/config/Config.ts Co-authored-by: Andrew Ferrazzutti <andrewf@element.io> Signed-off-by: Will Hunt <will@half-shot.uk> * update helm config * move UserTokenStore.ts * Port all the imports to new path. * Port RSA handling to rust. * Add tests. * linting * lint rust * Remove unwraps / panics * fix build script * Ensure we store and check with algorithm and key was used. * quieten false deadcode warnings * changelog * fix test imports * lazy mock out UTS * Refactor so that UserTokenStore is initiated by the time Bridge is created. * update defaults * replace if with match * Use the magic of ? * fmt --------- Signed-off-by: Will Hunt <will@half-shot.uk> Co-authored-by: Andrew Ferrazzutti <andrewf@element.io>
This commit is contained in:
parent
6618ab6db8
commit
6482c7ed55
167
Cargo.lock
generated
167
Cargo.lock
generated
@ -78,6 +78,12 @@ 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"
|
||||
@ -111,6 +117,12 @@ 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"
|
||||
@ -138,6 +150,12 @@ 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"
|
||||
@ -228,6 +246,17 @@ 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"
|
||||
@ -266,6 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
@ -608,6 +638,9 @@ name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
@ -625,6 +658,12 @@ 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"
|
||||
@ -672,6 +711,7 @@ name = "matrix-hookshot"
|
||||
version = "5.2.1"
|
||||
dependencies = [
|
||||
"atom_syndication",
|
||||
"base64ct",
|
||||
"contrast",
|
||||
"hex",
|
||||
"md-5",
|
||||
@ -681,6 +721,7 @@ dependencies = [
|
||||
"rand",
|
||||
"reqwest",
|
||||
"rgb",
|
||||
"rsa",
|
||||
"rss",
|
||||
"ruma",
|
||||
"serde",
|
||||
@ -821,6 +862,43 @@ 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"
|
||||
@ -828,6 +906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -922,6 +1001,15 @@ 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"
|
||||
@ -1020,6 +1108,27 @@ 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"
|
||||
@ -1193,6 +1302,26 @@ 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"
|
||||
@ -1450,6 +1579,16 @@ 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"
|
||||
@ -1481,6 +1620,22 @@ 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"
|
||||
@ -1513,6 +1668,12 @@ 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"
|
||||
@ -2046,3 +2207,9 @@ 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"
|
||||
|
@ -22,6 +22,7 @@ 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"
|
||||
|
1
changelog.d/915.misc
Normal file
1
changelog.d/915.misc
Normal file
@ -0,0 +1 @@
|
||||
Track which key was used to encrypt secrets in storage, and encrypt/decrypt secrets in Rust.
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# exit when any command fails
|
||||
set -e
|
||||
|
@ -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 });
|
||||
await user.sendText(roomId, "!hookshot help");
|
||||
const msg = await user.waitForRoomEvent<MessageEventContent>({
|
||||
const msg = user.waitForRoomEvent<MessageEventContent>({
|
||||
eventType: 'm.room.message', sender: testEnv.botMxid, roomId
|
||||
});
|
||||
await user.sendText(roomId, "!hookshot help");
|
||||
// Expect help text.
|
||||
expect(msg.data.content.body).to.include('!hookshot help` - This help text\n');
|
||||
expect((await msg).data.content.body).to.include('!hookshot help` - This help text\n');
|
||||
});
|
||||
|
||||
// TODO: Move test to it's own generic connections file.
|
||||
|
@ -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 "./UserTokenStore";
|
||||
import { UserTokenStore } from "./tokens/UserTokenStore";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import markdown from "markdown-it";
|
||||
type ProjectsListForRepoResponseData = Endpoints["GET /repos/{owner}/{repo}/projects"]["response"];
|
||||
|
@ -1,7 +1,7 @@
|
||||
import EventEmitter from "events";
|
||||
import { Intent } from "matrix-bot-sdk";
|
||||
import { BridgeConfig } from "./config/Config";
|
||||
import { UserTokenStore } from "./UserTokenStore";
|
||||
import { UserTokenStore } from "./tokens/UserTokenStore";
|
||||
|
||||
|
||||
export enum Category {
|
||||
|
@ -9,6 +9,7 @@ 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");
|
||||
@ -50,7 +51,8 @@ export async function start(config: BridgeConfig, registration: IAppserviceRegis
|
||||
|
||||
const botUsersManager = new BotUsersManager(config, appservice);
|
||||
|
||||
const bridgeApp = new Bridge(config, listener, appservice, storage, botUsersManager);
|
||||
const tokenStore = await UserTokenStore.fromKeyPath(config.passFile , appservice.botIntent, config);
|
||||
const bridgeApp = new Bridge(config, tokenStore, listener, appservice, storage, botUsersManager);
|
||||
|
||||
process.once("SIGTERM", () => {
|
||||
log.error("Got SIGTERM");
|
||||
|
@ -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 "./UserTokenStore";
|
||||
import { UserTokenStore } from "./tokens/UserTokenStore";
|
||||
import * as GitHubWebhookTypes from "@octokit/webhooks-types";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { Provisioner } from "./provisioning/provisioner";
|
||||
@ -49,11 +49,9 @@ 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<string, AdminRoom> = new Map();
|
||||
private widgetApi?: BridgeWidgetApi;
|
||||
private feedReader?: FeedReader;
|
||||
private provisioningApi?: Provisioner;
|
||||
private replyProcessor = new RichRepliesPreprocessor(true);
|
||||
@ -62,6 +60,7 @@ export class Bridge {
|
||||
|
||||
constructor(
|
||||
private config: BridgeConfig,
|
||||
private readonly tokenStore: UserTokenStore,
|
||||
private readonly listener: ListenerService,
|
||||
private readonly as: Appservice,
|
||||
private readonly storage: IBridgeStorageProvider,
|
||||
@ -71,8 +70,6 @@ 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}));
|
||||
@ -87,8 +84,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?.();
|
||||
|
||||
@ -755,7 +752,7 @@ export class Bridge {
|
||||
if (apps.length > 1) {
|
||||
throw Error('You may only bind `widgets` to one listener.');
|
||||
}
|
||||
this.widgetApi = new BridgeWidgetApi(
|
||||
new BridgeWidgetApi(
|
||||
this.adminRooms,
|
||||
this.config,
|
||||
this.storage,
|
||||
|
@ -17,7 +17,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 "./UserTokenStore";
|
||||
import { UserTokenStore } from "./tokens/UserTokenStore";
|
||||
import BotUsersManager from "./Managers/BotUsersManager";
|
||||
import { retry, retryMatrixErrorFilter } from "./PromiseUtil";
|
||||
import Metrics from "./Metrics";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
|
||||
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
import { ensureUserIsInRoom, getIntentForUser } from "../IntentUtils";
|
||||
|
@ -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 "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
|
@ -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 "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { emojify } from "node-emoji";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
|
@ -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 "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
|
||||
import { BotCommands, botCommand, compileBotCommands } from "../BotCommands";
|
||||
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
||||
|
@ -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 "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
|
||||
|
@ -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 "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import { CommandError, NotLoggedInError } from "../errors";
|
||||
import { ApiError, ErrCode } from "../api";
|
||||
import JiraApi from "jira-client";
|
||||
|
@ -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 "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
|
||||
const log = new Logger('GitLabGrantChecker');
|
||||
|
||||
|
@ -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 '../UserTokenStore';
|
||||
import { AllowedTokenTypes, TokenType, UserTokenStore } from '../tokens/UserTokenStore';
|
||||
|
||||
const log = new Logger("BridgeWidgetApi");
|
||||
|
||||
|
@ -550,7 +550,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;
|
||||
this.passFile = configData.passFile ?? "./passkey.pem";
|
||||
this.bot = configData.bot;
|
||||
this.serviceBots = configData.serviceBots;
|
||||
this.metrics = configData.metrics;
|
||||
|
@ -34,7 +34,7 @@ export const DefaultConfigRoot: BridgeConfigRoot = {
|
||||
level: "admin"
|
||||
}],
|
||||
}],
|
||||
passFile: "passkey.pem",
|
||||
passFile: "./passkey.pem",
|
||||
widgets: {
|
||||
publicUrl: `${hookshotWebhooksUrl}/widgetapi/v1/static`,
|
||||
addToAdminRooms: false,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { GitHubRepoConnection } from "../Connections";
|
||||
import { GrantChecker } from "../grants/GrantCheck";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import { Logger } from 'matrix-appservice-bridge';
|
||||
|
||||
const log = new Logger('GitHubGrantChecker');
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Router, Request, Response, NextFunction } from "express";
|
||||
import { BridgeConfigGitHub } from "../config/Config";
|
||||
import { ApiError, ErrCode } from "../api";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { GithubInstance } from "./GithubInstance";
|
||||
import { NAMELESS_ORG_PLACEHOLDER } from "./Types";
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { JiraProjectConnection } from "../Connections";
|
||||
import { GrantChecker } from "../grants/GrantCheck";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
|
||||
interface JiraGrantConnectionId{
|
||||
url: string;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BridgeConfigJira } from "../config/Config";
|
||||
import { MessageQueue } from "../MessageQueue";
|
||||
import { Router, Request, Response, NextFunction, json } from "express";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { UserTokenStore } from "../tokens/UserTokenStore";
|
||||
import { Logger } from "matrix-appservice-bridge";
|
||||
import { ApiError, ErrCode } from "../api";
|
||||
import { JiraOAuthRequestCloud, JiraOAuthRequestOnPrem, JiraOAuthRequestResult } from "./OAuth";
|
||||
|
@ -3,6 +3,7 @@ pub mod feeds;
|
||||
pub mod format_util;
|
||||
pub mod github;
|
||||
pub mod jira;
|
||||
pub mod tokens;
|
||||
pub mod util;
|
||||
|
||||
#[macro_use]
|
||||
|
@ -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,6 +31,8 @@ export const AllowedTokenTypes = ["github", "gitlab", "jira"];
|
||||
|
||||
interface StoredTokenData {
|
||||
encrypted: string|string[];
|
||||
keyId: string;
|
||||
algorithm: 'rsa';
|
||||
instance?: string;
|
||||
}
|
||||
|
||||
@ -51,20 +53,29 @@ 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<Emitter> {
|
||||
private key!: Buffer;
|
||||
|
||||
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 oauthSessionStore: Map<string, {userId: string, timeout: NodeJS.Timeout}> = new Map();
|
||||
private userTokens: Map<string, string>;
|
||||
public readonly jiraOAuth?: JiraOAuth;
|
||||
constructor(private keyPath: string, private intent: Intent, private config: BridgeConfig) {
|
||||
private tokenEncryption: TokenEncryption;
|
||||
private readonly keyId: string;
|
||||
constructor(key: Buffer, private readonly intent: Intent, private readonly 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);
|
||||
@ -76,11 +87,6 @@ export class UserTokenStore extends TypedEmitter<Emitter> {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@ -92,21 +98,16 @@ export class UserTokenStore extends TypedEmitter<Emitter> {
|
||||
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[] = [];
|
||||
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 tokenParts: string[] = this.tokenEncryption.encrypt(token);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -146,8 +147,19 @@ export class UserTokenStore extends TypedEmitter<Emitter> {
|
||||
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 = encryptedParts.map((t) => privateDecrypt(this.key, Buffer.from(t, "base64")).toString("utf-8")).join("");
|
||||
const token = this.tokenEncryption.decrypt(encryptedParts);
|
||||
this.userTokens.set(key, token);
|
||||
return token;
|
||||
} catch (ex) {
|
116
src/tokens/mod.rs
Normal file
116
src/tokens/mod.rs
Normal file
@ -0,0 +1,116 @@
|
||||
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<u8>) -> Result<Self, TokenEncryptionError> {
|
||||
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<Self, Error> {
|
||||
let buf: Vec<u8> = 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<String>) -> Result<String, Error> {
|
||||
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<String, DecryptError> {
|
||||
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<Vec<String>, Error> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut parts: Vec<String> = 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)
|
||||
}
|
||||
}
|
@ -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/UserTokenStore";
|
||||
import { UserTokenStore } from "../src/tokens/UserTokenStore";
|
||||
import { IntentMock } from "./utils/IntentMock";
|
||||
|
||||
const ROOM_ID = "!foo:bar";
|
||||
@ -14,9 +14,8 @@ function createAdminRoom(data: any = {admin_user: "@admin:bar"}): [AdminRoom, In
|
||||
if (!data.admin_user) {
|
||||
data.admin_user = "@admin:bar";
|
||||
}
|
||||
const tokenStore = new UserTokenStore("notapath", intent, DefaultConfig);
|
||||
return [new AdminRoom(ROOM_ID, data, NotifFilter.getDefaultContent(), intent, tokenStore, DefaultConfig, {} as ConnectionManager), intent];
|
||||
}
|
||||
return [new AdminRoom(ROOM_ID, data, NotifFilter.getDefaultContent(), intent, {} as UserTokenStore, DefaultConfig, {} as ConnectionManager), intent];
|
||||
}
|
||||
|
||||
describe("AdminRoom", () => {
|
||||
it("will present help text", async () => {
|
||||
|
@ -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/UserTokenStore";
|
||||
import { UserTokenStore } from "../../src/tokens/UserTokenStore";
|
||||
import { DefaultConfig } from "../../src/config/Defaults";
|
||||
import { AppserviceMock } from "../utils/AppserviceMock";
|
||||
import { ApiError, ErrCode, ValidatorApiError } from "../../src/api";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createMessageQueue } from "../../src/MessageQueue";
|
||||
import { UserTokenStore } from "../../src/UserTokenStore";
|
||||
import { UserTokenStore } from "../../src/tokens/UserTokenStore";
|
||||
import { AppserviceMock } from "../utils/AppserviceMock";
|
||||
import { ApiError, ErrCode, ValidatorApiError } from "../../src/api";
|
||||
import { GitLabRepoConnection, GitLabRepoConnectionState } from "../../src/Connections";
|
||||
|
48
tests/tokens/tokenencryption.spec.ts
Normal file
48
tests/tokens/tokenencryption.spec.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { TokenEncryption } from "../../src/libRs";
|
||||
import { RSAKeyPairOptions, generateKeyPair } from "node:crypto";
|
||||
import { expect } from "chai";
|
||||
|
||||
describe("TokenEncryption", () => {
|
||||
let keyPromise: Promise<Buffer>;
|
||||
async function createTokenEncryption() {
|
||||
return new TokenEncryption(await keyPromise);
|
||||
}
|
||||
|
||||
before('generate RSA key', () => {
|
||||
// Generate this once since it will take an age.
|
||||
keyPromise = new Promise<Buffer>((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);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user