3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-03-14 21:31:50 +00:00

Encrypt message data at rest

This commit is contained in:
Dragory 2020-09-16 22:32:43 +03:00
parent 3f3d6af4ed
commit baa3a5640e
No known key found for this signature in database
GPG key ID: 5F387BA66DF8AAC1
10 changed files with 121 additions and 3 deletions

1
.env.example Normal file
View file

@ -0,0 +1 @@
KEY=32_character_encryption_key

View file

@ -1,8 +1,14 @@
import "./loadEnv";
import { connect } from "../data/db";
import path from "path";
import { setIsAPI } from "../globals";
require("dotenv").config({ path: path.resolve(process.cwd(), "api.env") });
if (!process.env.KEY) {
// tslint:disable-next-line:no-console
console.error("Project root .env with KEY is required!");
process.exit(1);
}
function errorHandler(err) {
console.error(err.stack || err); // tslint:disable-line:no-console

View file

@ -0,0 +1,4 @@
import path from "path";
require("dotenv").config({ path: path.resolve(process.cwd(), "../.env") });
require("dotenv").config({ path: path.resolve(process.cwd(), "api.env") });

View file

@ -0,0 +1,22 @@
import { decrypt, encrypt } from "../utils/crypt";
import { ValueTransformer } from "typeorm";
interface EncryptedJsonTransformer<T> extends ValueTransformer {
from(dbValue: any): T;
to(entityValue: T): any;
}
export function createEncryptedJsonTransformer<T>(): EncryptedJsonTransformer<T> {
return {
// Database -> Entity
from(dbValue) {
const decrypted = decrypt(dbValue);
return JSON.parse(decrypted) as T;
},
// Entity -> Database
to(entityValue) {
return encrypt(JSON.stringify(entityValue));
},
};
}

View file

@ -1,4 +1,5 @@
import { Column, Entity, PrimaryColumn } from "typeorm";
import { createEncryptedJsonTransformer } from "../encryptedJsonTransformer";
export interface ISavedMessageData {
attachments?: object[];
@ -25,7 +26,11 @@ export class SavedMessage {
@Column() is_bot: boolean;
@Column("simple-json") data: ISavedMessageData;
@Column({
type: "mediumtext",
transformer: createEncryptedJsonTransformer<ISavedMessageData>(),
})
data: ISavedMessageData;
@Column() posted_at: string;

View file

@ -1,3 +1,5 @@
import "./loadEnv";
import path from "path";
import yaml from "js-yaml";
@ -25,7 +27,11 @@ import { PluginLoadError } from "knub/dist/plugins/PluginLoadError";
const fsp = fs.promises;
require("dotenv").config({ path: path.resolve(process.cwd(), "bot.env") });
if (!process.env.KEY) {
// tslint:disable-next-line:no-console
console.error("Project root .env with KEY is required!");
process.exit(1);
}
declare global {
// This is here so TypeScript doesn't give an error when importing twemoji

4
backend/src/loadEnv.ts Normal file
View file

@ -0,0 +1,4 @@
import path from "path";
require("dotenv").config({ path: path.resolve(process.cwd(), "../.env") });
require("dotenv").config({ path: path.resolve(process.cwd(), "bot.env") });

View file

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import { decrypt, encrypt } from "../utils/crypt";
export class EncryptExistingMessages1600283341726 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
// 1. Delete non-permanent messages
await queryRunner.query("DELETE FROM messages WHERE is_permanent = 0");
// 2. Encrypt all permanent messages
const messages = await queryRunner.query("SELECT id, data FROM messages");
for (const message of messages) {
const encryptedData = encrypt(message.data);
await queryRunner.query("UPDATE messages SET data = ? WHERE id = ?", [encryptedData, message.id]);
}
}
public async down(queryRunner: QueryRunner): Promise<any> {
// Decrypt all messages
const messages = await queryRunner.query("SELECT id, data FROM messages");
for (const message of messages) {
const decryptedData = decrypt(message.data);
await queryRunner.query("UPDATE messages SET data = ? WHERE id = ?", [decryptedData, message.id]);
}
}
}

View file

@ -0,0 +1,10 @@
import test from "ava";
import { encrypt, decrypt } from "./crypt";
test("encrypt() followed by decrypt()", t => {
const original = "banana 123 👀 💕"; // Includes emojis to verify utf8 stuff works
const encrypted = encrypt(original);
const decrypted = decrypt(encrypted);
t.is(decrypted, original);
});

View file

@ -0,0 +1,35 @@
import "../loadEnv";
import crypto, { DecipherGCM } from "crypto";
if (!process.env.KEY) {
// tslint:disable-next-line:no-console
console.error("Environment value KEY required for encryption");
process.exit(1);
}
const KEY = process.env.KEY;
const ALGORITHM = "aes-256-gcm";
export function encrypt(str) {
// Based on https://gist.github.com/rjz/15baffeab434b8125ca4d783f4116d81
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(str, "utf8", "base64");
encrypted += cipher.final("base64");
return `${iv.toString("base64")}.${cipher.getAuthTag().toString("base64")}.${encrypted}`;
}
export function decrypt(encrypted) {
// Based on https://gist.github.com/rjz/15baffeab434b8125ca4d783f4116d81
const [iv, authTag, encryptedStr] = encrypted.split(".");
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, Buffer.from(iv, "base64"));
decipher.setAuthTag(Buffer.from(authTag, "base64"));
let decrypted = decipher.update(encryptedStr, "base64", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}