[AES - CryptoJS] 암호화된 데이터 수정시 데이터 충돌

희선·2025년 4월 18일

Toucheese

목록 보기
3/3
post-thumbnail

유저 정보를 전역으로 저장한다면?

개발을 하면서 불가피하게 유저 정보를 localStorage에 저장해야할 일이 생겼다
우선 테스트 계정을 생성하여 개발을 하는 과정 중에 유저 정보를 localStorage에 저장한다는건 정말 찜찜하고 불편한 일이었다..
어떻게 하면 유저 정보를 안전하게 관리할 수 있을까?


🔐 유저 정보 암호화 하기

LocalStorage에 저장되는 데이터는 유저 이름, 전화번호, 이메일이다
꽤 중요한 정보이기 때문에 전역으로 저장하면서 보안을 유지하는 방법은 암호화라고 생각했다
처음에는 해싱에 대해 알아봤지만 해싱을 하면 복호화가 불가능 하므로 다른 페이지에서 유저 정보값을 사용할 수 없게된다!
내가 진행하는 프로젝트에서는 "${username} 님 안녕하세요! "와 같은 메세지를 출력해야 하기 때문이다
AES를 활용한 양방향 암호화 CryptoJS 라이브러리를 사용하여 암호화/복호화를 모두 진행하기로 결정!



✅ AES & SHA256 비교

방식목적복호화 가능여부사용 예시
SHA256데이터 무결성 검증X비밀번호 저장, 데이터 무결성 검증, 서명 검증 등
AES데이터 암호화O사용자 정보, 파일, 텍스트 암호화 등

1️⃣ AES란?

미국 정부에서 표준으로 채택한 대칭키 암호화 방식으로 암호화할 때와 복호화할 때 같은 비밀 키를 사용하는 특징이 있다!
대칭키 암호화 알고리즘 개념 이라고 정리할 수 있다

2️⃣ CryptoJS 라이브러리란?

JavaScript 환경에서 암호화를 쉽게 할 수 있게 도와주는 라이브러리로 AES를 구현한 모듈이 포함되어 있다! 정리하자면 AES 방식을 사용할 수 있게 해주는 도구인것!




🔗 AES 암호화 / 복호화 로직 적용 방법


1️⃣ crypto-js 라이브러리 설치

pnpm install crypto-js

터미널에 해당 라이브러리를 설치한다!

import CryptoJS from 'crypto-js';

그리고 사용할 파일에서 CryptoJS를 import!


2️⃣ secretKey 발급 및 설정

랜덤 문자열 생성 도구 라이브러리도 있지만 나는 터미널에 직접 입력해서 출력했다

openssl rand -base64 32

위 코드를 터미널에 작성하면 랜덤한 문자열이 생성된다!
(결과 출력 예시 : k9zA+5Wn2kY3vFp7r8Uu4X9NbqKvOYY9qqP8p0G2hcY=)

해당 문자열을 secretKey로 사용하면 되는데 랜덤한 문자열이라 해서 하드코딩은 절대 금물...
이렇게 생성된 문자열을 환경변수로 설정!


3️⃣ AES 암호화 알고리즘 적용

const encrypted = CryptoJS.AES.encrypt("홍길동", secretKey).toString();

AES 암호화 알고리즘을 적용하여 변수선언!
환경변수에 선언된 secretKey를 적용해야한다
그러면 '홍길동' 이라는 문자열이 랜덤한 문자열로 암호화가 되는것을 확인할 수 있다


4️⃣ AES 복호화 알고리즘 적용

const decrypted = CryptoJS.AES.decrypt(encrypted, secretKey).toString(CryptoJS.enc.Utf8);

복호화 알고리즘도 암호화 로직에서 사용되는 secretKey와 같은 secretKey를 사용한다!
암호화 된 변수와 secretKey를 적용하고 복호화 결과를 사람이 읽을 수 있는 텍스트로 변환해주는 역할을 하는 CryptoJS.enc.Utf8을 적용해야한다




👩🏻‍💻 실제 적용 과정

작업환경에서 암호화된 유저 정보가 저장되는 필드 네임은 이렇게 세개로 나눠진다
encryptedEmail, encryptedUsername, encryptedPhone

초기 로딩 시 유저 정보를 복호화 해서 localStorage에 이메일, 이름, 전화번호를 각각 저장하는 로직이었다! 암호화 된 데이터를 복호화 하여 브라우저에 출력하는 상황의 이미지다


🚨 유저 정보를 수정하면 새로운 필드가 생성되는 에러

유저의 이름과 전화번호 수정 시 수정된 username, phone 값이 암호화 없이 localStorage.userState에 저장되는데 이로인해 다음과 같은 문제가 발생한다


1️⃣ email 정보가 없는 것처럼 나옴
2️⃣ 페이지 이동 시 복호화된 encrypted만을 Zustand에 넣기 때문에 username, phone 정보가 사라짐
3️⃣ 암호화된 정보와 암호화되지 않은 정보가 동시에 존재하여 데이터 충돌 발생, UI 오류 유발



✔️ 문제점 파악

[ 유저가 정보를 수정했을 때 ]

암호화된 필드 (encryptedEmail, encryptedPhone, encryptedUsername)는 그대로 유지
수정된 일반 필드 (email, username, phone)만 갱신

그래서 localStorage에는 일반 텍스트 값이 저장되고

다음 페이지로 이동하면 zustandlocalStorage에서 데이터를 불러오고
암호화된 값만 복호화해서 state에 넣음

✅ 암호화되지 않은 일반 필드들은 무시됨.
결과적으로 암호화되지 않은 새 데이터는 state에서 사라지는 현상 발생




✅ 해결 방안

[ 유저가 정보를 수정했을 때 ]

1️⃣ encryptUserData새 값들을 암호화해서

2️⃣ 기존의 encryptedXXX 필드들을 업데이트하고

3️⃣ localStoragestate 전체를 갱신



❶ 암호화 복호화 유틸함수 생성

암호화 된 유저 데이터를 암호화/복호화 하는 함수를 다른 로직에서도 사용이 가능하도록 유틸함수로 수정!

/** 암호화 */
export const encryptUserData = ({ username, phone, email }: UserPlainData) => {
  return {
    // 유저 정보 암호화
    encryptedEmail: email ? CryptoJS.AES.encrypt(email, AES_SECRET_KEY).toString() : null,
    encryptedPhone: phone ? CryptoJS.AES.encrypt(phone, AES_SECRET_KEY).toString() : null,
    encryptedUsername: username ? CryptoJS.AES.encrypt(username, AES_SECRET_KEY).toString() : null,
  };
};

/** 복호화 */
export const decryptUserData = ({
  encryptedUsername,
  encryptedEmail,
  encryptedPhone,
}: UserEncryptedData) => {
  return {
    email: encryptedEmail
      ? CryptoJS.AES.decrypt(encryptedEmail, AES_SECRET_KEY).toString(CryptoJS.enc.Utf8)
      : null,
    phone: encryptedPhone
      ? CryptoJS.AES.decrypt(encryptedPhone, AES_SECRET_KEY).toString(CryptoJS.enc.Utf8)
      : null,
    username: encryptedUsername
      ? CryptoJS.AES.decrypt(encryptedUsername, AES_SECRET_KEY).toString(CryptoJS.enc.Utf8)
      : null,
  };
};

유틸함수이기 때문에 데이터 유무에 따라 적용해야하므로 옵셔널 체이닝을 적용하여 공통으로 사용!




❷ 변경된 값을 암호화 하여 필드 업데이트

  const handleSave = (data: any) => {
    // 기존 localStorage 데이터 가져오기
    const storedData = localStorage.getItem('userState');
    const parsedData = storedData ? JSON.parse(storedData) : '';

    const { encryptedPhone, encryptedUsername } = parsedData.state || '';

    // 변경된 값이 있을 경우 암호화
    const { encryptedPhone: newEncryptedPhone, encryptedUsername: newEncryptedUsername } =
      encryptUserData({
        phone: data.phone || null,
        username: data.username || null,
      });

    const updatedState = {
      ...parsedData.state,
      encryptedPhone: data.phone ? newEncryptedPhone : encryptedPhone,
      encryptedUsername: data.username ? newEncryptedUsername : encryptedUsername,
    };

    const updatedData = {
      ...parsedData,
      state: updatedState,
    };

    localStorage.setItem('userState', JSON.stringify(updatedData));
  };

유저 정보 변경후 저장하는 과정에서 암호화 함수 호출 후 저장!



🌟 해결 완료 화면


새롭게 필드를 업데이트 함으로써 유저 정보를 복호화 하여 잘 출력하고 있는것을 확인할 수 있다!
충돌되는 데이터 없이 해결!
처음부터 암호화를 하지 않고 정보를 저장하다보니 암호화/복호화를 거치는 과정에서 꼬인 문제라 생각보다 빨리 해결 할 수 있었다
당황스러웠지만.. 전체적인 로직을 그려가며 작업하는것이 중요하다고 느꼈다


profile
FE 개발자가 되기 위한 땅굴 파기! 🌱

0개의 댓글