diff --git a/.env.sample b/.env.sample index 0b70aa4..622a549 100644 --- a/.env.sample +++ b/.env.sample @@ -25,7 +25,7 @@ IPFS_CLUSTER_HOST=localhost IPFS_HOST=localhost IPFS_MIN_REPLICA=2 IPFS_MAX_REPLICA=3 -ENC_PASSWORD=secret +ENC_PASSWORD_5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY=secret # Alchemy SDK keys GOERLI_ALCHEMY_KEY="" diff --git a/src/logion/controllers/collection.controller.ts b/src/logion/controllers/collection.controller.ts index 799c186..1689d34 100644 --- a/src/logion/controllers/collection.controller.ts +++ b/src/logion/controllers/collection.controller.ts @@ -299,7 +299,7 @@ export class CollectionController extends ApiController { throw badRequest("File is already uploaded"); } - const cid = await this.fileStorageService.importFile(file.tempFilePath); + const cid = await this.fileStorageService.importFile(file.tempFilePath, collectionLoc.getOwner()); await this.collectionService.update(collectionLocId, itemId, async item => { item.setFileCid({ hash, cid }); }); @@ -351,7 +351,9 @@ export class CollectionController extends ApiController { const file = collectionItem.getFile(hash); const tempFilePath = CollectionController.tempFilePath({ collectionLocId, itemId, hash }); - await this.fileStorageService.exportFile(file, tempFilePath); + const collectionLoc = requireDefined(await this.locRequestRepository.findById(collectionLocId), + () => badRequest(`Collection ${ collectionLocId } not found`)); + await this.fileStorageService.exportFile(file, tempFilePath, collectionLoc.getOwner()); const generatedOn = moment(); const owner = authenticated.address; @@ -436,7 +438,9 @@ export class CollectionController extends ApiController { itemId, hash, }); - await this.fileStorageService.exportFile(file, tempFilePath); + const collectionLoc = requireDefined(await this.locRequestRepository.findById(collectionLocId), + () => badRequest(`Collection ${ collectionLocId } not found`)); + await this.fileStorageService.exportFile(file, tempFilePath, collectionLoc.getOwner()); const generatedOn = moment(); const owner = authenticated.address; @@ -768,7 +772,7 @@ export class CollectionController extends ApiController { const collectionItem = await this.getCollectionItemWithFile(collectionLocId, itemId, hash); const file = collectionItem.getFile(hash); const tempFilePath = CollectionController.tempFilePath({ collectionLocId, itemId, hash }); - await this.fileStorageService.exportFile(file, tempFilePath); + await this.fileStorageService.exportFile(file, tempFilePath, collectionLoc.getOwner()); downloadAndClean({ response: this.response, diff --git a/src/logion/controllers/locrequest.controller.ts b/src/logion/controllers/locrequest.controller.ts index 1b39257..7f794ca 100644 --- a/src/logion/controllers/locrequest.controller.ts +++ b/src/logion/controllers/locrequest.controller.ts @@ -628,7 +628,7 @@ export class LocRequestController extends ApiController { throw new Error("File already present"); } const file = await getUploadedFile(this.request, hash); - const cid = await this.fileStorageService.importFile(file.tempFilePath); + const cid = await this.fileStorageService.importFile(file.tempFilePath, request.getOwner()); try { const storedFile: StoredFile = { name: file.name, @@ -697,7 +697,7 @@ export class LocRequestController extends ApiController { throw forbidden("Authenticated user is not allowed to download this file"); } const tempFilePath = "/tmp/download-" + requestId + "-" + hash; - await this.fileStorageService.exportFile(file, tempFilePath); + await this.fileStorageService.exportFile(file, tempFilePath, request.getOwner()); downloadAndClean({ response: this.response, path: tempFilePath, diff --git a/src/logion/controllers/lofile.controller.ts b/src/logion/controllers/lofile.controller.ts index 0c56e10..93fca99 100644 --- a/src/logion/controllers/lofile.controller.ts +++ b/src/logion/controllers/lofile.controller.ts @@ -120,7 +120,7 @@ export class LoFileController extends ApiController { () => badRequest(`LO has not yet uploaded file with id ${ id }`) ) const tempFilePath = "/tmp/download-" + id; - await this.fileStorageService.exportFile(file, tempFilePath); + await this.fileStorageService.exportFile(file, tempFilePath, legalOfficer); downloadAndClean({ response: this.response, diff --git a/src/logion/controllers/records.controller.ts b/src/logion/controllers/records.controller.ts index 22ab294..9f266f7 100644 --- a/src/logion/controllers/records.controller.ts +++ b/src/logion/controllers/records.controller.ts @@ -257,7 +257,7 @@ export class TokensRecordController extends ApiController { throw badRequest("File is already uploaded"); } - const cid = await this.fileStorageService.importFile(file.tempFilePath); + const cid = await this.fileStorageService.importFile(file.tempFilePath, collectionLoc.getOwner()); await this.tokensRecordService.update(collectionLocId, recordId, async item => { item.setFileCid({ hash, cid }); }); @@ -297,6 +297,8 @@ export class TokensRecordController extends ApiController { @Async() @SendsResponse() async downloadItemFile(_body: never, collectionLocId: string, recordIdHex: string, hashHex: string, itemIdHex: string): Promise { + const collectionLoc = requireDefined(await this.locRequestRepository.findById(collectionLocId), + () => badRequest(`Collection ${ collectionLocId } not found`)); const recordId = Hash.fromHex(recordIdHex); const hash = Hash.fromHex(hashHex); const itemId = Hash.fromHex(itemIdHex); @@ -314,7 +316,7 @@ export class TokensRecordController extends ApiController { const file = collectionItem.getFile(hash); const tempFilePath = TokensRecordController.tempFilePath({ collectionLocId, recordId, hash }); - await this.fileStorageService.exportFile(file, tempFilePath); + await this.fileStorageService.exportFile(file, tempFilePath, collectionLoc.getOwner()); const generatedOn = moment(); const owner = authenticated.address; @@ -465,7 +467,7 @@ export class TokensRecordController extends ApiController { const tokensRecord = await this.getTokensRecordWithFile(collectionLocId, recordId, hash); const file = tokensRecord.getFile(hash); const tempFilePath = TokensRecordController.tempFilePath({ collectionLocId, recordId, hash }); - await this.fileStorageService.exportFile(file, tempFilePath); + await this.fileStorageService.exportFile(file, tempFilePath, collectionLoc.getOwner()); downloadAndClean({ response: this.response, diff --git a/src/logion/lib/crypto/EncryptedFile.ts b/src/logion/lib/crypto/EncryptedFile.ts index 47094ad..bd590a3 100644 --- a/src/logion/lib/crypto/EncryptedFile.ts +++ b/src/logion/lib/crypto/EncryptedFile.ts @@ -12,17 +12,15 @@ interface EncryptDecryptProps { clearFile?: string encryptedFile?: string keepSource?: boolean + password: string } export class EncryptedFileWriter { - constructor(password: string) { - this.password = password; + constructor() { } - private readonly password: string; - - async open(fileName: string): Promise { + async open(fileName: string, password: string): Promise { this.file = await open(fileName, 'w'); return new Promise((resolve, reject) => { @@ -30,7 +28,7 @@ export class EncryptedFileWriter { if (err) { reject(err); } - scrypt(this.password, salt, KEY_LENGTH, (err, key) => { + scrypt(password, salt, KEY_LENGTH, (err, key) => { if (err) { reject(err); } @@ -82,7 +80,7 @@ export class EncryptedFileWriter { async encrypt(props: EncryptDecryptProps): Promise { const clearFile = props.clearFile! const encryptedFile = props.encryptedFile ? props.encryptedFile : `${clearFile}.enc`; - await this.open(encryptedFile); + await this.open(encryptedFile, props.password); await this.writeFromFile(clearFile); await this.close(); if (!props.keepSource) { @@ -94,14 +92,11 @@ export class EncryptedFileWriter { export class EncryptedFileReader { - constructor(password: string) { - this.password = password; + constructor() { this.buffer = new Uint8Array(READ_BUFFER_SIZE); } - private readonly password: string; - - async open(fileName: string): Promise { + async open(fileName: string, password: string): Promise { this.file = await open(fileName, 'r'); const salt = new Uint8Array(SALT_LENGTH); @@ -111,7 +106,7 @@ export class EncryptedFileReader { await this.file.read(iv, 0, IV_LENGTH); return new Promise((resolve, reject) => { - scrypt(this.password, salt, KEY_LENGTH, (err, key) => { + scrypt(password, salt, KEY_LENGTH, (err, key) => { if (err) { reject(err); } @@ -167,7 +162,7 @@ export class EncryptedFileReader { async decrypt(props: EncryptDecryptProps): Promise { const encryptedFile = props.encryptedFile! const clearFile = props.clearFile ? props.clearFile : `${encryptedFile}.clear` - await this.open(encryptedFile); + await this.open(encryptedFile, props.password); await this.readToFile(clearFile); await this.close() if (!props.keepSource) { diff --git a/src/logion/services/file.storage.service.ts b/src/logion/services/file.storage.service.ts index 8ae34a8..5ed7c38 100644 --- a/src/logion/services/file.storage.service.ts +++ b/src/logion/services/file.storage.service.ts @@ -3,6 +3,9 @@ import { exportFile, deleteFile, importFile } from '../lib/db/large_objects.js'; import { FileManager, DefaultFileManager, DefaultFileManagerConfiguration } from "../lib/ipfs/FileManager.js"; import { DefaultShell } from "../lib/Shell.js"; import { EncryptedFileWriter, EncryptedFileReader } from "../lib/crypto/EncryptedFile.js"; +import { ValidAccountId } from "@logion/node-api"; +import { DB_SS58_PREFIX } from "../model/supportedaccountid.model.js"; +import { requireDefined } from "@logion/rest-api-core"; export interface FileId { oid?: number @@ -22,13 +25,12 @@ export class FileStorageService { ipfsHost: process.env.IPFS_HOST!, }; this.fileManager = new DefaultFileManager(fileManagerConfiguration) - const password = process.env.ENC_PASSWORD! - this.encryptedFileWriter = new EncryptedFileWriter(password) - this.encryptedFileReader = new EncryptedFileReader(password) + this.encryptedFileWriter = new EncryptedFileWriter() + this.encryptedFileReader = new EncryptedFileReader() } - async importFile(path: string): Promise { - const encrypted = await this.encryptedFileWriter.encrypt({ clearFile: path }) + async importFile(path: string, legalOfficer: ValidAccountId): Promise { + const encrypted = await this.encryptedFileWriter.encrypt({ clearFile: path, password: this.getPassword(legalOfficer) }) return this.fileManager.moveToIpfs(encrypted) } @@ -36,13 +38,13 @@ export class FileStorageService { return await importFile(path, comment); } - async exportFile(id: FileId, path: string): Promise { + async exportFile(id: FileId, path: string, legalOfficer: ValidAccountId): Promise { if (id.oid) { return await exportFile(id.oid, path); } else if (id.cid) { const encryptedFile = `${path}.enc` await this.fileManager.downloadFromIpfs(id.cid, encryptedFile) - await this.encryptedFileReader.decrypt({ encryptedFile, clearFile: path }) + await this.encryptedFileReader.decrypt({ encryptedFile, clearFile: path, password: this.getPassword(legalOfficer) }) } else { throw new Error("File to download has no id") } @@ -61,4 +63,13 @@ export class FileStorageService { private readonly fileManager: FileManager; private readonly encryptedFileWriter: EncryptedFileWriter; private readonly encryptedFileReader: EncryptedFileReader; + + private getPassword(legalOfficer: ValidAccountId): string { + const propertyName = `ENC_PASSWORD_${ legalOfficer.getAddress(DB_SS58_PREFIX) }`; + const password = process.env[propertyName]; + return requireDefined( + password, + () => new Error(`Cannot encrypt/decrypt file. Property ${ propertyName } not found.`) + ) + } } diff --git a/src/logion/services/idenfy/idenfy.service.ts b/src/logion/services/idenfy/idenfy.service.ts index cc29ddf..0f8d0db 100644 --- a/src/logion/services/idenfy/idenfy.service.ts +++ b/src/logion/services/idenfy/idenfy.service.ts @@ -1,5 +1,5 @@ import { Log, requireDefined } from "@logion/rest-api-core"; -import { Hash } from "@logion/node-api"; +import { Hash, ValidAccountId } from "@logion/node-api"; import { AxiosError, AxiosInstance } from "axios"; import { injectable } from "inversify"; import { DateTime } from "luxon"; @@ -154,7 +154,7 @@ export class EnabledIdenfyService extends IdenfyService { request.updateIdenfyVerification(json, raw.toString()); for(const file of files) { request.addFile(file, "MANUAL_BY_USER"); - } + } }); } @@ -163,15 +163,15 @@ export class EnabledIdenfyService extends IdenfyService { const clientId = json.clientId; const request = requireDefined(await this.locRequestRepository.findById(clientId)); - const submitter = request.getOwner(); + const legalOfficer = request.getOwner(); const randomPrefix = DateTime.now().toMillis().toString(); - const { hash, cid, size } = await this.storePayload(randomPrefix, raw); + const { hash, cid, size } = await this.storePayload(randomPrefix, raw, legalOfficer); files.push({ contentType: "application/json", name: "idenfy-callback-payload.json", nature: "iDenfy Verification Result", - submitter, + submitter: legalOfficer, hash, cid, restrictedDelivery: false, @@ -181,11 +181,11 @@ export class EnabledIdenfyService extends IdenfyService { for(const fileType of IdenfyCallbackPayloadFileTypes) { const fileUrlString = json.fileUrls[fileType]; if(fileUrlString) { - const { fileName, hash, cid, contentType, size } = await this.storeFile(randomPrefix, fileUrlString); + const { fileName, hash, cid, contentType, size } = await this.storeFile(randomPrefix, fileUrlString, legalOfficer); files.push({ name: fileName, nature: `iDenfy ${ fileType }`, - submitter, + submitter: legalOfficer, contentType, hash, cid, @@ -198,21 +198,21 @@ export class EnabledIdenfyService extends IdenfyService { return files; } - private async storePayload(tempFileNamePrefix: string, raw: Buffer): Promise { + private async storePayload(tempFileNamePrefix: string, raw: Buffer, legalOfficer: ValidAccountId): Promise { const fileName = path.join(os.tmpdir(), `${ tempFileNamePrefix }-idenfy-callback-payload.json`); await writeFile(fileName, raw); - return await this.hashAndImport(fileName); + return await this.hashAndImport(fileName, legalOfficer); } - private async hashAndImport(fileName: string): Promise { + private async hashAndImport(fileName: string, legalOfficer: ValidAccountId): Promise { const hash = await sha256File(fileName); const stats = await stat(fileName); const size = stats.size; - const cid = await this.fileStorageService.importFile(fileName); + const cid = await this.fileStorageService.importFile(fileName, legalOfficer); return { hash, cid, size }; } - private async storeFile(tempFileNamePrefix: string, fileUrlString: string): Promise { + private async storeFile(tempFileNamePrefix: string, fileUrlString: string, legalOfficer: ValidAccountId): Promise { const fileUrl = new URL(fileUrlString); const fileUrlPath = fileUrl.pathname; const fileUrlPathElements = fileUrlPath.split("/"); @@ -238,7 +238,7 @@ export class EnabledIdenfyService extends IdenfyService { } }); }); - const ipfsFile = await this.hashAndImport(tempFileName); + const ipfsFile = await this.hashAndImport(tempFileName, legalOfficer); return { ...ipfsFile, fileName, diff --git a/test/unit/controllers/collection.controller.spec.ts b/test/unit/controllers/collection.controller.spec.ts index 10db215..694cd20 100644 --- a/test/unit/controllers/collection.controller.spec.ts +++ b/test/unit/controllers/collection.controller.spec.ts @@ -688,9 +688,9 @@ function mockModel( const fileStorageService = new Mock() const filePath = CollectionController.tempFilePath({ collectionLocId, itemId, hash: SOME_DATA_HASH }); - fileStorageService.setup(instance => instance.importFile(filePath)) + fileStorageService.setup(instance => instance.importFile(filePath, collectionLocOwner)) .returns(Promise.resolve(CID)) - fileStorageService.setup(instance => instance.exportFile({ cid: CID }, filePath)) + fileStorageService.setup(instance => instance.exportFile({ cid: CID }, filePath, collectionLocOwner)) .returns(Promise.resolve()) container.bind(FileStorageService).toConstantValue(fileStorageService.object()) diff --git a/test/unit/controllers/locrequest.controller.items.spec.ts b/test/unit/controllers/locrequest.controller.items.spec.ts index 3effb08..11d5db4 100644 --- a/test/unit/controllers/locrequest.controller.items.spec.ts +++ b/test/unit/controllers/locrequest.controller.items.spec.ts @@ -319,7 +319,7 @@ function mockModelForAddFile(container: Container, request: Mock instance.importFile(It.IsAny())) + fileStorageService.setup(instance => instance.importFile(It.IsAny(), It.IsAny())) .returns(Promise.resolve("cid-42")); } @@ -366,7 +366,7 @@ function mockModelForDownloadFile(container: Container, issuerMode: SetupIssuerM }); const filePath = "/tmp/download-" + REQUEST_ID + "-" + hash.toHex(); - fileStorageService.setup(instance => instance.exportFile({ oid: SOME_OID }, filePath)) + fileStorageService.setup(instance => instance.exportFile({ oid: SOME_OID }, filePath, ALICE_ACCOUNT)) .returns(Promise.resolve()); setupSelectedIssuer(loc, issuerMode); diff --git a/test/unit/controllers/lofile.controller.spec.ts b/test/unit/controllers/lofile.controller.spec.ts index 8408437..fd14a0f 100644 --- a/test/unit/controllers/lofile.controller.spec.ts +++ b/test/unit/controllers/lofile.controller.spec.ts @@ -15,7 +15,7 @@ import { LoFileService, NonTransactionalLoFileService } from "../../../src/logio import { ALICE, ALICE_ACCOUNT, BOB_ACCOUNT } from "../../helpers/addresses.js"; import { LegalOfficerSettingId } from "../../../src/logion/model/legalofficer.model.js"; import { mockAuthenticatedUser, mockAuthenticationWithAuthenticatedUser } from "@logion/rest-api-core/dist/TestApp.js"; -import { Hash } from "@logion/node-api"; +import { Hash, ValidAccountId } from "@logion/node-api"; const existingFile: LoFileDescription = { id: 'file1', @@ -158,7 +158,7 @@ function mockModel(container: Container): void { .returns(Promise.resolve(newFile.oid)); fileStorageService.setup(instance => instance.deleteFile(It.IsAny())) .returns(Promise.resolve()); - fileStorageService.setup(instance => instance.exportFile(It.IsAny(), It.IsAny())) + fileStorageService.setup(instance => instance.exportFile(It.IsAny(), It.IsAny(), It.IsAny())) .returns(Promise.resolve()) factory = new Mock(); diff --git a/test/unit/controllers/records.controller.spec.ts b/test/unit/controllers/records.controller.spec.ts index b61ed34..d58f5ac 100644 --- a/test/unit/controllers/records.controller.spec.ts +++ b/test/unit/controllers/records.controller.spec.ts @@ -436,9 +436,9 @@ function mockModel( const fileStorageService = new Mock() const filePath = TokensRecordController.tempFilePath({ collectionLocId, recordId, hash: SOME_DATA_HASH }); - fileStorageService.setup(instance => instance.importFile(filePath)) + fileStorageService.setup(instance => instance.importFile(filePath, collectionLocOwner)) .returns(Promise.resolve(CID)) - fileStorageService.setup(instance => instance.exportFile({ cid: CID }, filePath)) + fileStorageService.setup(instance => instance.exportFile({ cid: CID }, filePath, collectionLocOwner)) .returns(Promise.resolve()) container.bind(FileStorageService).toConstantValue(fileStorageService.object()) diff --git a/test/unit/lib/crypto/EncryptedFile.spec.ts b/test/unit/lib/crypto/EncryptedFile.spec.ts index e6d74f7..581bf93 100644 --- a/test/unit/lib/crypto/EncryptedFile.spec.ts +++ b/test/unit/lib/crypto/EncryptedFile.spec.ts @@ -11,13 +11,13 @@ const password = "secret"; describe("EncryptedFile", () => { it("encrypts and decrypts properly", async () => { - const writer = new EncryptedFileWriter(password); - await writer.open(tempFileName); + const writer = new EncryptedFileWriter(); + await writer.open(tempFileName, password); await writer.write(Buffer.from(clearText, 'utf-8')); await writer.close(); - const reader = new EncryptedFileReader(password); - await reader.open(tempFileName); + const reader = new EncryptedFileReader(); + await reader.open(tempFileName, password); const data = await reader.readAll(); await reader.close(); @@ -25,17 +25,17 @@ describe("EncryptedFile", () => { }); it("encrypts to file", async () => { - const writer = new EncryptedFileWriter(password); + const writer = new EncryptedFileWriter(); const clearFile = "test/unit/lib/crypto/assets.png"; - const encryptedFile = await writer.encrypt({ clearFile, keepSource: true }); + const encryptedFile = await writer.encrypt({ clearFile, keepSource: true, password }); expect(existsSync(encryptedFile)).toBeTrue(); const clearFileContent = readFileSync(clearFile); const encryptedFileContent = readFileSync(encryptedFile); expect(clearFileContent).not.toEqual(encryptedFileContent) - const reader = new EncryptedFileReader(password); - const decryptedFile = await reader.decrypt( { encryptedFile, keepSource: true }); + const reader = new EncryptedFileReader(); + const decryptedFile = await reader.decrypt( { encryptedFile, keepSource: true, password }); expect(existsSync(decryptedFile)).toBeTrue(); const decryptedFileContent = readFileSync(decryptedFile) diff --git a/test/unit/services/idenfy/idenfy.service.spec.ts b/test/unit/services/idenfy/idenfy.service.spec.ts index 80597ff..580f3a0 100644 --- a/test/unit/services/idenfy/idenfy.service.spec.ts +++ b/test/unit/services/idenfy/idenfy.service.spec.ts @@ -17,6 +17,7 @@ import { mockOwner } from "../../controllers/locrequest.controller.shared.js"; import { expectAsyncToThrow } from "../../../helpers/asynchelper.js"; import { FileDescription } from "src/logion/model/loc_items.js"; import { LocRequestDescription } from "src/logion/model/loc_vos.js"; +import { ValidAccountId } from "@logion/node-api"; describe("DisabledIdenfyService", () => { @@ -155,19 +156,19 @@ function mockEnabledIdenfyService(): { const fileStorageService = new Mock(); // In reverse order of actual addition by service, otherwise CIDs won't match fileStorageService - .setup(instance => instance.importFile(It.IsAny())) + .setup(instance => instance.importFile(It.IsAny(), It.Is(param => param.equals(LOC_OWNER_ACCOUNT)))) .play(PlayTimes.Once()) .returnsAsync(EXPECTED_FILES.FACE.cid!); fileStorageService - .setup(instance => instance.importFile(It.IsAny())) + .setup(instance => instance.importFile(It.IsAny(), It.Is(param => param.equals(LOC_OWNER_ACCOUNT)))) .play(PlayTimes.Once()) .returnsAsync(EXPECTED_FILES.BACK.cid!); fileStorageService - .setup(instance => instance.importFile(It.IsAny())) + .setup(instance => instance.importFile(It.IsAny(), It.Is(param => param.equals(LOC_OWNER_ACCOUNT)))) .play(PlayTimes.Once()) .returnsAsync(EXPECTED_FILES.FRONT.cid!); fileStorageService - .setup(instance => instance.importFile(It.IsAny())) + .setup(instance => instance.importFile(It.IsAny(), It.Is(param => param.equals(LOC_OWNER_ACCOUNT)))) .play(PlayTimes.Once()) .returnsAsync(EXPECTED_FILES.PAYLOAD.cid!);