Skip to content

Commit 23994cd

Browse files
committed
Add support for proper large string encrypted storage
1 parent ac708c0 commit 23994cd

File tree

4 files changed

+66
-34
lines changed

4 files changed

+66
-34
lines changed

src/bridge/AdminRoomHandler.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,18 @@ export class AdminRoomHandler {
315315
private async handleCertfp(
316316
req: BridgeRequest, args: string[], sender: string, adminRoom: MatrixRoom): Promise<MatrixAction> {
317317
const server = this.extractServerFromArgs(args);
318+
if (!server.useSasl()) {
319+
return new MatrixAction(
320+
ActionType.Notice,
321+
'This bridge does not support SASL authentication, so you cannot store a certificate.',
322+
);
323+
}
318324
req.log.info(`${sender} is attempting to store a cert for ${server.domain}`);
319325
await this.ircBridge.sendMatrixAction(
320326
adminRoom, this.botUser, new MatrixAction(
321327
ActionType.Notice,
322-
`Please enter your certificate and private key (without formatting) for ${server.getReadableName()}.' +
323-
' Say 'cancel' to cancel.`
328+
`Please enter your certificate and private key (without formatting) for ${server.domain}.` +
329+
" Say 'cancel' to cancel.",
324330
)
325331
);
326332
let certfp: string;
@@ -423,14 +429,14 @@ export class AdminRoomHandler {
423429
const domain = ircServer.domain;
424430

425431
try {
426-
await this.ircBridge.getStore().removePass(userId, domain);
432+
await this.ircBridge.getStore().removeClientCert(userId, domain);
427433
return new MatrixAction(
428-
ActionType.Notice, `Successfully removed password.`
434+
ActionType.Notice, `Successfully removed certfp.`
429435
);
430436
}
431437
catch (err) {
432438
return new MatrixAction(
433-
ActionType.Notice, `Failed to remove password: ${err.message}`
439+
ActionType.Notice, `Failed to remove certfp: ${err.message}`
434440
);
435441
}
436442
}

src/datastore/NedbDataStore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ export class NeDBDataStore implements DataStore {
582582
try {
583583
clientConfig.setCertificate({
584584
cert: configData.certificate.cert,
585-
key: this.cryptoStore.decryptLargeString(configData.certificate.key),
585+
key: await this.cryptoStore.decryptLargeString(configData.certificate.key),
586586
})
587587
}
588588
catch (ex) {
@@ -628,7 +628,7 @@ export class NeDBDataStore implements DataStore {
628628
try {
629629
domainCfg.certificate = {
630630
cert: config.certificate.cert,
631-
key: this.cryptoStore.encryptLargeString(config.certificate.key),
631+
key: await this.cryptoStore.encryptLargeString(config.certificate.key),
632632
};
633633
}
634634
catch (ex) {

src/datastore/StringCrypto.ts

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import * as crypto from "crypto";
17+
import { createCipheriv, createDecipheriv, privateDecrypt, publicEncrypt, scrypt as scryptCb } from "node:crypto";
1818
import * as fs from "fs";
1919
import { getLogger } from "../logging";
20+
import { randomBytes } from "node:crypto";
21+
import { promisify } from "node:util";
22+
23+
const scrypt = promisify(scryptCb);
2024

2125
const log = getLogger("CryptoStore");
26+
const algorithm = 'aes-256-cbc';
2227

2328
export class StringCrypto {
2429
private privateKey!: string;
@@ -29,7 +34,7 @@ export class StringCrypto {
2934

3035
// Test whether key is a valid PEM key (publicEncrypt does internal validation)
3136
try {
32-
crypto.publicEncrypt(
37+
publicEncrypt(
3338
this.privateKey,
3439
Buffer.from("This is a test!")
3540
);
@@ -48,41 +53,59 @@ export class StringCrypto {
4853
}
4954

5055
public encrypt(plaintext: string): string {
51-
if (plaintext.includes(' ')) {
52-
throw Error('Cannot encode spaces')
53-
}
54-
const salt = crypto.randomBytes(16).toString('base64');
55-
return crypto.publicEncrypt(
56+
const salt = randomBytes(16).toString('base64');
57+
return publicEncrypt(
5658
this.privateKey,
5759
Buffer.from(salt + ' ' + plaintext)
5860
).toString('base64');
5961
}
6062

61-
public encryptLargeString(plaintext: string): string {
62-
const cryptoParts = [];
63-
while (plaintext.length > 0) {
64-
const part = plaintext.slice(0, 64);
65-
cryptoParts.push(this.encrypt(part));
66-
plaintext = plaintext.slice(64);
67-
}
68-
return 'lg:' + cryptoParts.join(',');
69-
}
70-
71-
7263
public decrypt(encryptedString: string): string {
73-
const decryptedPass = crypto.privateDecrypt(
64+
const decryptedPass = privateDecrypt(
7465
this.privateKey,
7566
Buffer.from(encryptedString, 'base64')
7667
).toString();
7768
// Extract the password by removing the prefixed salt and seperating space
78-
return decryptedPass.slice(17)[1];
69+
return decryptedPass.slice(25);
70+
}
71+
72+
public async encryptLargeString(plaintext: string): Promise<string> {
73+
const password = randomBytes(32).toString('base64');
74+
const key = await scrypt(password, 'salt', 32) as Buffer;
75+
const iv = randomBytes(16);
76+
const cipher = createCipheriv(algorithm, key, iv);
77+
cipher.setEncoding('base64');
78+
let encrypted = '';
79+
const secret = this.encrypt(`${key.toString('base64')}_${iv.toString('base64')}`);
80+
const streamPromise = new Promise<string>((resolve, reject) => {
81+
cipher.on('error', (err) => reject(err));
82+
cipher.on('end', () => resolve(
83+
`lg:${secret}:${encrypted}`
84+
));
85+
});
86+
cipher.on('data', (chunk) => { encrypted += chunk });
87+
cipher.write(plaintext);
88+
cipher.end();
89+
return streamPromise;
7990
}
8091

81-
public decryptLargeString(encryptedString: string): string {
82-
if (encryptedString !== 'lg:') {
92+
public async decryptLargeString(encryptedString: string): Promise<string> {
93+
if (!encryptedString.startsWith('lg:')) {
8394
throw Error('Not a large string');
8495
}
85-
encryptedString = encryptedString.slice(3);
86-
return encryptedString.split(',').map(v => this.decrypt(v)).join('');
96+
const [, keyPlusIvEnc, data] = encryptedString.split(':', 3);
97+
const [keyB64, ivB64] = this.decrypt(keyPlusIvEnc).split('_');
98+
const iv = Buffer.from(ivB64, "base64");
99+
const key = Buffer.from(keyB64, "base64");
100+
const decipher = createDecipheriv(algorithm, key, iv);
101+
let decrypted = '';
102+
decipher.on('data', (chunk) => { decrypted += chunk });
103+
const streamPromise = new Promise<string>((resolve, reject) => {
104+
decipher.on('error', (err) => reject(err));
105+
decipher.on('end', () => resolve(decrypted));
106+
});
107+
decipher.write(Buffer.from(data, 'base64'));
108+
decipher.end();
109+
return streamPromise;
87110
}
88111
}

src/datastore/postgres/PgDataStore.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,12 +536,14 @@ export class PgDataStore implements DataStore, ProvisioningStore {
536536
const cryptoStore = this.cryptoStore;
537537
if (config.certificate && row.key && cryptoStore) {
538538
try {
539-
config.certificate.key = cryptoStore.decryptLargeString(row.key);
539+
console.log(row);
540+
config.certificate.key = await cryptoStore.decryptLargeString(row.key);
540541
}
541542
catch (ex) {
542543
log.warn(`Failed to decrypt TLS key for ${userId} ${domain}`, ex);
543544
}
544545
}
546+
console.log(config);
545547
return new IrcClientConfig(userId, domain, config);
546548
}
547549

@@ -561,10 +563,10 @@ export class PgDataStore implements DataStore, ProvisioningStore {
561563
password = this.cryptoStore.encrypt(password);
562564
}
563565

564-
566+
console.log(config.certificate);
565567
if (config.certificate && this.cryptoStore) {
566568
keypair.cert = config.certificate.cert;
567-
keypair.key = this.cryptoStore.encryptLargeString(config.certificate.key);
569+
keypair.key = await this.cryptoStore.encryptLargeString(config.certificate.key);
568570
}
569571
const parameters = {
570572
user_id: userId,
@@ -577,6 +579,7 @@ export class PgDataStore implements DataStore, ProvisioningStore {
577579
};
578580
const statement = PgDataStore.BuildUpsertStatement(
579581
"client_config", "ON CONSTRAINT cons_client_config_unique", Object.keys(parameters));
582+
console.log(statement, parameters);
580583
await this.pgPool.query(statement, Object.values(parameters));
581584
}
582585

0 commit comments

Comments
 (0)