OnePass는 쉽고 안전한 로그인 서비스를 지원하는 비밀번호 관리 어플리케이션입니다.
사이트별 아이디와 비밀번호를 저장하여, 사용자가 비밀번호를 기억할 필요 없이 바로 로그인할 수 있도록 도와줍니다.
로그인할 때 비밀번호가 기억나지 않아 여러 번 입력해보신 적 없으신가요?
사이트별 요구사항이 달라 비밀번호를 조금씩 다르게 설정하다 보면 잘 기억나지 않을 때가 많습니다.
때로는 여러 사이트에 동일한 비밀번호를 사용하여 보안이 걱정이 될 때도 있습니다.
이러한 문제를 해결하고자 나만의 비밀번호 관리자, OnePass를 개발하게 되었습니다.
OnePass 바로가기
OnePass 크롬 익스텐션 추가하기
Server Repository 바로가기
🌈 Feature
🔧 Installation
Frontend | Backend |
---|---|
1. 클라이언트 레포지토리를 클론받습니다. | 1. 서버 레포지토리를 클론받습니다. |
git clone https://github.com/eunhye210/onepass-client.git | git clone https://github.com/eunhye210/onepass-server.git |
2. 다음과 같이 환경변수를 설정합니다. | 2. 다음과 같이 환경변수를 설정합니다. |
REACT_APP_SERVER_URL=<YOUR_SERVER_URL> | PORT=<YOUR_PORT_NUMBER> MONGOOSE_URL=<YOUR_MONGOOSE_URL> MAILJET_APIKEY_PUBLIC=<YOUR_MAILJET_PUBLIC_APIKEY> MAILJET_APIKEY_SECRET=<YOUR_MAILJET_SECRET_APIKEY> AWS_ACCESS_KEY_ID=<YOUR_AWS_ACCESS_KEY_ID> AWS_SECRET_ACCESS_KEY=<YOUR_AWS_SECRET_ACCESS_KEY> AWS_KEY_ARN=<YOUR_AWS_KEY_ARN> AWS_KEY_REGION=<YOUR_AWS_KEY_REGION> |
3. 터미널에서 아래 명령어를 실행합니다. | 3. 터미널에서 아래 명령어를 실행합니다. |
npm install npm start |
npm install npm start |
🗓 Project Schedule
1주차 ( 22.11.07 ~ 22.11.13 ) |
---|
아이디어 확정, DB schema 설계, API 명세서 작성, PoC 진행 |
2주차 ( 22.11.14 ~ 22.11.20 ) |
메인 기능 작업, Frontend / Backend 개발 |
3주차 ( 22.11.21 ~ 22.11.27 ) |
메인 기능 업그레이드, 배포(Netlify, AWS Elastic Beanstalk) |
🗂 Stack
Frontend | Backend | ||
---|---|---|---|
React | v 18.2.0 | Node.js | v 14.17.0 |
React-router-dom | v 6.4.3 | Express | v 4.16.1 |
React-redux | v 8.0.5 | MongoDB | v 3.6.3 |
thinbus-srp | v 1.8.0 | thinbus-srp | v 1.8.0 |
mongodb-client-encryption | v 1.2.1 |
사용자의 기밀 데이터를 다루는 프로젝트였던 만큼, 클라이언트와 서버간 데이터를 어떻게 주고 받을 지에 대한 고민을 많이 했습니다. 기존에 시도했던 로그인 방식은 비밀번호를 암호화하여 DB에 저장하는 것으로, 해시 암호화를 통해 데이터를 안전하게 저장한다는 장점이 있지만, 클라이언트와 서버간의 통신 속 데이터가 유출되거나 방대한 해시 데이터를 가진 공격자로부터 취약해질 수 있다는 위험성이 있었습니다. 로그인 이후의 요청과 응답에서도, 데이터를 단순히 body에 넣기보다 암호화를 통해 안전한 통신이 이루어질 필요가 있었습니다.
위와 같은 문제에 대응하고자 SRP 방식을 채택하게 되었습니다. SRP 로그인은 인증 과정에서 비밀번호가 서버로 전송되지 않고, 매 로그인 마다 일회성의 sessionKey가 생성되어 양방향 암호화 통신이 가능하다는 장점이 있습니다.
- 회원가입시 비밀번호 대신 전송되는 verifier은 ( salt, email, password )로부터 도출한 암호키를 활용하여 생성된 랜덤 문자열로, 해싱과는 달리 비밀번호를 추측하는데 사용될 수 없습니다.
- 사용자 인증은 일회성의 public Key(A, B) & secret Key(a, b)를 바탕으로 이루어집니다. 클라이언트와 서버는 A와 B를 서로 교환함으로써, 자신의 secret Key와 상대방의 public Key를 갖고 인증 여부를 계산합니다. 각자의 키로 계산한 결과값이 서로 일치하였을 때 최종적으로 로그인이 성공합니다.
- 로그인 성공시 클라이언트와 서버는 일회성의 sessionKey를 공유하게 되며, 로그인 이후의 모든 요청 & 응답에서 해당 키를 활용한 양방향 암호화 통신(AES 알고리즘 사용)을 진행했습니다.
< DB 저장 예시 >
// __keyVault
{
_id: UUID("<string>"),
keyMaterial: BinData(0,"<encrypted binary data string>"), // 데이터 암호화, 복호화에 사용
creationDate: ISODate("2022-11-25T13:44:55.192+00:00"),
updateDate: ISODate("2022-11-25T13:44:55.192+00:00"),
masterKey: {
provider: "<string>", // aws
region: "<string>", // ap-northeast-2
key: "<string>" // AWS ARN : Amazon의 리소스를 고유하게 식별하기 위해 사용
}
}
// User
{
_id: ObjectId("<string>"),
username: "<string>",
passwordList: [
{
url: "www.naver.com",
username: "[email protected]",
password: ******** // Binary 형식으로 저장됨
}
]
...
}
DB에 저장하는 방식 또한 중요합니다. 단순히 사용자별 대칭키를 생성하여 양방향 암호화 방식을 진행할 수도 있었지만, 해당 방식으로는 DB가 해킹되었을 경우 데이터 하나가 복호화된다면 나머지도 자연스럽게 복호화된다는 위험성이 었었습니다. 이에 별도의 공간에 MasterKey를 생성하여 이중 암호화 절차를 밟는 방식을 진행하게 되었습니다.
- Master Key : AWS KMS(Key Management Service)에서 관리했습니다. 사용자의 데이터가 저장된 MongoDB가 아닌 별도의 공간(AWS)에서 관리함으로써 만약의 DB 유출시에도 데이터를 복호화할 수 없도록 이중 보안을 구축했습니다. MasterKey는 DEK를 암호화하거나 복호화하는데 사용됩니다.
- DEK : libmongocrypt에서 생성되고 MasterKey를 사용하여 암호화된 키입니다. DEK는 데이터를 암호화하고 해독하는데 사용됩니다. 무작위 암호화 알고리즘(AEAD_AES_256_CBC_HMAC_SHA_512-Random)을 사용하여 같은 데이터 값이라도 매번 다른 암호화 결과물이 도출될 수 있도록 했습니다.
크롬 익스텐션을 사용하기 위한 환경을 구축하며 빌드 엔트리를 나눌 필요가 있었습니다. CRA는 기본적으로 SPA를 지원하기 때문에 익스텐션을 위한 별도의 페이지를 추가하기 위해서는 multiply entry를 설정해야 했습니다. 하지만 막상 웹팩 설정을 변경하려고 하니 어디서 어떻게 바꿔야할지 막막했던 것 같습니다. CRA의 편함을 즐기기 위해선 CRA가 무엇을 해주는지 알아야 할 필요가 있었지만, 실상은 그 편함만을 당연히 여기고 있었기 때문입니다. 그러한 과정 속 무심코 지나갔던 웹팩에 대해 보다 자세히 알아보는 계기가 되었습니다. CRA가 많은 것을 대신 해주고 있었다는 것을 깨닫는 동시에, 기본적으로 제공하는 것들 중에 사용하지 않은 부분도 많다는 점을 깨닫게 되었습니다. 프로젝트 구조를 파악하는 것의 중요성을 느끼며, 이번 경험을 토대로 다음 프로젝트에서는 CRA 없이 좀 더 능동적이고 자유로운 개발환경도 구축해보고자 합니다.
실제 익스텐션을 개발하는 과정에 있어서는 탭 별 정보를 얻어 DOM에서 필요한 부분만을 조작하는 것이 핵심이었습니다. 이에 크롬 api를 활용하여 탭 이동 및 새 탭 활성화에 따른 tab id 및 url 정보를 얻었고, 여기서 도메인 네임만을 추출하여 사용자 DB 속 데이터 유무를 확인했습니다. 비밀번호는 input type이 password로 설정되어있다는 점을 바탕으로, DOM 내 해당 태그를 찾아 사용자의 username과 password 값을 넣어줬습니다.
보이지 않는 영역일수록 간과하기 쉽기 때문에 그만큼 더 위험 요소가 많아질 수 있음을 배웠습니다. 스니핑 공격 등 네트워크 취약의 위험을 알게 되며, 단순히 외적인 결과물을 만드는 것에 앞서 실제 사용자가 믿고 사용할 수 있는 서비스에 초점을 맞추었습니다. 그러나 그 과정이 결코 쉽지만은 않았습니다. 보안 알고리즘을 공부하며 깊은 수학적 지식이 바탕되어야 한다는 점을 알게 되었고, 이에 해당 로직을 직접 계산하지 못하고 이론에 맡겨야 하는 점이 답답하게 느껴지기도 했습니다.
그럼에도 제가 집중했던 부분은, '보안과 렌더링 최적화 간 균형' 을 이루는 것이었습니다. 예를 들어, 비대칭키는 대칭키에 비해 안전하지만 속도가 느리다는 단점이 있습니다. 이에 각 로직별 특징에 따른 알고리즘의 적용으로 안전성과 효율성 간 절충을 고려했습니다. 우선 보안이 중요한 로직과 그렇지 않은 로직으로 나눴고, 보안이 중요한 로직은 크게 3가지 경우로 나눠 진행했습니다. 처음으로 유저를 인증하는 로그인에서는 비대칭키를 사용하여 보안에 집중했고, 그 이후의 통신에는 대칭키를 사용하여 클라이언트와 서버 간 빠른 통신을 구축했습니다. 마지막으로 DB에서는, 대칭키 외 별도의 마스터키를 적용함으로써 대칭키 보안의 단점을 극복하고자 노력했습니다.
평소 무심코 지나갔던 데이터 보안과 네트워크에 집중하며, 저의 프로젝트를 다양한 관점에서 접근해 볼 수 있었습니다. 보이지 않는 작은 부분도 세심하게 놓치지 않는 자세와, 자료구조와 CS 등 기초가 탄탄한 개발자로 성장할 수 있도록 노력해 나가겠습니다.