이번 프로젝트를 진행하며 한 가지 신경을 썼던 점이라면 바로 비밀번호와 토큰을 암호화하여 저장하는 것이었다. 개인적으로도 그리고 팀적으로도 암호화에 관심이 있었고, 프론트엔드에서도 암호화를 적용하여 조금 더 프로젝트의 완성도를 올려보고 싶었다.
또한 이번 프로젝트에서 사용되었던 API는 다수의 요구사항에 맞춰야 했고 실습의 느낌에 더 가까운 API였기에 백엔드단에서의 암호화에 대한 로직이 전혀 없었고 심지어 특정 API에서는 원본 비밀번호 그대로 응답이 오는 경우도 있었기에 암호화를 적용해야 할 이유가 있다고 판단했다.
여기서 감지한 문제점은 다음과 같다. 첫 번째로 앞서 언급한 것처럼 원본 비밀번호가 노출된다는 것. 두 번째로는 토큰의 만료 시간이 없다는 것이다. 실제 많은 서비스들은 토큰 만료 시간이 있는데 이번에는 그런 로직이 백엔드에서 제공되지 않기에 비밀번호와 토큰을 암호화해야겠다고 생각했다.
CryptoJS vs Web Crypto API
조사한 바에 의하면 웹 프로젝트에서 암호화를 할 수 있는 방법은 총 3가지이다. 아래 레퍼런스 글에서도 이야기하듯이
- CryptoJS 라이브러리를 이용하는 방법
- 웹 브라우저에서 제공하는 Web Crypto API 기능
- 직접 구현하는 방식
이렇게 총 3가지가 있다.
프로젝트의 기간도 짧고 암호화 알고리즘을 직접 구현하기에는 공수가 많이 들기에 일단 마지막 방식은 논외로 두고 나의 고민은 CryptoJS 라이브러리와 Web Crypto API로 나뉘었다. 일단 두 방식 모두 장점이 확실했다. CryptoJS 라이브러리는 이미 꽤 알려진 라이브러리로 사용된 코드나 사용방법이 공식 사이트는 약간 부족하지만 예제가 웹 상에 정말 많았고 Web Crypto API는 MDN 공식문서가 꽤 세세하게 자세히 작성되어 있었다. 딱히 단점을 뽑기에는 애매했다.
결론부터 말하면 최종적으로 고르게 된 것은 CryptoJS 라이브러리를 사용하는 것이었다. 프로젝트 특성상 빠르게를 지향해야 했기에 코드를 작성할 때 옵션이 조금 더 직관적인 CryptoJS 라이브러리를 선택했다. Web Crypto API도 좋지만 옵션에 대한 부분이 많아서 약간의 연습시간이 필요할 것 같다고 판단했다. 그렇다고 CryptoJS 라이브러리에서 옵션을 줄 수 없는 것도 아니었다. 이러한 이유와 몇 가지 추가적인 사유(지금은 해결된 것 같지만 Web Crypto API의 호환성 이야기, AES암호화 중 CryptoJS가 CBC 방식을 기본값으로 사용하는 점 등)로 CryptoJS 라이브러리를 선택했다.
유틸함수 작성
const { VITE_CRYPTO_KEY } = import.meta.env;
import CryptoJS, { SHA256 } from "crypto-js";
export const sha256Encrypt = (data: string) => {
return SHA256(data).toString();
};
export const aesEncrypt = (data: string) => {
return CryptoJS.AES.encrypt(data, VITE_CRYPTO_KEY).toString();
};
export const aesDecrypt = (data: string | null) => {
if (!data) return "";
const bytes = CryptoJS.AES.decrypt(data, VITE_CRYPTO_KEY);
const decryptedData = bytes.toString(CryptoJS.enc.Utf8);
return decryptedData;
};
막상 작성하고 보니 일부 리팩토링 할 로직이 보이기는 한다. 어쨌든 이번에 프로젝트에서 사용할 암호화 로직은 AES암호화와 SHA256 암호화 로직이다. 각각의 로직은 비밀번호와 토큰값을 저장하는 로직에 사용된다. 간단하게 두 암호화 방식에 대해서 설명하자면 다음과 같다.
SHA256 암호화란?
단방향의 해시 함수로 임의의 길이의 데이터를 고정된 길이의 해시 값으로 변환하는 데 사용된다. 단방향이기에 해시된 값으로 원본 데이터를 복원할 수 없다.
AES 암호화란?
대칭키 암호화 알고리즘으로 특정한 키를 기준으로 암호화와 복호화를 진행한다. 키를 기준으로 복호화가 가능하기에 데이터를 안전하게 전송, 저장할 때 사용된다. 이번 프로젝트에서 키는 환경변수로 관리하여 외부에 노출되지 않도록 하였다.
SHA256을 사용한 비밀번호 로직부터 살펴보면 다음과 같다.
로그인, 회원가입을 진행하는 로직에서 사용자가 form을 통해 데이터를 제출하면 그 데이터를 그대로 보내주는 것이 아니라 한 번 암호화해서 보내주는 것이다. 그래서 실제로는 아래와 같이 원본 비밀번호가 고정된 난수 값으로 바뀌게 된다.
따라서 실제 저장되는 암호의 값은 해당 난수이다. 이때 어차피 비밀번호가 외부로 유출되면 소용없는 것 아니냐라는 의견이 있었지만 여러 사이트에서 동일한 비밀번호를 사용하는 사용자들이 많기에 만약 우리 사이트에서 비밀번호가 유출되더라도 원본 비밀번호는 알 수 없도록 하여 추가적인 피해를 방지하고자 이와 같은 암호화 로직을 적용하였다.
AES 암호화 로직은 사용자 토큰을 저장하는 데에 사용했다.
이번 프로젝트에서 새로고침이 일어나면 redux 스토어에 저장되어 있는 사용자 정보가 없어지는 현상이 있었고 해당 문제를 LocalStorage에 토큰을 저장하고 해당 토큰값으로 auth-user API를 호출하여 유저 정보를 다시 가져와서 redux 스토어에 전달해 주는 로직을 구성하였다. 따라서 로컬스토리지에 저장하는 토큰의 값은 역시나 무방비로 존재하고 여러 공격에 노출될 수 있다고 판단하여 AES암호화를 적용했다.
로그인, 회원가입 시 받아오는 토큰을 로컬스토리지에 암호화하여 저장하고 사용할 때는 복호화하여 사용하도록 구현하였다.
이뿐만 아니라 redux 스토어에도 토큰의 원본을 저장했었는데 크롬 확장프로그램으로 redux 스토어에 저장된 값들을 볼 수 있는 기능이 있길래 redux 스토어 쪽에도 암호화하여 저장하는 로직을 추가해 주었다.
한계점
물론 이러한 로직들에 한계점이 있다는 것을 구현하면서, 자료조사하면서 인지하게 되었다.
일단 자료조사를 하면서 인지한 것처럼 https 통신에서는 중간에 패킷 가로채서 정보를 가로채는 것이 불가능하므로 의미 없는 활동이 될 수 있고, 해킹되었다는 것 자체가 많은 사용자 데이터가 노출되었다는 것인데 거기에서 평문 비밀번호가 노출되지 않아서 다행이다라고 하고 있을 수는 없다.
그리고 이렇게 구현했을 때 사용자에게 원본 비밀번호를 제공할 방법이 전혀 없으므로 메일을 통한 재설정 코드를 전달해야 한다. 즉 새로운 로직이 또 추가되어야 한다는 것이다.
의의
프론트엔드는 신경 쓸 것이 많다. 정말 많은 분야에 신경 써야 하고 매우 중요하다. 그중 가장 중요한 요소 중 하나가 바로 보안이다. 이번 프로젝트에서는 위 로직을 적용해 봤는데, 여전히 부족한 요소가 많은 것 같다. API 쪽에서 암호화가 진행되지 못하기에 나름 최선을 다했다고 생각했지만 자료조사를 하면 할수록 아쉬운 점이 보인다. 어쩔 수 없다고 생각하지만 이번 경험을 토대로 앞으로의 프로젝트에서도 보안에 신경을 더 써봐야겠다. 즐거운 경험이었다.
++ 1/27 추가
문제점
지속적으로 조사를 하다 보니 발생한 몇 가지 문제점이 더 있었다.
일단 비밀번호의 경우 그래도 결국 Request proxy 공격에 취약하다고 하는 것 같은데... 보완할 수 있는 방향에 대해 지속적으로 찾아보고 있다. 그리고 이 로직이 브라우저단에서 동작해서 만약 salt를 추가했을 때 해당 로직이 노출된다는 단점이 있었다. 그리고..
가장 큰 문제점은 바로 동작로직이 브라우저에서 동작하고, AES 암호화 키가 브라우저에 그대로 노출되고 있다는 것이었다. 내가 저질렀던 실수는 바로 환경변수를 사용하여 키를 관리하여 소스 맵에 키가 노출되고 있었다는 점이었다. 이런 방식이라면 나의 보안로직은 쓸모없는 코드가 되어버린 것이다.
여기서 드는 추가적인 궁금증이 있다.
1. 서버리스 형태로 해당 암호화 로직이 동작하는 위치를 변경하면 상관없을까? 이전에 Vue에서는 서버리스 형태로 API를 호출시켰는데 소스 맵에 환경 변수가 노출되지 않았다.
2. 소스 맵만 노출시키지 않는다면 브라우저에서 환경변수 값을 가져올 수 있는 방법이 추가적으로 있는 건가?
조사하면 할수록 오히려 잘 모르겠다. 하지만 끝장을 보고 싶다.
참고한 레퍼런스 및 같이 읽어보면 좋은 글
https://www.palindrom615.dev/client-side-hashing-is-not-helping
'개발 > 프로젝트' 카테고리의 다른 글
내 웹사이트에서 사용자는 어떤 행동을 할까? feat. hotjar🔥 (0) | 2024.04.17 |
---|---|
[Toy Project] K-age-calculator 프로젝트 - 1 (0) | 2023.11.18 |