From 4f1c06418996992188c3c12c56bbef0807b5d115 Mon Sep 17 00:00:00 2001 From: OxSaitama Date: Wed, 10 May 2023 23:23:20 +0200 Subject: [PATCH 1/4] fix data encryption --- package.json | 7 +- src/app/api/super-admin/FilesController.ts | 22 ++++++- .../20230510204321_v4/migration.sql | 9 +++ src/common/databases/schema.prisma | 2 +- src/common/databases/seeders/seeder.ts | 17 ++--- src/common/repositories/FilesRepository.ts | 9 ++- .../CryptoService/CryptoService.ts | 66 +++++++------------ .../FilesService/FilesService.ts | 64 +++++++++++------- 8 files changed, 113 insertions(+), 83 deletions(-) create mode 100644 src/common/databases/migrations/20230510204321_v4/migration.sql diff --git a/package.json b/package.json index 14fa336e..7fe55c8c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@Test": "./dist/test" }, "scripts": { - "build-db": "npx prisma migrate dev", + "build-db": "npx prisma migrate dev && node ./dist/common/databases/seeders/seeder.js", "build": "tsc", "start": "node ./dist/entries/App.js", "api:start": "npm run migrate && npm run start", @@ -48,7 +48,7 @@ "cors": "^2.8.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.44", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.46", "module-alias": "^2.2.2", "multer": "^1.4.5-lts.1", "next": "^13.1.5", @@ -60,7 +60,8 @@ "tslib": "^2.4.1", "typedi": "^0.10.0", "typescript": "^4.9.4", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "uuidv4": "^6.2.13" }, "devDependencies": { "@types/cors": "^2.8.13", diff --git a/src/app/api/super-admin/FilesController.ts b/src/app/api/super-admin/FilesController.ts index ae38660a..68c2dfd1 100644 --- a/src/app/api/super-admin/FilesController.ts +++ b/src/app/api/super-admin/FilesController.ts @@ -39,6 +39,26 @@ export default class FilesController extends ApiController { } } + /** + * @description Get a specific File by uid + */ + @Get("/api/v1/super-admin/files/upload/:uid") + protected async getFileData(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + throw new Error("No uid provided"); + } + + const file = await this.filesService.updload(uid); + + this.httpSuccess(response, file); + } catch (error) { + this.httpBadRequest(response, error); + return; + } + } + /** * @description Create a new File * @returns File created @@ -135,7 +155,7 @@ export default class FilesController extends ApiController { /** * @description Get a specific File by uid */ - @Get("/api/v1/super-admin/Files/:uid") + @Get("/api/v1/super-admin/files/:uid") protected async getOneByUid(req: Request, response: Response) { try { const uid = req.params["uid"]; diff --git a/src/common/databases/migrations/20230510204321_v4/migration.sql b/src/common/databases/migrations/20230510204321_v4/migration.sql new file mode 100644 index 00000000..463c9e19 --- /dev/null +++ b/src/common/databases/migrations/20230510204321_v4/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `iv` on the `files` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "files" DROP COLUMN "iv", +ADD COLUMN "key" VARCHAR(255); diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index b1b04496..4261b9ea 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -204,7 +204,7 @@ model Files { document_uid String @db.VarChar(255) file_path String @unique @db.VarChar(255) file_name String @db.VarChar(255) - iv String @db.VarChar(255) + key String? @db.VarChar(255) created_at DateTime? @default(now()) updated_at DateTime? @updatedAt diff --git a/src/common/databases/seeders/seeder.ts b/src/common/databases/seeders/seeder.ts index 08b8869a..999503de 100644 --- a/src/common/databases/seeders/seeder.ts +++ b/src/common/databases/seeders/seeder.ts @@ -25,12 +25,6 @@ import { (async () => { const prisma = new PrismaClient(); - const existingData = await prisma.contacts.findFirst({ where: { email: "john.doe@example.com" } }); - if (existingData) { - console.log("Seed data already exists. Skipping seeding process."); - return; - } - const randomString = () => { const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; let result = ""; @@ -92,6 +86,13 @@ import { const uidDocumentHistory1: string = randomString(); const uidDocumentHistory2: string = randomString(); + // const existingData = await prisma.contacts.findFirst({ where: { uid: uidContact4 } }); + // if (existingData) { + // console.log("Seed data already exists. Skipping seeding process."); + // return; + // } + + const customers: Customers[] = [ { uid: uidCustomer1, @@ -427,7 +428,7 @@ import { document_uid: uidDocument1, file_name: "fileName1", file_path: "https://www.google1.com", - iv: "randomIv1", + key: '', created_at: new Date(), updated_at: new Date(), }, @@ -436,7 +437,7 @@ import { document_uid: uidDocument1, file_name: "fileName2", file_path: "https://www.google2.com", - iv: "randomIv2", + key: '', created_at: new Date(), updated_at: new Date(), }, diff --git a/src/common/repositories/FilesRepository.ts b/src/common/repositories/FilesRepository.ts index af5b8044..d48fb7d5 100644 --- a/src/common/repositories/FilesRepository.ts +++ b/src/common/repositories/FilesRepository.ts @@ -27,7 +27,7 @@ export default class FilesRepository extends BaseRepository { /** * @description : Create a file linked to a document */ - public async create(file: File): Promise { + public async create(file: File, key: string): Promise { return this.model.create({ data: { document: { @@ -37,7 +37,7 @@ export default class FilesRepository extends BaseRepository { }, file_name: file.file_name, file_path: file.file_path, - iv: file.iv + key: key }, include: { document: true } }); @@ -61,10 +61,13 @@ export default class FilesRepository extends BaseRepository { * @description : Delete a file */ public async delete(uid: string): Promise { - return this.model.delete({ + return this.model.update({ where: { uid: uid, }, + data: { + key: null + } }); } diff --git a/src/services/private-services/CryptoService/CryptoService.ts b/src/services/private-services/CryptoService/CryptoService.ts index 548ed6b0..5ac62af1 100644 --- a/src/services/private-services/CryptoService/CryptoService.ts +++ b/src/services/private-services/CryptoService/CryptoService.ts @@ -5,62 +5,44 @@ import crypto from "crypto"; @Service() export default class CryptoService extends BaseService { - private jwkKey: JsonWebKey; - private subtle: SubtleCrypto = crypto.webcrypto.subtle + + private static readonly CRYPTO_ALGORITHM = "aes-256-ctr"; + constructor(protected variables: BackendVariables) { super(); - this.jwkKey = { - kty: "oct", - k: variables.KEY_DATA, - alg: "A256GCM", - ext: true, - }; } - private async getKey() { - return await this.subtle.importKey("jwk", this.jwkKey, {name: "AES-GCM"}, false, ["encrypt", "decrypt"]); + private getKey(key: string) { + return crypto.createHash('sha256').update(String(key)).digest('base64').slice(0, 32); } /** * @description : encrypt data * @throws {Error} If data cannot be encrypted */ - public async encrypt(data: string) { - const encodedData = Buffer.from(data); - const iv = crypto.webcrypto.getRandomValues(new Uint8Array(16)) - const key = await this.getKey(); - const cipherData = await this.subtle.encrypt( - { - name: "AES-GCM", - iv, - }, - key, - encodedData, - ); - - const cipherText = Buffer.from(cipherData).toString('base64'); - const ivStringified = Buffer.from(iv).toString('base64'); - - return { cipherText, ivStringified }; + public async encrypt(buffer: Buffer, key: string): Promise { + // Create an initialization vector + const iv = crypto.randomBytes(16); + // Create a new cipher using the algorithm, key, and iv + const cipher = crypto.createCipheriv(CryptoService.CRYPTO_ALGORITHM, this.getKey(key), iv); + // Create the new (encrypted) buffer + const result = Buffer.concat([iv, cipher.update(buffer), cipher.final()]); + return result; } - + /** * @description : decrypt data with an initialization vector * @throws {Error} If data cannot be decrypted */ - public async decrypt(cipherText: string, ivStringified: string): Promise { - const cipherData = Buffer.from(cipherText, 'base64'); - const iv = Buffer.from(ivStringified, 'base64'); - const key = await this.getKey(); - const decryptedData = await this.subtle.decrypt( - { - name: "AES-GCM", - iv, - }, - key, - cipherData, - ); - - return Buffer.from(decryptedData).toString('utf-8'); + public async decrypt(encrypted: Buffer, key: string): Promise { + // Get the iv: the first 16 bytes + const iv = encrypted.subarray(0, 16); + // Get the rest + encrypted = encrypted.subarray(16); + // Create a decipher + const decipher = crypto.createDecipheriv(CryptoService.CRYPTO_ALGORITHM, this.getKey(key), iv); + // Actually decrypt it + const result = Buffer.concat([decipher.update(encrypted), decipher.final()]); + return result; } } diff --git a/src/services/private-services/FilesService/FilesService.ts b/src/services/private-services/FilesService/FilesService.ts index 88b1d3b9..8affd988 100644 --- a/src/services/private-services/FilesService/FilesService.ts +++ b/src/services/private-services/FilesService/FilesService.ts @@ -1,16 +1,21 @@ import FilesRepository from "@Repositories/FilesRepository"; import BaseService from "@Services/BaseService"; import { Service } from "typedi"; -import { File } from "le-coffre-resources/dist/SuperAdmin" +import { File } from "le-coffre-resources/dist/SuperAdmin"; import CryptoService from "../CryptoService/CryptoService"; import IpfsService from "../IpfsService/IpfsService"; -//import fs from "fs"; import { BackendVariables } from "@Common/config/variables/Variables"; import { Readable } from "stream"; +import { uuid } from "uuidv4"; @Service() export default class FilesService extends BaseService { - constructor(private filesRepository: FilesRepository, private ipfsService: IpfsService, private variables: BackendVariables, private cryptoService: CryptoService) { + constructor( + private filesRepository: FilesRepository, + private ipfsService: IpfsService, + private variables: BackendVariables, + private cryptoService: CryptoService, + ) { super(); } @@ -22,17 +27,39 @@ export default class FilesService extends BaseService { return this.filesRepository.findMany(query); } + /** + * @description : Get a file by uid + * @throws {Error} If project cannot be created + */ + public async getByUid(uid: string) { + return this.filesRepository.findOneByUid(uid); + } + + /** + * @description : view a file + * @throws {Error} If file cannot be deleted + */ + public async updload(uid: string) { + const file = await this.filesRepository.findOneByUid(uid); + if (!file.key) throw new Error("file deleted"); + const fileResult = await fetch(file.file_path); + const fileContent = await fileResult.arrayBuffer(); + return await this.cryptoService.decrypt(Buffer.from(fileContent), file.key); + } + /** * @description : Create a new file * @throws {Error} If file cannot be created */ public async create(file: File, fileData: Express.Multer.File) { - const upload = await this.ipfsService.pinFile(Readable.from(fileData.buffer), fileData.originalname); - const encryptedPath = await this.cryptoService.encrypt(this.variables.PINATA_GATEWAY.concat(upload.IpfsHash)); - file.file_name = fileData.originalname; - file.file_path = encryptedPath.cipherText; - file.iv = encryptedPath.ivStringified; - return this.filesRepository.create(file); + const key = uuid(); //crypto.getRandomValues(new Uint8Array(16)); + const encryptedFile = await this.cryptoService.encrypt(fileData.buffer, key); + //const encryptedFileName = await this.cryptoService.encrypt(Buffer.from(fileData.originalname, 'utf-8'), key); + const upload = await this.ipfsService.pinFile(Readable.from(encryptedFile), fileData.originalname); + file.file_name = fileData.originalname; //encryptedFileName.toString('utf-8') + file.file_path = this.variables.PINATA_GATEWAY.concat(upload.IpfsHash); + + return this.filesRepository.create(file, key); } /** @@ -48,23 +75,10 @@ export default class FilesService extends BaseService { * @throws {Error} If file cannot be deleted */ public async delete(uid: string) { - try { - const fileToUnpin = await this.filesRepository.findOneByUid(uid); - const decryptedFilePath = await this.cryptoService.decrypt(fileToUnpin.file_path, fileToUnpin.iv); - const fileHash= decryptedFilePath.substring(this.variables.PINATA_GATEWAY.length); - await this.ipfsService.unpinFile(fileHash) - } catch (error) { - console.log(error); - } + const fileToUnpin = await this.filesRepository.findOneByUid(uid); + const fileHash = fileToUnpin.file_path.substring(this.variables.PINATA_GATEWAY.length); + await this.ipfsService.unpinFile(fileHash); return this.filesRepository.delete(uid); } - - /** - * @description : Get a file by uid - * @throws {Error} If project cannot be created - */ - public async getByUid(uid: string) { - return this.filesRepository.findOneByUid(uid); - } } From 6904d1be02ee53aa36371da0ecb5821741f37565 Mon Sep 17 00:00:00 2001 From: OxSaitama Date: Thu, 11 May 2023 08:54:08 +0200 Subject: [PATCH 2/4] remove deprecated method uuid() --- src/services/private-services/FilesService/FilesService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/private-services/FilesService/FilesService.ts b/src/services/private-services/FilesService/FilesService.ts index 8affd988..8c050c48 100644 --- a/src/services/private-services/FilesService/FilesService.ts +++ b/src/services/private-services/FilesService/FilesService.ts @@ -6,7 +6,7 @@ import CryptoService from "../CryptoService/CryptoService"; import IpfsService from "../IpfsService/IpfsService"; import { BackendVariables } from "@Common/config/variables/Variables"; import { Readable } from "stream"; -import { uuid } from "uuidv4"; +import { v4 } from "uuid"; @Service() export default class FilesService extends BaseService { @@ -52,7 +52,7 @@ export default class FilesService extends BaseService { * @throws {Error} If file cannot be created */ public async create(file: File, fileData: Express.Multer.File) { - const key = uuid(); //crypto.getRandomValues(new Uint8Array(16)); + const key = v4(); const encryptedFile = await this.cryptoService.encrypt(fileData.buffer, key); //const encryptedFileName = await this.cryptoService.encrypt(Buffer.from(fileData.originalname, 'utf-8'), key); const upload = await this.ipfsService.pinFile(Readable.from(encryptedFile), fileData.originalname); From 9c84f995981f54c4dee1c23effc1e57299f6e3c8 Mon Sep 17 00:00:00 2001 From: OxSaitama Date: Thu, 11 May 2023 11:07:59 +0200 Subject: [PATCH 3/4] add archived to files on delete --- package.json | 2 +- .../databases/migrations/20230511085908_v5/migration.sql | 2 ++ src/common/databases/schema.prisma | 3 ++- src/common/databases/seeders/seeder.ts | 2 ++ src/common/repositories/FilesRepository.ts | 3 ++- 5 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/common/databases/migrations/20230511085908_v5/migration.sql diff --git a/package.json b/package.json index 7fe55c8c..327677fa 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "cors": "^2.8.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.46", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.47", "module-alias": "^2.2.2", "multer": "^1.4.5-lts.1", "next": "^13.1.5", diff --git a/src/common/databases/migrations/20230511085908_v5/migration.sql b/src/common/databases/migrations/20230511085908_v5/migration.sql new file mode 100644 index 00000000..a5b3abcb --- /dev/null +++ b/src/common/databases/migrations/20230511085908_v5/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "files" ADD COLUMN "archived_at" TIMESTAMP(3); diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index 4261b9ea..742b331b 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -39,7 +39,7 @@ model Contacts { cell_phone_number String @unique @db.VarChar(50) civility ECivility @default(MALE) address Addresses? @relation(fields: [address_uid], references: [uid], onDelete: Cascade) - address_uid String? @unique @db.VarChar(255) + address_uid String? @unique @db.VarChar(255) birthdate DateTime? created_at DateTime? @default(now()) updated_at DateTime? @updatedAt @@ -204,6 +204,7 @@ model Files { document_uid String @db.VarChar(255) file_path String @unique @db.VarChar(255) file_name String @db.VarChar(255) + archived_at DateTime? key String? @db.VarChar(255) created_at DateTime? @default(now()) updated_at DateTime? @updatedAt diff --git a/src/common/databases/seeders/seeder.ts b/src/common/databases/seeders/seeder.ts index 999503de..6a3eaf55 100644 --- a/src/common/databases/seeders/seeder.ts +++ b/src/common/databases/seeders/seeder.ts @@ -429,6 +429,7 @@ import { file_name: "fileName1", file_path: "https://www.google1.com", key: '', + archived_at: null, created_at: new Date(), updated_at: new Date(), }, @@ -438,6 +439,7 @@ import { file_name: "fileName2", file_path: "https://www.google2.com", key: '', + archived_at: null, created_at: new Date(), updated_at: new Date(), }, diff --git a/src/common/repositories/FilesRepository.ts b/src/common/repositories/FilesRepository.ts index d48fb7d5..34f2f89c 100644 --- a/src/common/repositories/FilesRepository.ts +++ b/src/common/repositories/FilesRepository.ts @@ -66,7 +66,8 @@ export default class FilesRepository extends BaseRepository { uid: uid, }, data: { - key: null + key: null, + archived_at: new Date(Date.now()) } }); } From d2d2263bd23fe4fc967f8599d37a5febb9621335 Mon Sep 17 00:00:00 2001 From: OxSaitama Date: Thu, 11 May 2023 11:12:58 +0200 Subject: [PATCH 4/4] add error catcher for ipfs --- src/services/private-services/FilesService/FilesService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/private-services/FilesService/FilesService.ts b/src/services/private-services/FilesService/FilesService.ts index 8c050c48..88e5948b 100644 --- a/src/services/private-services/FilesService/FilesService.ts +++ b/src/services/private-services/FilesService/FilesService.ts @@ -75,9 +75,13 @@ export default class FilesService extends BaseService { * @throws {Error} If file cannot be deleted */ public async delete(uid: string) { - const fileToUnpin = await this.filesRepository.findOneByUid(uid); + try { + const fileToUnpin = await this.filesRepository.findOneByUid(uid); const fileHash = fileToUnpin.file_path.substring(this.variables.PINATA_GATEWAY.length); await this.ipfsService.unpinFile(fileHash); + } catch(error) { + console.error(error); + } return this.filesRepository.delete(uid); }