Skip to content
Merged
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
Binary file modified _scripts/db/dumps/domifa_test.postgres.custom.gz
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { appLogger, cleanPath } from "../util";
import { structureDocRepository } from "../database";
import { join } from "node:path";
import { FileManagerService } from "../util/file-manager/file-manager.service";
import { StructureDoc } from "@domifa/common";
import { CommonDoc, StructureDoc } from "@domifa/common";
import { createHash } from "node:crypto";

type Docs = StructureDoc & {
structureUuid: string;
};
export class EncryptStructureDocsMigration1748264444998
export class EncryptStructureDocsMigration1748264444999
implements MigrationInterface
{
public fileManagerService: FileManagerService;
Expand Down Expand Up @@ -81,6 +82,15 @@ export class EncryptStructureDocsMigration1748264444998
object
);

const isIntact = await this.compareFileIntegrity(
filePath,
newFilePath,
{ ...doc, encryptionContext }
);

if (!isIntact) {
errorCount++;
}
await structureDocRepository.update(
{ uuid: doc.uuid },
{
Expand Down Expand Up @@ -129,4 +139,66 @@ export class EncryptStructureDocsMigration1748264444998

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async down(_queryRunner: QueryRunner): Promise<void> {}

public calculateHash(data: string | Buffer): string {
const hash = createHash("sha256");
if (typeof data === "string") {
hash.update(data, "binary");
} else {
hash.update(data);
}
return hash.digest("hex");
}
private async compareFileIntegrity(
originalPath: string,
encryptedPath: string,
doc: CommonDoc
): Promise<boolean> {
try {
const originalStream = await this.fileManagerService.getFileBody(
originalPath
);
const originalChunks: Buffer[] = [];
for await (const chunk of originalStream) {
originalChunks.push(
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
);
}
const originalBuffer = Buffer.concat(originalChunks);
const originalHash = this.calculateHash(originalBuffer);

const decryptedContent =
await this.fileManagerService.getDecryptedFileContent(
encryptedPath,
doc
);
const decryptedHash = this.calculateHash(decryptedContent);

const isIntact = originalHash === decryptedHash;

appLogger.info(`🔍 ${originalPath}`);
appLogger.info(
` Original: ${originalHash.substring(0, 16)}... (${
originalBuffer.length
} bytes)`
);
appLogger.info(
` Decrypted: ${decryptedHash.substring(0, 16)}... (${
decryptedContent.length
} chars)`
);
appLogger.info(` Status: ${isIntact ? "✅ OK" : "❌ CORRUPTED"}`);

if (!isIntact) {
appLogger.error(`🔴 CORRUPTION DETECTED: ${originalPath}`);
}

return isIntact;
} catch (error) {
appLogger.error(
`🔴 Comparison failed for ${originalPath}: ${error.message}`
);
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,7 @@ export class UsagerStructureDocsController {
});

try {
const docGenerated = await generateCustomDoc(
content.toString(),
docValues
);
const docGenerated = await generateCustomDoc(content, docValues);
return res.end(docGenerated);
} catch (e) {
return res
Expand Down Expand Up @@ -145,7 +142,7 @@ export class UsagerStructureDocsController {
customDocType: docType,
});

let content = "";
let content: Buffer;

const users = await userStructureRepository.getVerifiedUsersByStructureId(
user.structureId
Expand All @@ -158,7 +155,10 @@ export class UsagerStructureDocsController {
`${doc.path}.sfe`
);

content = await this.fileManagerService.getObjectAndStream(filePath);
content = await this.fileManagerService.getDecryptedFileContent(
filePath,
doc
);
} else {
content = await customDocTemplateLoader.loadDefaultDocTemplate({
docType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ async function loadDefaultDocTemplate({
docType,
}: {
docType: StructureDocTypesAvailable;
}) {
}): Promise<Buffer> {
const defaultTemplatePath = buildDefaultTemplatePath(docType);
return await readFile(resolve(defaultTemplatePath), "binary");
return await readFile(resolve(defaultTemplatePath));
}

function buildDefaultTemplatePath(docType: StructureDocTypesAvailable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { appLogger } from "../../../util";
import { StructureCustomDocTags } from "../../../_common/model";

export async function generateCustomDoc(
content: string, // template file content
content: Buffer,
docValues: StructureCustomDocTags
): Promise<Buffer> {
let doc: Docxtemplater;
Expand Down
76 changes: 51 additions & 25 deletions packages/backend/src/util/file-manager/file-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "@socialgouv/streaming-file-encryption";
import { Response } from "express";
import { compressAndResizeImage } from "./FileManager";
import { appLogger } from "../logs";

@Injectable()
export class FileManagerService {
Expand All @@ -33,7 +34,7 @@ export class FileManagerService {
secretAccessKey: domifaConfig().upload.bucketSecretKey,
},
region: domifaConfig().upload.bucketRegion,
forcePathStyle: true,
forcePathStyle: domifaConfig().envId === "local" ? true : false,
});
}

Expand Down Expand Up @@ -69,7 +70,6 @@ export class FileManagerService {
}
}

// Return body for encrypted files
public async getFileBody(path: string): Promise<Readable> {
const { Body } = await this.getObject(
`${domifaConfig().upload.bucketRootDir}/${path}`
Expand All @@ -95,19 +95,6 @@ export class FileManagerService {
}
}

public async getObjectAndStream(filePath: string): Promise<string> {
const readable = await this.getFileBody(filePath);

const chunks: Uint8Array[] = [];

for await (const chunk of readable) {
chunks.push(chunk);
}

const buffer = Buffer.concat(chunks);
return buffer.toString("binary");
}

public async deleteAllUnderStructure(prefix: string) {
const listParams = {
Bucket: domifaConfig().upload.bucketName,
Expand Down Expand Up @@ -140,22 +127,37 @@ export class FileManagerService {
public async getDecryptedFileContent(
filePath: string,
doc: CommonDoc
): Promise<string> {
): Promise<Buffer> {
const mainSecret = domifaConfig().security.mainSecret;
const readable = await this.getFileBody(filePath);

const decryptedStream = readable.pipe(
decryptFile(mainSecret, doc.encryptionContext)
);

const chunks: Uint8Array[] = [];
const chunks: Buffer[] = [];
let totalLength = 0;
const maxSize = 50 * 1024 * 1024;

for await (const chunk of decryptedStream) {
chunks.push(chunk);
}
try {
for await (const chunk of decryptedStream) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);

if (totalLength + buffer.length > maxSize) {
chunks.length = 0;
throw new Error(`Max size ${maxSize} bytes`);
}

const buffer = Buffer.concat(chunks);
return buffer.toString("binary");
chunks.push(buffer);
totalLength += buffer.length;
}
const finalBuffer = Buffer.concat(chunks, totalLength);
chunks.length = 0;
return finalBuffer;
} catch (error) {
chunks.length = 0;
appLogger.error(`Erreur décryptage ${filePath}:`, error.message);
throw error;
}
}

public async dowloadEncryptedFile(
Expand All @@ -164,9 +166,33 @@ export class FileManagerService {
doc: CommonDoc
) {
const mainSecret = domifaConfig().security.mainSecret;
const body = await this.getFileBody(filePath);

return body.pipe(decryptFile(mainSecret, doc.encryptionContext)).pipe(res);
try {
res.setHeader("Content-Type", doc.filetype || "application/octet-stream");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");

const body = await this.getFileBody(filePath);
const decryptStream = decryptFile(mainSecret, doc.encryptionContext);

await pipeline(body, decryptStream, res);

appLogger.debug(`📤 File downloaded successfully: ${filePath}`);
} catch (error) {
appLogger.error(`❌ Download failed for ${filePath}:`, error);

if (!res.headersSent) {
res.status(500).json({
message: "DOWNLOAD_FAILED",
});
} else {
// Si le stream a déjà commencé, on ne peut que fermer
res.destroy();
}

throw error;
}
}

public async saveEncryptedFile(
Expand Down
Loading