Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1011.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `legacyPassKey` config option to specify a read-only key, allowing a gradual migration to a new key.
2 changes: 1 addition & 1 deletion src/App/BridgeApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,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 tokenStore = await UserTokenStore.fromKeyPath(appservice.botIntent, config);
const bridgeApp = new Bridge(config, tokenStore, listener, appservice, storage, botUsersManager);

process.once("SIGTERM", () => {
Expand Down
4 changes: 4 additions & 0 deletions src/config/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ export interface BridgeConfigRoot {
logging: BridgeConfigLogging;
metrics?: BridgeConfigMetrics;
passFile: string;
legacyPassFile?: string;
permissions?: BridgeConfigActorPermission[];
provisioning?: BridgeConfigProvisioning;
queue?: BridgeConfigQueue;
Expand Down Expand Up @@ -442,6 +443,8 @@ export class BridgeConfig {
@configKey(`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`)
public readonly passFile: string;
@configKey(`A passkey that can be used to decrypt old values, and will not be used to encrypt new values.`, true)
public readonly legacyPassFile?: string;
@configKey("Configure this to enable GitHub support", true)
public readonly github?: BridgeConfigGitHub;
@configKey("Configure this to enable GitLab support", true)
Expand Down Expand Up @@ -504,6 +507,7 @@ export class BridgeConfig {
this.feeds = configData.feeds && new BridgeConfigFeeds(configData.feeds);
this.provisioning = configData.provisioning;
this.passFile = configData.passFile ?? "./passkey.pem";
this.legacyPassFile = configData.legacyPassFile;
this.bot = configData.bot;
this.serviceBots = configData.serviceBots;
this.metrics = configData.metrics;
Expand Down
2 changes: 0 additions & 2 deletions src/config/sections/encryption.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { ConfigError } from "../../errors";
import { configKey } from "../Decorators";
import { BridgeConfigCache } from "./cache";
import { BridgeConfigQueue } from "./queue";

interface BridgeConfigEncryptionYAML {
storagePath: string;
Expand Down
36 changes: 27 additions & 9 deletions src/tokens/UserTokenStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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, stringToAlgo } from "../libRs";
import { hashId, TokenEncryption, stringToAlgo, Algo } 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:";
Expand Down Expand Up @@ -60,20 +60,27 @@ interface Emitter {
}
export class UserTokenStore extends TypedEmitter<Emitter> {

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);
public static async fromKeyPath(intent: Intent, config: BridgeConfig) {
log.info(`Loading token key file ${config.passFile}`);
const key = await fs.readFile(config.passFile);
let oldKey;
if (config.legacyPassFile) {
log.info(`Loading LEGACY (read-only) token key file ${config.legacyPassFile}`);
oldKey = await fs.readFile(config.legacyPassFile);
}
return new UserTokenStore(key, intent, config, oldKey);
}

private oauthSessionStore: Map<string, {userId: string, timeout: NodeJS.Timeout}> = new Map();
private userTokens: Map<string, string>;
public readonly jiraOAuth?: JiraOAuth;
private tokenEncryption: TokenEncryption;
private readonly tokenEncryption: TokenEncryption;
private readonly legacyTokenEncryption?: TokenEncryption;
private readonly keyId: string;
constructor(key: Buffer, private readonly intent: Intent, private readonly config: BridgeConfig) {
constructor(key: Buffer, private readonly intent: Intent, private readonly config: BridgeConfig, legacyKey?: Buffer) {
super();
this.tokenEncryption = new TokenEncryption(key);
this.legacyTokenEncryption = legacyKey && new TokenEncryption(legacyKey);
this.userTokens = new Map();
this.keyId = hashId(key.toString('utf-8'));
if (config.jira?.oauth) {
Expand Down Expand Up @@ -130,6 +137,17 @@ export class UserTokenStore extends TypedEmitter<Emitter> {
return this.storeUserToken("jira", userId, JSON.stringify(token));
}

private tryDecryptKey(encryptedParts: string[], algorithm: Algo) {
try {
return this.tokenEncryption.decrypt(encryptedParts, algorithm);
} catch (ex) {
if (this.legacyTokenEncryption) {
return this.legacyTokenEncryption.decrypt(encryptedParts, algorithm);
}
throw ex;
}
}

public async getUserToken(type: TokenType, userId: string, instanceUrl?: string): Promise<string|null> {
if (!AllowedTokenTypes.includes(type)) {
throw Error('Unknown token type');
Expand All @@ -156,7 +174,7 @@ export class UserTokenStore extends TypedEmitter<Emitter> {
}

const encryptedParts = typeof obj.encrypted === "string" ? [obj.encrypted] : obj.encrypted;
const token = this.tokenEncryption.decrypt(encryptedParts, algorithm);
const token = this.tryDecryptKey(encryptedParts, algorithm);
this.userTokens.set(key, token);
return token;
} catch (ex) {
Expand Down Expand Up @@ -192,7 +210,7 @@ export class UserTokenStore extends TypedEmitter<Emitter> {
}

const encryptedParts = typeof obj.encrypted === "string" ? [obj.encrypted] : obj.encrypted;
const token = this.tokenEncryption.decrypt(encryptedParts, algorithm);
const token = this.tryDecryptKey(encryptedParts, algorithm);
return token;
}

Expand Down
Loading