Skip to content

Commit

Permalink
Merge pull request #3733 from CAFECA-IO/fix/mobilePhoneUpload
Browse files Browse the repository at this point in the history
add room key
  • Loading branch information
Luphia authored Dec 25, 2024
2 parents 11492e4 + 21943c5 commit ae9a341
Show file tree
Hide file tree
Showing 19 changed files with 426 additions and 84 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iSunFA",
"version": "0.8.5+229",
"version": "0.8.5+230",
"private": false,
"scripts": {
"dev": "next dev",
Expand Down
8 changes: 4 additions & 4 deletions src/components/certificate/certificate_edit_modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ const CertificateEditModal: React.FC<CertificateEditModalProps> = ({
);

const isFormValid = useCallback(() => {
const { no, date: formDate, priceBeforeTax, totalPrice, counterParty } = formState;
const { date: formDate, priceBeforeTax, totalPrice, counterParty } = formState;
return (
no &&
no.trim() !== '' &&
// no &&
// no.trim() !== '' &&
formDate &&
formDate > 0 &&
priceBeforeTax &&
Expand Down Expand Up @@ -265,7 +265,7 @@ const CertificateEditModal: React.FC<CertificateEditModalProps> = ({
<div id="price" className="absolute -top-20"></div>
<p className="text-sm font-semibold text-input-text-primary">
{t('certificate:EDIT.INVOICE_NUMBER')}
<span className="text-text-state-error">*</span>
{/* <span className="text-text-state-error">*</span> */}
</p>
<div className="flex w-full items-center">
<input
Expand Down
37 changes: 34 additions & 3 deletions src/components/invoice_upload.tsx/invoice_upload.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useModalContext } from '@/contexts/modal_context';
import APIHandler from '@/lib/utils/api_handler';
Expand All @@ -14,6 +14,7 @@ import { ToastId } from '@/constants/toast_id';
import { useUserCtx } from '@/contexts/user_context';
import { FREE_COMPANY_ID } from '@/constants/config';
import { compressImageToTargetSize } from '@/lib/utils/image_compress';
import { encryptFileWithPublicKey, importPublicKey } from '@/lib/utils/crypto';

interface InvoiceUploadProps {
isDisabled: boolean;
Expand All @@ -32,8 +33,10 @@ const InvoiceUpload: React.FC<InvoiceUploadProps> = ({
const { selectedCompany } = useUserCtx();
const { toastHandler, messageModalDataHandler, messageModalVisibilityHandler } =
useModalContext();
const [publicKey, setPublicKey] = useState<CryptoKey | null>(null);
const { trigger: uploadFileAPI } = APIHandler<IFileUIBeta>(APIName.FILE_UPLOAD);
const { trigger: createCertificateAPI } = APIHandler<ICertificate>(APIName.CERTIFICATE_POST_V2);
const { trigger: fetchPublicKey } = APIHandler<JsonWebKey>(APIName.PUBLIC_KEY_GET);

const handleUploadCancelled = useCallback(() => {
setFiles([]);
Expand Down Expand Up @@ -75,11 +78,39 @@ const InvoiceUpload: React.FC<InvoiceUploadProps> = ({
]
);

const encryptFileWithKey = async (file: File) => {
try {
let key = publicKey;
if (!key) {
const { success, data } = await fetchPublicKey({
params: { companyId: selectedCompany?.id ?? FREE_COMPANY_ID },
});
if (!success || !data) {
throw new Error(t('certificate:UPLOAD.FAILED'));
}
const cryptokey = await importPublicKey(data);
setPublicKey(cryptokey);
key = cryptokey;
}
const { encryptedFile, iv, encryptedSymmetricKey } = await encryptFileWithPublicKey(
file,
key
);
const formData = new FormData();
formData.append('file', encryptedFile);
formData.append('encryptedSymmetricKey', encryptedSymmetricKey);
formData.append('publicKey', JSON.stringify(key));
formData.append('iv', Array.from(iv).join(','));
return formData;
} catch (error) {
throw new Error(t('certificate:ERROR.ENCRYPT_FILE'));
}
};

const handleUpload = useCallback(
async (file: File) => {
try {
const formData = new FormData();
formData.append('file', file);
const formData = await encryptFileWithKey(file);
const targetSize = 1 * 1024 * 1024; // Info: (20241206 - tzuhan) 1MB
const maxSize = 4 * 1024 * 1024;
if (file.size > maxSize) {
Expand Down
7 changes: 7 additions & 0 deletions src/constants/api_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export enum APIName {
REPORT_GENERATE = 'REPORT_GENERATE',
ROOM_ADD = 'ROOM_ADD',
ROOM_GET_BY_ID = 'ROOM_GET_BY_ID',
ROOM_GET_PUBLIC_KEY_BY_ID = 'ROOM_GET_PUBLIC_KEY_BY_ID',
ROOM_DELETE = 'ROOM_DELETE',
STATUS_INFO_GET = 'STATUS_INFO_GET',
ACCOUNT_LIST = 'ACCOUNT_LIST',
Expand Down Expand Up @@ -230,6 +231,7 @@ export enum APIPath {
REPORT_GENERATE = `${apiPrefixV2}/company/:companyId/report/public`,
ROOM_ADD = `${apiPrefixV2}/room`,
ROOM_GET_BY_ID = `${apiPrefixV2}/room/:roomId`,
ROOM_GET_PUBLIC_KEY_BY_ID = `${apiPrefixV2}/room/:roomId/key`,
ROOM_DELETE = `${apiPrefixV2}/room/:roomId`,
STATUS_INFO_GET = `${apiPrefixV2}/status_info`,
ACCOUNT_LIST = `${apiPrefixV2}/company/:companyId/account`,
Expand Down Expand Up @@ -550,6 +552,11 @@ export const APIConfig: Record<IAPIName, IAPIConfig> = {
method: HttpMethod.DELETE,
path: APIPath.ROOM_DELETE,
}),
[APIName.ROOM_GET_PUBLIC_KEY_BY_ID]: createConfig({
name: APIName.ROOM_GET_PUBLIC_KEY_BY_ID,
method: HttpMethod.GET,
path: APIPath.ROOM_GET_PUBLIC_KEY_BY_ID,
}),
[APIName.LABOR_COST_CHART]: createConfig({
name: APIName.LABOR_COST_CHART,
method: HttpMethod.GET,
Expand Down
2 changes: 2 additions & 0 deletions src/constants/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum AuthFunctionsKeysNew {

export const AUTH_WHITELIST = {
[APIName.FILE_UPLOAD]: { query: { type: UploadType.ROOM } },
[APIName.ROOM_GET_PUBLIC_KEY_BY_ID]: { query: {} },
[APIName.STATUS_INFO_GET]: { query: {} },
[APIName.REPORT_GET_BY_ID]: { query: {} },
};
Expand Down Expand Up @@ -71,6 +72,7 @@ export const AUTH_CHECK = {
[APIName.FILE_EXPORT]: [AuthFunctionsKeysNew.user], // ToDo: (20241112 - Luphia) need to define the schema for file export
[APIName.ROOM_ADD]: [AuthFunctionsKeysNew.user],
[APIName.ROOM_GET_BY_ID]: [AuthFunctionsKeysNew.user],
[APIName.ROOM_GET_PUBLIC_KEY_BY_ID]: [AuthFunctionsKeysNew.user],
[APIName.ROOM_DELETE]: [AuthFunctionsKeysNew.user],

[APIName.AGREE_TO_TERMS]: [AuthFunctionsKeysNew.user],
Expand Down
8 changes: 7 additions & 1 deletion src/constants/zod_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ import { roleListSchema } from '@/lib/utils/zod_schema/role';
import { assetExportSchema } from '@/lib/utils/zod_schema/export_asset';
import { nullAPISchema } from '@/lib/utils/zod_schema/common';
import { ledgerListSchema } from '@/lib/utils/zod_schema/ledger';
import { roomDeleteSchema, roomGetSchema, roomPostSchema } from '@/lib/utils/zod_schema/room';
import {
roomDeleteSchema,
roomGetPublicKeySchema,
roomGetSchema,
roomPostSchema,
} from '@/lib/utils/zod_schema/room';
import {
fileDeleteSchema,
fileGetSchema,
Expand Down Expand Up @@ -185,6 +190,7 @@ export const ZOD_SCHEMA_API = {
[APIName.ROLE_LIST]: roleListSchema,
[APIName.ROOM_ADD]: roomPostSchema,
[APIName.ROOM_GET_BY_ID]: roomGetSchema,
[APIName.ROOM_GET_PUBLIC_KEY_BY_ID]: roomGetPublicKeySchema,
[APIName.ROOM_DELETE]: roomDeleteSchema,
[APIName.NEWS_LIST]: newsListSchema,
[APIName.CREATE_NEWS]: newsPostSchema,
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/api_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type IAPIName =
| 'ROOM_ADD'
| 'ROOM_GET_BY_ID'
| 'ROOM_DELETE'
| 'ROOM_GET_PUBLIC_KEY_BY_ID'
| 'STATUS_INFO_GET'
| 'ACCOUNT_LIST'
| 'FILE_UPLOAD'
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ export interface IRoom {
password: string;
fileList: IFileBeta[];
}

export interface IRoomWithPrivateData extends IRoom {
companyId: number;
}
88 changes: 67 additions & 21 deletions src/lib/utils/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@/constants/crypto';
import { promises as fs } from 'fs';
import path from 'path';
import { IV_LENGTH } from '@/constants/config';

/* Info: (20240822 - Shirley)
- 實作混合加密 (hybrid encryption),用對稱加密密鑰將檔案加密,用非對稱加密的 public key 加密對稱密鑰,用非對稱加密的 private key 解密對稱密鑰
Expand Down Expand Up @@ -37,21 +38,23 @@ const sssSecret = new SSSSecret();
/**
* Info: (20240830 - Murky)
* Postgres can only store bytea for "iv" so we need to convert it to Uint8Array
* @param uint8Array - The Uint8Array to convert to Buffer
* @returns Buffer - The Buffer converted from Uint8Array
* @param buffer - The Buffer to convert to Uint8Array
* @returns Uint8Array - The Uint8Array converted from Buffer
*/
export function uint8ArrayToBuffer(uint8Array: Uint8Array): Buffer {
return Buffer.from(uint8Array);
export function bufferToUint8Array(buffer: Buffer): Uint8Array {
return new Uint8Array(buffer);
}

/**
* Info: (20240830 - Murky)
* Postgres can only store bytea for "iv" so we need to convert it to Uint8Array
* @param buffer - The Buffer to convert to Uint8Array
* @returns Uint8Array - The Uint8Array converted from Buffer
* @param uint8Array - The Uint8Array to convert to Buffer
* @returns Buffer - The Buffer converted from Uint8Array
*/
export function bufferToUint8Array(buffer: Buffer): Uint8Array {
return new Uint8Array(buffer);
export function uint8ArrayToBuffer(uint8Array: Uint8Array): Buffer {
const buffer = Buffer.from(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength);

return buffer;
}

/*
Expand Down Expand Up @@ -126,6 +129,7 @@ export async function decryptData(
privateKey: CryptoKey
): Promise<string> {
const decoder = new TextDecoder();

const decryptedData = await crypto.subtle.decrypt(
{
name: ASYMMETRIC_CRYPTO_ALGORITHM,
Expand Down Expand Up @@ -179,21 +183,48 @@ export const decryptFile = async (
privateKey: CryptoKey,
iv: Uint8Array
): Promise<ArrayBuffer> => {
const decryptedSymmetricKeyJSON = await decrypt(encryptedSymmetricKey, privateKey);
let decryptedSymmetricKeyJSON: string;

try {
decryptedSymmetricKeyJSON = await decrypt(encryptedSymmetricKey, privateKey);
} catch (error) {
// Deprecated: (20241225 - Murky) crypto.ts 前端也要用,所以不能用logger
// eslint-disable-next-line no-console
console.error(error);
throw new Error('Failed to decrypt symmetric key');
}
const decryptedSymmetricKey = new Uint8Array(JSON.parse(decryptedSymmetricKeyJSON)).buffer;
const importedSymmetricKey = await crypto.subtle.importKey(
SYMMETRIC_KEY_FORMAT,
decryptedSymmetricKey,
{ name: SYMMETRIC_CRYPTO_ALGORITHM, length: SYMMETRIC_KEY_LENGTH },
true,
[CryptoOperationMode.DECRYPT]
);

const decryptedContent = await crypto.subtle.decrypt(
{ name: SYMMETRIC_CRYPTO_ALGORITHM, iv },
importedSymmetricKey,
encryptedContent
);
let importedSymmetricKey: CryptoKey;

try {
importedSymmetricKey = await crypto.subtle.importKey(
SYMMETRIC_KEY_FORMAT,
decryptedSymmetricKey,
{ name: SYMMETRIC_CRYPTO_ALGORITHM, length: SYMMETRIC_KEY_LENGTH },
true,
[CryptoOperationMode.DECRYPT]
);
} catch (error) {
// Deprecated: (20241225 - Murky) crypto.ts 前端也要用,所以不能用logger
// eslint-disable-next-line no-console
console.error(error);
throw new Error('Failed to import symmetric key');
}

let decryptedContent: ArrayBuffer;
try {
decryptedContent = await crypto.subtle.decrypt(
{ name: SYMMETRIC_CRYPTO_ALGORITHM, iv },
importedSymmetricKey,
encryptedContent
);
} catch (error) {
// Deprecated: (20241225 - Murky) crypto.ts 前端也要用,所以不能用logger
// eslint-disable-next-line no-console
console.error(error);
throw new Error('Failed to decrypt content');
}

return decryptedContent;
};
Expand Down Expand Up @@ -331,3 +362,18 @@ export function arrayBufferToBuffer(arrayBuffer: ArrayBuffer): Buffer {
}
return buffer;
}

export const encryptFileWithPublicKey = async (file: File, publicKey: CryptoKey) => {
const arrayBuffer = await file.arrayBuffer();
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const { encryptedContent, encryptedSymmetricKey } = await encryptFile(arrayBuffer, publicKey, iv);
const encryptedFile = new File([encryptedContent], file.name, {
type: file.type,
});

return {
encryptedFile,
iv,
encryptedSymmetricKey,
};
};
Loading

0 comments on commit ae9a341

Please sign in to comment.