Skip to content

Commit

Permalink
Merge pull request #312 from logion-network/fix/enc-pwd-per-llo
Browse files Browse the repository at this point in the history
One enc pass per LLO.
  • Loading branch information
benoitdevos authored Jun 24, 2024
2 parents f11488d + 989626f commit 7295911
Show file tree
Hide file tree
Showing 14 changed files with 78 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down
12 changes: 8 additions & 4 deletions src/logion/controllers/collection.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/logion/controllers/locrequest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/logion/controllers/lofile.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions src/logion/controllers/records.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
Expand Down Expand Up @@ -297,6 +297,8 @@ export class TokensRecordController extends ApiController {
@Async()
@SendsResponse()
async downloadItemFile(_body: never, collectionLocId: string, recordIdHex: string, hashHex: string, itemIdHex: string): Promise<void> {
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);
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
23 changes: 9 additions & 14 deletions src/logion/lib/crypto/EncryptedFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,23 @@ 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<void> {
async open(fileName: string, password: string): Promise<void> {
this.file = await open(fileName, 'w');

return new Promise<void>((resolve, reject) => {
randomFill(new Uint8Array(SALT_LENGTH), (err, salt) => {
if (err) {
reject(err);
}
scrypt(this.password, salt, KEY_LENGTH, (err, key) => {
scrypt(password, salt, KEY_LENGTH, (err, key) => {
if (err) {
reject(err);
}
Expand Down Expand Up @@ -82,7 +80,7 @@ export class EncryptedFileWriter {
async encrypt(props: EncryptDecryptProps): Promise<string> {
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) {
Expand All @@ -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<void> {
async open(fileName: string, password: string): Promise<void> {
this.file = await open(fileName, 'r');

const salt = new Uint8Array(SALT_LENGTH);
Expand All @@ -111,7 +106,7 @@ export class EncryptedFileReader {
await this.file.read(iv, 0, IV_LENGTH);

return new Promise<void>((resolve, reject) => {
scrypt(this.password, salt, KEY_LENGTH, (err, key) => {
scrypt(password, salt, KEY_LENGTH, (err, key) => {
if (err) {
reject(err);
}
Expand Down Expand Up @@ -167,7 +162,7 @@ export class EncryptedFileReader {
async decrypt(props: EncryptDecryptProps): Promise<string> {
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) {
Expand Down
25 changes: 18 additions & 7 deletions src/logion/services/file.storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,27 +25,26 @@ 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<string> {
const encrypted = await this.encryptedFileWriter.encrypt({ clearFile: path })
async importFile(path: string, legalOfficer: ValidAccountId): Promise<string> {
const encrypted = await this.encryptedFileWriter.encrypt({ clearFile: path, password: this.getPassword(legalOfficer) })
return this.fileManager.moveToIpfs(encrypted)
}

async importFileInDB(path: string, comment: string): Promise<number> {
return await importFile(path, comment);
}

async exportFile(id: FileId, path: string): Promise<void> {
async exportFile(id: FileId, path: string, legalOfficer: ValidAccountId): Promise<void> {
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")
}
Expand All @@ -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.`)
)
}
}
26 changes: 13 additions & 13 deletions src/logion/services/idenfy/idenfy.service.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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");
}
}
});
}

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -198,21 +198,21 @@ export class EnabledIdenfyService extends IdenfyService {
return files;
}

private async storePayload(tempFileNamePrefix: string, raw: Buffer): Promise<IPFSFile> {
private async storePayload(tempFileNamePrefix: string, raw: Buffer, legalOfficer: ValidAccountId): Promise<IPFSFile> {
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<IPFSFile> {
private async hashAndImport(fileName: string, legalOfficer: ValidAccountId): Promise<IPFSFile> {
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<IPFSFile & { fileName: string, contentType: string }> {
private async storeFile(tempFileNamePrefix: string, fileUrlString: string, legalOfficer: ValidAccountId): Promise<IPFSFile & { fileName: string, contentType: string }> {
const fileUrl = new URL(fileUrlString);
const fileUrlPath = fileUrl.pathname;
const fileUrlPathElements = fileUrlPath.split("/");
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions test/unit/controllers/collection.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,9 +688,9 @@ function mockModel(

const fileStorageService = new Mock<FileStorageService>()
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())

Expand Down
4 changes: 2 additions & 2 deletions test/unit/controllers/locrequest.controller.items.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ function mockModelForAddFile(container: Container, request: Mock<LocRequestAggre

setupSelectedIssuer(loc, issuerMode);

fileStorageService.setup(instance => instance.importFile(It.IsAny<string>()))
fileStorageService.setup(instance => instance.importFile(It.IsAny<string>(), It.IsAny<ValidAccountId>()))
.returns(Promise.resolve("cid-42"));
}

Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions test/unit/controllers/lofile.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -158,7 +158,7 @@ function mockModel(container: Container): void {
.returns(Promise.resolve(newFile.oid));
fileStorageService.setup(instance => instance.deleteFile(It.IsAny<FileId>()))
.returns(Promise.resolve());
fileStorageService.setup(instance => instance.exportFile(It.IsAny<FileId>(), It.IsAny<String>()))
fileStorageService.setup(instance => instance.exportFile(It.IsAny<FileId>(), It.IsAny<String>(), It.IsAny<ValidAccountId>()))
.returns(Promise.resolve())

factory = new Mock<LoFileFactory>();
Expand Down
4 changes: 2 additions & 2 deletions test/unit/controllers/records.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,9 +436,9 @@ function mockModel(

const fileStorageService = new Mock<FileStorageService>()
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())

Expand Down
Loading

0 comments on commit 7295911

Please sign in to comment.