- xkeypad 보안 키패드 라이브러리 추가 - 비밀번호 변경 페이지에 보안 키패드 적용 - RSA 암호화 기능 통합 - route 설정 및 Sentry 설정 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
656 lines
17 KiB
TypeScript
656 lines
17 KiB
TypeScript
// XKModule 및 관련 타입 정의
|
|
declare global {
|
|
interface Window {
|
|
XKModule: any;
|
|
XKConfigMobile: {
|
|
maxInputSize: number;
|
|
rsaPublicKey?: {
|
|
n: string;
|
|
e: string;
|
|
};
|
|
};
|
|
RSASetPublic: (N: string, E: string) => void;
|
|
RSAEncrypt: (text: string) => string;
|
|
}
|
|
}
|
|
|
|
export interface XKSessionInfo {
|
|
sessionId: string;
|
|
input: string;
|
|
secToken: string;
|
|
}
|
|
|
|
export interface XKModuleInstance {
|
|
initialize: (options: any) => number;
|
|
open: () => void;
|
|
close: () => void;
|
|
isOpen: () => boolean;
|
|
clear: () => void;
|
|
destroy: () => void;
|
|
get_input: () => string;
|
|
get_sessionInfo: () => XKSessionInfo;
|
|
setRSAPublicKey: (n: string, e: string) => void;
|
|
}
|
|
|
|
export type KeyType = 'qwertysmart' | 'number';
|
|
export type ViewType = 'half' | 'normal';
|
|
export type NumberKeyRowCount = 2 | 3 | 4;
|
|
|
|
export interface XKeypadOptions {
|
|
keyType?: KeyType;
|
|
viewType?: ViewType;
|
|
numberKeyRowCount?: NumberKeyRowCount;
|
|
maxInputSize?: number;
|
|
width?: number;
|
|
position?: { top?: number | null; left?: number | null };
|
|
closeDelay?: number;
|
|
autoKeyResize?: boolean;
|
|
isE2E?: boolean;
|
|
onlyMobile?: boolean;
|
|
hasPressEffect?: boolean;
|
|
useModal?: boolean;
|
|
useOverlay?: boolean; // 오버레이 사용 여부
|
|
onInputChange?: (newLength: number) => void;
|
|
onKeypadClose?: () => void;
|
|
}
|
|
|
|
export interface XKeypadResult {
|
|
type: 'E2E' | 'Plain';
|
|
sessionId?: string;
|
|
encryptedInput?: string;
|
|
secToken?: string;
|
|
plainText?: string;
|
|
rsaEncrypted?: string;
|
|
}
|
|
|
|
export interface RSAConfig {
|
|
modulus: string;
|
|
exponent: string;
|
|
}
|
|
|
|
// 기본 RSA 키 설정
|
|
const DEFAULT_RSA_CONFIG: RSAConfig = {
|
|
modulus: "C4F7B39E2E93DB19C016C7A0C1C05B028A1D57CB9B91E13F5B7353F8FB5AC6CE6BE31ABEB8E8F7AD18B90C08F4EBC011A6A8FCE614EA879ED5B96296B969CE92923BC9BAD6FD87F00E08F529F93010EA77E40937BDAC1C866E79ACE2F2822A3ECD982F90532D5301CF90D9BF89E953A0593AB6C5F31E99B690DD582FB85F85A9",
|
|
exponent: "10001"
|
|
};
|
|
|
|
export class XKeypadManager {
|
|
private static instance: XKeypadManager;
|
|
private scriptsLoaded: boolean = false;
|
|
private loadingPromise: Promise<void> | null = null;
|
|
private xkModules: Map<string, XKModuleInstance> = new Map();
|
|
private rsaConfig: RSAConfig;
|
|
|
|
private constructor(rsaConfig?: RSAConfig) {
|
|
this.rsaConfig = rsaConfig || DEFAULT_RSA_CONFIG;
|
|
}
|
|
|
|
public static getInstance(rsaConfig?: RSAConfig): XKeypadManager {
|
|
if (!XKeypadManager.instance) {
|
|
XKeypadManager.instance = new XKeypadManager(rsaConfig);
|
|
}
|
|
return XKeypadManager.instance;
|
|
}
|
|
|
|
/**
|
|
* XKeypad 스크립트를 로드합니다.
|
|
*/
|
|
public async loadScripts(): Promise<void> {
|
|
// 이미 로드 중이거나 로드됨
|
|
if (this.loadingPromise) {
|
|
return this.loadingPromise;
|
|
}
|
|
|
|
if (this.scriptsLoaded && window.XKModule) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
this.loadingPromise = this.loadScriptsInternal();
|
|
await this.loadingPromise;
|
|
this.scriptsLoaded = true;
|
|
}
|
|
|
|
private async loadScriptsInternal(): Promise<void> {
|
|
try {
|
|
// Check if scripts are already loaded
|
|
if (window.XKModule) {
|
|
this.scriptsLoaded = true;
|
|
// Set RSA keys after scripts are loaded
|
|
if (this.rsaConfig && window.RSASetPublic) {
|
|
window.RSASetPublic(this.rsaConfig.modulus, this.rsaConfig.exponent);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Load scripts in order
|
|
const scripts = [
|
|
'/src/shared/ui/assets/js/xkeypad_config.js',
|
|
'/src/shared/ui/assets/js/rsa_crypto.js',
|
|
'/src/shared/ui/assets/js/xkeypad.js'
|
|
];
|
|
|
|
for (const src of scripts) {
|
|
await this.loadScript(src);
|
|
}
|
|
|
|
// Add CSS files
|
|
this.loadCSS('/src/shared/ui/assets/css/xkeypad-modal.css', 'xkeypad-modal-css');
|
|
this.loadCSS('/src/shared/ui/assets/css/xkeypad.css', 'xkeypad-css');
|
|
|
|
// Set RSA keys after scripts are loaded
|
|
if (this.rsaConfig && window.RSASetPublic) {
|
|
window.RSASetPublic(this.rsaConfig.modulus, this.rsaConfig.exponent);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load XKeypad scripts:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private loadScript(src: string): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const script = document.createElement('script');
|
|
script.src = src;
|
|
script.charset = 'utf-8';
|
|
script.onload = () => resolve();
|
|
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
private loadCSS(href: string, id: string): void {
|
|
if (!document.getElementById(id)) {
|
|
const link = document.createElement('link');
|
|
link.id = id;
|
|
link.rel = 'stylesheet';
|
|
link.href = href;
|
|
document.head.appendChild(link);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 새로운 XKModule 인스턴스를 생성합니다.
|
|
*/
|
|
public createModule(id: string): XKModuleInstance | null {
|
|
if (!this.scriptsLoaded || !window.XKModule) {
|
|
console.error('XKeypad scripts not loaded');
|
|
return null;
|
|
}
|
|
|
|
const module = new window.XKModule();
|
|
this.xkModules.set(id, module);
|
|
return module;
|
|
}
|
|
|
|
/**
|
|
* 기존 XKModule 인스턴스를 가져옵니다.
|
|
*/
|
|
public getModule(id: string): XKModuleInstance | null {
|
|
return this.xkModules.get(id) || null;
|
|
}
|
|
|
|
/**
|
|
* XKModule 인스턴스를 제거합니다.
|
|
*/
|
|
public removeModule(id: string): void {
|
|
const module = this.xkModules.get(id);
|
|
if (module) {
|
|
if (module.isOpen()) {
|
|
module.close();
|
|
}
|
|
this.xkModules.delete(id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 모든 키패드를 닫습니다.
|
|
*/
|
|
public closeAllKeypads(): void {
|
|
this.xkModules.forEach(module => {
|
|
if (module && module.isOpen()) {
|
|
module.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* RSA 공개키를 설정합니다.
|
|
*/
|
|
public setRSAPublicKey(modulus: string, exponent: string): void {
|
|
this.rsaConfig = { modulus, exponent };
|
|
|
|
// RSASetPublic이 있는지 확인하고 설정
|
|
if (window.RSASetPublic && typeof window.RSASetPublic === 'function') {
|
|
try {
|
|
window.RSASetPublic(modulus, exponent);
|
|
} catch (error) {
|
|
console.error('Failed to set RSA public key:', error);
|
|
}
|
|
}
|
|
|
|
// Update all existing modules
|
|
this.xkModules.forEach(module => {
|
|
if (module && typeof module.setRSAPublicKey === 'function') {
|
|
module.setRSAPublicKey(modulus, exponent);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* RSA로 텍스트를 암호화합니다.
|
|
*/
|
|
public encryptRSA(text: string): string | null {
|
|
if (!text) {
|
|
return null;
|
|
}
|
|
|
|
// RSAEncrypt 함수가 있는지 확인
|
|
if (!window.RSAEncrypt || typeof window.RSAEncrypt !== 'function') {
|
|
console.error('RSA encryption not available');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// RSA 키가 설정되어 있는지 확인
|
|
if (this.rsaConfig && window.RSASetPublic) {
|
|
window.RSASetPublic(this.rsaConfig.modulus, this.rsaConfig.exponent);
|
|
}
|
|
|
|
return window.RSAEncrypt(text);
|
|
} catch (error) {
|
|
console.error('RSA encryption failed:', error);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* XKeypad 인스턴스를 관리하는 클래스
|
|
*/
|
|
export class XKeypad {
|
|
private manager: XKeypadManager;
|
|
private module: XKModuleInstance | null = null;
|
|
private moduleId: string;
|
|
private options: XKeypadOptions;
|
|
private containerName: string;
|
|
private inputElement: HTMLInputElement | null = null;
|
|
private overlayElement: HTMLDivElement | null = null;
|
|
|
|
constructor(
|
|
moduleId: string,
|
|
options: XKeypadOptions = {},
|
|
rsaConfig?: RSAConfig
|
|
) {
|
|
this.manager = XKeypadManager.getInstance(rsaConfig);
|
|
this.moduleId = moduleId;
|
|
this.options = {
|
|
keyType: 'qwertysmart',
|
|
viewType: 'half',
|
|
numberKeyRowCount: 3,
|
|
maxInputSize: 50,
|
|
width: 100,
|
|
closeDelay: 300,
|
|
autoKeyResize: false,
|
|
isE2E: false,
|
|
onlyMobile: false,
|
|
hasPressEffect: true,
|
|
useModal: false,
|
|
useOverlay: true, // 기본값: 오버레이 사용
|
|
...options
|
|
};
|
|
this.containerName = this.options.useModal
|
|
? `xk-modal-${moduleId}`
|
|
: `xk-pad-${moduleId}`;
|
|
}
|
|
|
|
/**
|
|
* 키패드를 초기화합니다.
|
|
*/
|
|
public async initialize(inputElement: HTMLInputElement): Promise<number> {
|
|
// 스크립트 로드
|
|
await this.manager.loadScripts();
|
|
|
|
// 입력 요소 저장
|
|
this.inputElement = inputElement;
|
|
|
|
// 모듈 생성
|
|
this.module = this.manager.createModule(this.moduleId);
|
|
if (!this.module) {
|
|
return -1;
|
|
}
|
|
|
|
// RSA 키 설정 (E2E가 아닌 경우)
|
|
if (!this.options.isE2E) {
|
|
const rsaConfig = (this.manager as any).rsaConfig;
|
|
if (rsaConfig && typeof this.module.setRSAPublicKey === 'function') {
|
|
this.module.setRSAPublicKey(rsaConfig.modulus, rsaConfig.exponent);
|
|
}
|
|
}
|
|
|
|
// 모달 컨테이너 생성 (필요시)
|
|
if (this.options.useModal) {
|
|
this.createModalContainer();
|
|
}
|
|
|
|
// 키패드 초기화
|
|
const result = this.module.initialize({
|
|
name: this.containerName,
|
|
editBox: inputElement,
|
|
keyType: this.options.keyType,
|
|
maxInputSize: this.options.maxInputSize,
|
|
width: this.options.width,
|
|
position: {
|
|
top: this.options.useModal ? 0 : (this.options.position?.top ?? null),
|
|
left: this.options.position?.left ?? null
|
|
},
|
|
viewType: this.options.useModal ? 'normal' : this.options.viewType,
|
|
numberKeyRowCount: this.options.numberKeyRowCount,
|
|
closeDelay: this.options.closeDelay,
|
|
autoKeyResize: this.options.autoKeyResize,
|
|
isE2E: this.options.isE2E,
|
|
onlyMobile: this.options.onlyMobile,
|
|
hasPressEffect: this.options.hasPressEffect,
|
|
onInputChange: this.options.onInputChange,
|
|
onKeypadClose: () => {
|
|
if (this.options.useModal) {
|
|
this.hideModal();
|
|
}
|
|
if (this.options.useOverlay && !this.options.useModal) {
|
|
this.hideOverlay();
|
|
}
|
|
if (this.options.onKeypadClose) {
|
|
this.options.onKeypadClose();
|
|
}
|
|
}
|
|
});
|
|
|
|
if (result === 0) {
|
|
if (this.options.useModal) {
|
|
this.showModal();
|
|
} else if (this.options.useOverlay) {
|
|
this.showOverlay();
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 키패드를 엽니다.
|
|
*/
|
|
public open(): void {
|
|
if (this.module && !this.module.isOpen()) {
|
|
this.module.open();
|
|
if (this.options.useModal) {
|
|
this.showModal();
|
|
} else if (this.options.useOverlay) {
|
|
this.showOverlay();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 키패드를 닫습니다.
|
|
*/
|
|
public close(): void {
|
|
if (this.module && this.module.isOpen()) {
|
|
this.module.close();
|
|
if (this.options.useModal) {
|
|
this.hideModal();
|
|
} else if (this.options.useOverlay) {
|
|
this.hideOverlay();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 키패드가 열려있는지 확인합니다.
|
|
*/
|
|
public isOpen(): boolean {
|
|
return this.module ? this.module.isOpen() : false;
|
|
}
|
|
|
|
/**
|
|
* 입력을 초기화합니다.
|
|
*/
|
|
public clear(): void {
|
|
if (this.module) {
|
|
this.module.clear();
|
|
}
|
|
if (this.inputElement) {
|
|
this.inputElement.value = '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 키패드를 파괴합니다.
|
|
*/
|
|
public destroy(): void {
|
|
if (this.module) {
|
|
if (this.module.isOpen()) {
|
|
this.module.close();
|
|
}
|
|
this.module.destroy();
|
|
}
|
|
this.manager.removeModule(this.moduleId);
|
|
|
|
// Remove modal container if exists
|
|
if (this.options.useModal) {
|
|
const wrapper = document.getElementById(`${this.containerName}-wrapper`);
|
|
if (wrapper) {
|
|
wrapper.remove();
|
|
}
|
|
}
|
|
|
|
// Remove overlay if exists
|
|
if (this.options.useOverlay && !this.options.useModal) {
|
|
this.hideOverlay();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 입력 값을 가져옵니다.
|
|
*/
|
|
public getValue(): XKeypadResult | null {
|
|
if (!this.module) return null;
|
|
|
|
const result: XKeypadResult = {} as XKeypadResult;
|
|
|
|
if (this.options.isE2E) {
|
|
const sessionInfo = this.module.get_sessionInfo();
|
|
result.type = 'E2E';
|
|
result.sessionId = sessionInfo.sessionId;
|
|
result.encryptedInput = sessionInfo.input;
|
|
result.secToken = sessionInfo.secToken;
|
|
} else {
|
|
const plainText = this.module.get_input();
|
|
result.type = 'Plain';
|
|
result.plainText = plainText;
|
|
|
|
// RSA encryption if available
|
|
if (plainText) {
|
|
const encrypted = this.manager.encryptRSA(plainText);
|
|
if (encrypted) {
|
|
result.rsaEncrypted = encrypted;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 평문 입력 값을 가져옵니다.
|
|
*/
|
|
public getPlainText(): string {
|
|
if (!this.module) return '';
|
|
return this.module.get_input();
|
|
}
|
|
|
|
/**
|
|
* 세션 정보를 가져옵니다. (E2E 모드에서만 유효)
|
|
*/
|
|
public getSessionInfo(): XKSessionInfo | null {
|
|
if (!this.module || !this.options.isE2E) return null;
|
|
return this.module.get_sessionInfo();
|
|
}
|
|
|
|
// Modal helper methods
|
|
private createModalContainer(): void {
|
|
const name = this.containerName;
|
|
|
|
// Remove existing container if any
|
|
const existing = document.getElementById(`${name}-wrapper`);
|
|
if (existing) {
|
|
existing.remove();
|
|
}
|
|
|
|
// Create modal structure
|
|
const wrapper = document.createElement('div');
|
|
wrapper.id = `${name}-wrapper`;
|
|
wrapper.className = 'xkeypad-modal-wrapper';
|
|
wrapper.style.display = 'none';
|
|
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'xkeypad-modal-overlay';
|
|
overlay.onclick = () => this.close();
|
|
|
|
const container = document.createElement('div');
|
|
container.className = 'xkeypad-modal-container';
|
|
|
|
const content = document.createElement('div');
|
|
content.id = name;
|
|
content.className = 'xkeypad-modal-content';
|
|
|
|
container.appendChild(content);
|
|
wrapper.appendChild(overlay);
|
|
wrapper.appendChild(container);
|
|
document.body.appendChild(wrapper);
|
|
}
|
|
|
|
private showModal(): void {
|
|
const wrapper = document.getElementById(`${this.containerName}-wrapper`);
|
|
if (wrapper) {
|
|
wrapper.style.display = 'block';
|
|
setTimeout(() => {
|
|
wrapper.classList.add('show');
|
|
}, 10);
|
|
}
|
|
}
|
|
|
|
private hideModal(): void {
|
|
const wrapper = document.getElementById(`${this.containerName}-wrapper`);
|
|
if (wrapper) {
|
|
wrapper.classList.remove('show');
|
|
setTimeout(() => {
|
|
wrapper.style.display = 'none';
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
// Overlay methods for non-modal mode
|
|
private showOverlay(): void {
|
|
// Remove existing overlay if any
|
|
this.hideOverlay();
|
|
|
|
// Create overlay
|
|
this.overlayElement = document.createElement('div');
|
|
this.overlayElement.id = `xkeypad-overlay-${this.moduleId}`;
|
|
this.overlayElement.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 9998;
|
|
transition: opacity 0.3s ease;
|
|
opacity: 0;
|
|
`;
|
|
|
|
// Add click handler to close keypad
|
|
this.overlayElement.onclick = () => this.close();
|
|
|
|
// Append to body
|
|
document.body.appendChild(this.overlayElement);
|
|
|
|
// Trigger transition
|
|
setTimeout(() => {
|
|
if (this.overlayElement) {
|
|
this.overlayElement.style.opacity = '1';
|
|
}
|
|
}, 10);
|
|
|
|
// Adjust keypad z-index to be above overlay
|
|
const keypadContainer = document.getElementById(this.containerName);
|
|
if (keypadContainer) {
|
|
keypadContainer.style.zIndex = '9999';
|
|
}
|
|
}
|
|
|
|
private hideOverlay(): void {
|
|
if (this.overlayElement) {
|
|
this.overlayElement.style.opacity = '0';
|
|
setTimeout(() => {
|
|
if (this.overlayElement && this.overlayElement.parentNode) {
|
|
this.overlayElement.parentNode.removeChild(this.overlayElement);
|
|
this.overlayElement = null;
|
|
}
|
|
}, 300);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 간편 사용을 위한 헬퍼 함수들
|
|
*/
|
|
|
|
/**
|
|
* 비밀번호 입력용 키패드를 생성합니다.
|
|
*/
|
|
export function createPasswordKeypad(
|
|
inputElement: HTMLInputElement,
|
|
options?: Partial<XKeypadOptions>,
|
|
rsaConfig?: RSAConfig
|
|
): XKeypad {
|
|
return new XKeypad('password', {
|
|
keyType: 'qwertysmart',
|
|
maxInputSize: 16,
|
|
...options
|
|
}, rsaConfig);
|
|
}
|
|
|
|
/**
|
|
* PIN 입력용 키패드를 생성합니다.
|
|
*/
|
|
export function createPinKeypad(
|
|
inputElement: HTMLInputElement,
|
|
options?: Partial<XKeypadOptions>,
|
|
rsaConfig?: RSAConfig
|
|
): XKeypad {
|
|
return new XKeypad('pin', {
|
|
keyType: 'number',
|
|
maxInputSize: 6,
|
|
...options
|
|
}, rsaConfig);
|
|
}
|
|
|
|
/**
|
|
* 카드번호 입력용 키패드를 생성합니다.
|
|
*/
|
|
export function createCardKeypad(
|
|
inputElement: HTMLInputElement,
|
|
options?: Partial<XKeypadOptions>,
|
|
rsaConfig?: RSAConfig
|
|
): XKeypad {
|
|
return new XKeypad('card', {
|
|
keyType: 'number',
|
|
maxInputSize: 16,
|
|
numberKeyRowCount: 4,
|
|
...options
|
|
}, rsaConfig);
|
|
}
|
|
|
|
// Default export
|
|
export default XKeypad; |