From 5a20b3900f9d5d2637e465f0006faab7f2d99171 Mon Sep 17 00:00:00 2001 From: jinoosss Date: Wed, 29 Jan 2025 10:39:31 +0900 Subject: [PATCH] feat: improve password encryption methods --- packages/adena-extension/src/background.ts | 15 +++- .../common/constants/command-key.constant.ts | 7 ++ .../common/provider/memory/memory-provider.ts | 15 ++++ .../src/common/utils/crypto-utils.ts | 57 ++++++++------ .../src/inject/message/command-handler.ts | 73 ++++++++++++++++++ .../src/inject/message/command-message.ts | 61 +++++++++++++++ .../src/inject/message/commands/encrypt.ts | 77 +++++++++++++++++++ .../src/inject/message/message-handler.ts | 5 +- .../src/repositories/wallet/wallet.ts | 20 ++++- 9 files changed, 300 insertions(+), 30 deletions(-) create mode 100644 packages/adena-extension/src/common/constants/command-key.constant.ts create mode 100644 packages/adena-extension/src/common/provider/memory/memory-provider.ts create mode 100644 packages/adena-extension/src/inject/message/command-handler.ts create mode 100644 packages/adena-extension/src/inject/message/command-message.ts create mode 100644 packages/adena-extension/src/inject/message/commands/encrypt.ts diff --git a/packages/adena-extension/src/background.ts b/packages/adena-extension/src/background.ts index 0375d0c8..a04b96a4 100644 --- a/packages/adena-extension/src/background.ts +++ b/packages/adena-extension/src/background.ts @@ -1,6 +1,12 @@ +import { MemoryProvider } from '@common/provider/memory/memory-provider'; import { ChromeLocalStorage } from '@common/storage'; +import { CommandHandler } from '@inject/message/command-handler'; +import { isCommandMessageData } from '@inject/message/command-message'; import { MessageHandler } from './inject/message'; +const inMemoryProvider = new MemoryProvider(); +inMemoryProvider.init(); + function existsWallet(): Promise { const storage = new ChromeLocalStorage(); return storage @@ -44,4 +50,11 @@ chrome.action.onClicked.addListener(async () => { }); }); -chrome.runtime.onMessage.addListener(MessageHandler.createHandler); +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (isCommandMessageData(message)) { + CommandHandler.createHandler(inMemoryProvider, message, sender, sendResponse); + return true; + } + + return MessageHandler.createHandler(message, sender, sendResponse); +}); diff --git a/packages/adena-extension/src/common/constants/command-key.constant.ts b/packages/adena-extension/src/common/constants/command-key.constant.ts new file mode 100644 index 00000000..ea007b54 --- /dev/null +++ b/packages/adena-extension/src/common/constants/command-key.constant.ts @@ -0,0 +1,7 @@ +export const COMMAND_KEYS = { + encryptPassword: 'ENCRYPT_PASSWORD', + decryptPassword: 'DECRYPT_PASSWORD', + clearEncryptKey: 'CLEAR_ENCRYPT_KEY', +} as const; +export type CommandKeyType = keyof typeof COMMAND_KEYS; +export type CommandValueType = (typeof COMMAND_KEYS)[keyof typeof COMMAND_KEYS]; diff --git a/packages/adena-extension/src/common/provider/memory/memory-provider.ts b/packages/adena-extension/src/common/provider/memory/memory-provider.ts new file mode 100644 index 00000000..9495ceee --- /dev/null +++ b/packages/adena-extension/src/common/provider/memory/memory-provider.ts @@ -0,0 +1,15 @@ +export class MemoryProvider { + private memory: Map = new Map(); + + public get = (key: string): T => { + return this.memory.get(key) as T; + }; + + public set = (key: string, value: T): void => { + this.memory.set(key, value); + }; + + public async init(): Promise { + this.memory = new Map(); + } +} diff --git a/packages/adena-extension/src/common/utils/crypto-utils.ts b/packages/adena-extension/src/common/utils/crypto-utils.ts index 8a76eeac..ae1a2533 100644 --- a/packages/adena-extension/src/common/utils/crypto-utils.ts +++ b/packages/adena-extension/src/common/utils/crypto-utils.ts @@ -1,37 +1,46 @@ +import { CommandMessage, CommandMessageData } from '@inject/message/command-message'; import CryptoJS from 'crypto-js'; -import { v4 as uuidv4 } from 'uuid'; - -// Static cipher key used for encrypting the cryptographic key -const ENCRYPT_CIPHER_KEY = 'r3v4'; export const encryptSha256Password = (password: string): string => { return CryptoJS.SHA256(password).toString(); }; -// Encrypts a password with a dynamically generated key and returns the encrypted key and password -export const encryptPassword = ( +// Sends a message to the background script to encrypt a password +export const encryptPassword = async ( password: string, -): { encryptedKey: string; encryptedPassword: string } => { - const cryptKey = uuidv4(); - const adenaKey = ENCRYPT_CIPHER_KEY; - const encryptedKey = CryptoJS.AES.encrypt(cryptKey, adenaKey).toString(); - const encryptedPassword = CryptoJS.AES.encrypt(password, cryptKey).toString(); +): Promise<{ encryptedKey: string; encryptedPassword: string }> => { + const result = await sendMessage(CommandMessage.command('encryptPassword', { password })); + if (!result.data) { + throw new Error('Encryption key not initialized.'); + } + return { - encryptedKey, - encryptedPassword, + encryptedKey: result.data.encryptedKey, + encryptedPassword: result.data.encryptedPassword, }; }; -// Decrypts a password using the encrypted key and password -export const decryptPassword = (encryptedKey: string, encryptedPassword: string): string => { - const adenaKey = ENCRYPT_CIPHER_KEY; - const key = CryptoJS.AES.decrypt(encryptedKey, adenaKey).toString(CryptoJS.enc.Utf8); - if (key === '') { - throw new Error('CipherKey Decryption Failed'); - } - const password = CryptoJS.AES.decrypt(encryptedPassword, key).toString(CryptoJS.enc.Utf8); - if (password === '') { - throw new Error('Password Decryption Failed'); +// Sends a message to the background script to encrypt a password +export const decryptPassword = async (iv: string, encryptedPassword: string): Promise => { + const result = await sendMessage( + CommandMessage.command('decryptPassword', { + iv, + encryptedPassword, + }), + ); + if (!result.data) { + throw new Error('Encryption key not initialized.'); } - return password; + + return result.data.password; +}; + +export const clearInMemoryKey = async (): Promise => { + await sendMessage(CommandMessage.command('clearEncryptKey')); }; + +function sendMessage(message: CommandMessageData): Promise> { + return new Promise((resolve) => { + chrome.runtime.sendMessage(message, resolve); + }); +} diff --git a/packages/adena-extension/src/inject/message/command-handler.ts b/packages/adena-extension/src/inject/message/command-handler.ts new file mode 100644 index 00000000..231cd861 --- /dev/null +++ b/packages/adena-extension/src/inject/message/command-handler.ts @@ -0,0 +1,73 @@ +import { MemoryProvider } from '@common/provider/memory/memory-provider'; +import { CommandMessageData } from './command-message'; +import { + clearInMemoryKey, + decryptPassword, + encryptPassword, + getInMemoryKey, +} from './commands/encrypt'; + +export class CommandHandler { + public static createHandler = async ( + inMemoryProvider: MemoryProvider, + message: CommandMessageData, + _: chrome.runtime.MessageSender, + sendResponse: (response?: CommandMessageData) => void, + ): Promise => { + if (message.code !== 0) { + return; + } + + if (message.command === 'encryptPassword') { + const key = await getInMemoryKey(inMemoryProvider); + if (!key) { + sendResponse({ + ...message, + code: 500, + }); + return; + } + + const password = message.data.password; + const resultData = await encryptPassword(key, password); + + sendResponse({ + ...message, + code: 200, + data: resultData, + }); + + return; + } + + if (message.command === 'decryptPassword') { + const key = await getInMemoryKey(inMemoryProvider); + if (!key) { + sendResponse({ + ...message, + code: 500, + }); + return; + } + + const iv = message.data.iv; + const encryptedPassword = message.data.encryptedPassword; + const decryptedPassword = await decryptPassword(key, iv, encryptedPassword); + + sendResponse({ + ...message, + code: 200, + data: { + password: decryptedPassword, + }, + }); + return; + } + + if (message.command === 'clearEncryptKey') { + await clearInMemoryKey(inMemoryProvider); + sendResponse({ ...message, code: 200 }); + return; + } + }; +} diff --git a/packages/adena-extension/src/inject/message/command-message.ts b/packages/adena-extension/src/inject/message/command-message.ts new file mode 100644 index 00000000..7e39c3a9 --- /dev/null +++ b/packages/adena-extension/src/inject/message/command-message.ts @@ -0,0 +1,61 @@ +import { CommandKeyType } from '@common/constants/command-key.constant'; + +type StatusType = 'command'; + +export function isCommandMessageData(data: any): data is CommandMessageData { + return data.status === 'command'; +} + +export interface CommandMessageData { + key: string; + code: number; + status: StatusType; + command: CommandKeyType; + data: T; +} + +export class CommandMessage { + private code: number; + + private key: string; + + private status: StatusType; + + private command: CommandKeyType; + + private data: any; + + constructor(command: CommandKeyType, data?: any, key?: string) { + this.code = 0; + this.key = key ?? ''; + this.command = command; + this.status = 'command'; + this.data = data; + } + + public get message(): CommandMessageData { + return { + code: this.code, + key: this.key, + status: this.status, + command: this.command, + data: this.data, + }; + } + + public getCommand = (): CommandKeyType => { + return this.command; + }; + + public getStatus = (): 'command' => { + return this.status; + }; + + public getData = (): any => { + return this.data; + }; + + public static command = (command: CommandKeyType, data?: any): CommandMessageData => { + return new CommandMessage(command, data).message; + }; +} diff --git a/packages/adena-extension/src/inject/message/commands/encrypt.ts b/packages/adena-extension/src/inject/message/commands/encrypt.ts new file mode 100644 index 00000000..2d8dc899 --- /dev/null +++ b/packages/adena-extension/src/inject/message/commands/encrypt.ts @@ -0,0 +1,77 @@ +import { MemoryProvider } from '@common/provider/memory/memory-provider'; + +const MEMORY_KEY = 'encryptKey'; + +const KEY_LENGTH = 256; // AES-256 key length +const IV_LENGTH = 12; // GCM nonce length (12 bytes is recommended) + +export async function getInMemoryKey(memoryProvider: MemoryProvider): Promise { + const key = memoryProvider.get(MEMORY_KEY) || null; + if (!key) { + const generated = await generateInMemoryKey(); + memoryProvider.set(MEMORY_KEY, generated); + } + + return memoryProvider.get(MEMORY_KEY) || null; +} + +export async function clearInMemoryKey(memoryProvider: MemoryProvider): Promise { + const random = await generateInMemoryKey(); + memoryProvider.set(MEMORY_KEY, random); + memoryProvider.set(MEMORY_KEY, null); +} + +// Encrypts a password using AES-GCM +export const encryptPassword = async ( + key: CryptoKey, + password: string, +): Promise<{ encryptedKey: string; encryptedPassword: string }> => { + const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); + const enc = new TextEncoder(); + const encrypted = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + }, + key, + enc.encode(password), + ); + + return { + encryptedKey: Buffer.from(iv).toString('base64'), + encryptedPassword: Buffer.from(encrypted).toString('base64'), + }; +}; + +// Decrypts a password using AES-GCM +export const decryptPassword = async ( + key: CryptoKey, + iv: string, + encryptedPassword: string, +): Promise => { + const encryptedData = Buffer.from(encryptedPassword, 'base64'); + const ivBytes = Buffer.from(iv, 'base64'); + const dec = new TextDecoder(); + + const decrypted = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: ivBytes, + }, + key, + encryptedData, + ); + + return dec.decode(decrypted); +}; + +const generateInMemoryKey = async (): Promise => { + return crypto.subtle.generateKey( + { + name: 'AES-GCM', + length: KEY_LENGTH, + }, + true, + ['encrypt', 'decrypt'], + ); +}; diff --git a/packages/adena-extension/src/inject/message/message-handler.ts b/packages/adena-extension/src/inject/message/message-handler.ts index 23d8e3e6..c387d763 100644 --- a/packages/adena-extension/src/inject/message/message-handler.ts +++ b/packages/adena-extension/src/inject/message/message-handler.ts @@ -1,14 +1,15 @@ import { WalletResponseFailureType } from '@adena-wallet/sdk'; import { HandlerMethod } from '.'; +import { CommandMessageData } from './command-message'; import { InjectionMessage, InjectionMessageInstance } from './message'; import { existsPopups, removePopups } from './methods'; import { InjectCore } from './methods/core'; export class MessageHandler { public static createHandler = ( - message: InjectionMessage | any, + message: InjectionMessage | CommandMessageData | any, sender: chrome.runtime.MessageSender, - sendResponse: (response?: InjectionMessage | any) => void, + sendResponse: (response?: InjectionMessage | CommandMessageData | any) => void, ): boolean => { try { if (message?.status) { diff --git a/packages/adena-extension/src/repositories/wallet/wallet.ts b/packages/adena-extension/src/repositories/wallet/wallet.ts index bb379b4f..abee6e34 100644 --- a/packages/adena-extension/src/repositories/wallet/wallet.ts +++ b/packages/adena-extension/src/repositories/wallet/wallet.ts @@ -1,8 +1,9 @@ import { WalletError } from '@common/errors'; import { StorageManager } from '@common/storage/storage-manager'; import { - encryptPassword, + clearInMemoryKey, decryptPassword, + encryptPassword, encryptSha256Password, } from '@common/utils/crypto-utils'; @@ -29,11 +30,13 @@ export class WalletRepository { if (!serializedWallet || serializedWallet === '') { throw new WalletError('NOT_FOUND_SERIALIZED'); } + return serializedWallet; }; public updateSerializedWallet = async (serializedWallet: string): Promise => { await this.localStorage.set('SERIALIZED', serializedWallet); + return true; }; @@ -41,6 +44,7 @@ export class WalletRepository { await this.localStorage.remove('SERIALIZED'); await this.localStorage.remove('ENCRYPTED_STORED_PASSWORD'); await this.localStorage.remove('QUESTIONNAIRE_EXPIRED_DATE'); + return true; }; @@ -66,8 +70,9 @@ export class WalletRepository { } try { - const password = decryptPassword(encryptedKey, encryptedPassword); + const password = await decryptPassword(encryptedKey, encryptedPassword); this.updateStoragePassword(password); + return password; } catch (e) { throw new WalletError('NOT_FOUND_PASSWORD'); @@ -75,18 +80,24 @@ export class WalletRepository { }; public updateWalletPassword = async (password: string): Promise => { - const { encryptedKey, encryptedPassword } = encryptPassword(password); + const { encryptedKey, encryptedPassword } = await encryptPassword(password); const storedPassword = encryptSha256Password(password); + this.updateStoragePassword(password); + await this.localStorage.set('ENCRYPTED_STORED_PASSWORD', storedPassword); await this.sessionStorage.set('ENCRYPTED_KEY', encryptedKey); await this.sessionStorage.set('ENCRYPTED_PASSWORD', encryptedPassword); + return true; }; public deleteWalletPassword = async (): Promise => { + await clearInMemoryKey(); + await this.sessionStorage.remove('ENCRYPTED_KEY'); await this.sessionStorage.remove('ENCRYPTED_PASSWORD'); + return true; }; @@ -104,6 +115,7 @@ export class WalletRepository { if (!expiredDateTime) { return null; } + return Number(expiredDateTime); }; @@ -116,6 +128,7 @@ export class WalletRepository { if (!confirmDate) { return null; } + return Number(confirmDate); }; @@ -128,6 +141,7 @@ export class WalletRepository { if (!confirmDate) { return null; } + return Number(confirmDate); };