게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌17 - NodeJS - 7년 전 등록 / 6년 전 수정

crypto 모듈을 사용한 암호화

안녕하세요. 이번 시간에는 crypto 모듈을 사용해서 비밀번호를 암호화하는 방법에 대해 알아보겠습니다.

예전 패스포트 강좌에서는 패스포트 기능 설명에 중점을 두었기 때문에 비밀번호는 그냥 평문으로 저장하는 strategy를 세웠는데요. 비밀번호를 입력받은 그대로 DB에 저장하는 것은 범죄나 다름없습니다. DB가 해킹당하는 순간, 고객들의 비밀번호도 고스란히 해커 손에 넘겨주는 것이기 때문이죠. 비밀번호는 반드시 암호화해서 저장해야 합니다.

암호화에는 크게 세 가지 방법이 있습니다. 단방향 암호화와 양방향 암호화인데요. 양방향 암호화에 비대칭형 암호화대칭형 암호화가 있기 때문에 크게 세 가지라고 말씀드렸습니다. 단방향 암호화는 복호화할 수 없는 암호화 방법입니다. 복호화란 암호화된 문자열을 다시 원래 문자열로 돌려놓는 것을 의미합니다. 단방향 암호화는 한 번 암호화하면 복호화할 수 없습니다. (다만 몇 가지 암호화 알고리즘은 뚤렸습니다. MD5나 SHA1 방식은 절대 써서는 안 됩니다.)

복호화할 수 없다면 그게 왜 필요한지 의문이 들 수도 있습니다. 하지만 잘 생각해보면 홈페이지 비밀번호같은 경우는 복호화할 필요가 없습니다. 비밀번호를 암호화해서 DB에 저장해둔 후, 나중에 로그인할 때, 다시 입력받은 비밀번호를 같은 알고리즘으로 암호화해서 DB에 저장된 문자열과 비교하면 됩니다. 즉 원래 비밀번호는 어디에도 저장되지 않고, 암호화된 문자열로만 비교하게 되는 것이죠.

단방향 암호화

단방향 암호화의 가장 간단한 방식은 해시 함수를 사용하는 겁니다. 미리 말씀드리지만, 단순하게 해시만 사용해서 암호화를 하면 절대 안 됩니다. 아래 예시는 간단히 보여드리기 위한 것이고, 이 방식의 단점도 알려드릴 것이기 때문에 밑에 salt를 사용하는 방법을 사용하시기 바랍니다.

const crypto = require('crypto');
crypto.createHash('sha512').update('비밀번호').digest('base64'); // 'dvfV6nyLRRt3NxKSlTHOkkEGgqW2HRtfu19Ou/psUXvwlebbXCboxIPmDYOFRIpqav2eUTBFuHaZri5x+usy1g=='
crypto.createHash('sha512').update('비밀번호').digest('base64'); // 위와 같은 결과
crypto.createHash('sha512').update('비밀번호').digest('hex'); // '76f7d5ea7c8b451b773712929531ce92410682a5b61d1b5fbb5f4ebbfa6c517bf095e6db5c26e8c483e60d8385448a6a6afd9e513045b87699ae2e71faeb32d6'

네. 크립토 모듈을 불러와서 createHash 메소드를 사용하면 됩니다. 인자로 사용할 알고리즘을 넣어주는데요. sha256, sha512같은 게 있습니다. 현재는 sha512를 추천합니다. 더 길지만 그만큼 더 안전합니다. update 메소드에는 암호화할 비밀번호를 넣어주고요. digest에는 어떤 인코딩 방식으로 암호화된 문자열을 표시할지를 정해줍니다. base64, hex, latin1 등의 방식이 있는데 base64가 짧아서 더 선호됩니다.

잘 보시면 같은 알고리즘과 같은 인코딩 방식을 사용하면 같은 비밀번호에 대해 같은 결과를 반환한다는 것을 알 수 있습니다. 그래서 비밀번호 암호화에 사용할 수 있는 것이죠. 그런데 이게 또 문제가 됩니다. 만약 해커가 모든 암호에 대해 어떤 결과가 나올지 데이터베이스화 해두었다면, 결과만 보고도 원래 암호를 유추해낼 수 있습니다. 이러한 데이터베이스를 레인보우 테이블이라고 합니다.

따라서 해커가 레인보우 테이블을 사용하지 못하게 하는 방법이 필요합니다.(할 수는 있지만 수 십, 수 백년의 시간이 걸립니다) 바로 소금을 뿌려주면 됩니다. salt라는 특정 값을 통해서 위에서 나온 결과를 변형할 수 있습니다. 비밀번호에 salt 문자열을 붙여서 그것을 수 만번 반복 해시화 하는 거죠. salt는 랜덤 문자열을 생성해서 비밀번호와 같이 DB에 저장하시면 됩니다.

crypto.randomBytes(64, (err, buf) => {
  crypto.pbkdf2('비밀번호', buf.toString('base64'), 100000, 64, 'sha512', (err, key) => {
    console.log(key.toString('base64')); // 'dWhPkH6c4X1Y71A/DrAHhML3DyKQdEkUOIaSmYCI7xZkD5bLZhPF0dOSs2YZA/Y4B8XNfWd3DHIqR5234RtHzw=='
  });
});

한 줄로 했던 hash 함수와는 다르게 좀 복잡합니다. 일단 randomBytes 메소드로 64바이트 길이의 salt를 생성해줍니다. buf는 버퍼 형식이기 때문에 buf.toString('base64')로 base64 문자열 salt로 변경해줍니다. pbkdf2라는 메소드를 사용하는데요. 단방향 암호화할 때 가장 선호하는 방식 중 하나입니다.

pbkdf2에는 인자가 5개나 들어가는데요. 비밀번호, salt, 반복 횟수, 비밀번호 길이, 해시 알고리즘 순입니다. 역시 key가 버퍼 형태로 리턴해주기 때문에 base64 방식으로 문자열로 만들어서 저장해야 합니다. 반복 횟수는 해시 함수를 몇 번 반복하느냐를 나타냅니다. 10만번으로 정해주었는데요. 너무 많다고 생각하실지도 모르는데 이렇게 해도 1초가 안 걸립니다. 숫자는 1초 정도 걸릴 때까지 높여주시면 됩니다. 이 숫자가 높을 수록 슈퍼컴퓨터를 써도 레인보우 테이블을 만들기 힘들어집니다. 그리고 숫자도 10만처럼 깔끔한 숫자 말고, 108236 이런 불규칙적인 숫자를 사용하세요. 비밀번호 길이는 적당한 길이로 정해주면 됩니다.

salt를 key와 같이 저장해야 한다는 것을 꼭 기억하세요! randomBytes 메소드는 매번 다른 salt값을 뱉어내기 때문에 암호화 매번 결과가 달라집니다. 같은 salt값으로 비밀번호를 찾아야 비교가 가능합니다. 또한 반복 횟수, 비밀번호 길이, 해시 알고리즘, 인코딩 방식까지 다 같아야 같은 결과가 나옵니다.

crypto.pbkdf2('입력비밀번호', '기존salt', 100000, 64, 'sha512', (err, key) => {
  console.log(key.toString('base64') === '기존 비밀번호');
});

해킹 수법이 진화하는 것처럼 암호화 수법도 진화하고 있습니다. 요즘은 pbkdf보다 더 나아가 scrypt 방식으로 비밀번호 암호화를 하고 있습니다. 관심 있으신 분들은 이 방식도 한 번 찾아보세요.

양방향 암호화

이번에는 간단하게 양방향 암호화를 하는 방법을 알아봅시다. 먼저 대칭형 암호화인데요. 키가 있으면 그 키로 암호환된 문자열을 다시 복호화할 수 있습니다. 하지만 웹에서는 이 방식을 사용하기가 힘듭니다. 특정 키로 암호화한 문자열을 서버에서 클라이언트로 보냈을 때, 그 문자열을 복호화하려면 그 키도 같이 보내주어야하는데요. 클라이언트에서는 무슨 일이 일어날 지 모르기때문에 키를 클라이언트로 보내서는 안 됩니다. 해결 방법은 비대칭형 암호화입니다. 이건 훨씬 복잡하기 때문에 나중에 알아보고 대칭형 암호화부터 알아봅시다.

const cipher = crypto.createCipher('aes-256-cbc', '열쇠');
let result = cipher.update('암호화할문장', 'utf8', 'base64'); // 'HbMtmFdroLU0arLpMflQ'
result += cipher.final('base64'); // 'HbMtmFdroLU0arLpMflQYtt8xEf4lrPn5tX5k+a8Nzw='

const decipher = crypto.createDecipher('aes-256-cbc', '열쇠');
let result2 = decipher.update(result, 'base64', 'utf8'); // 암호화할문 (base64, utf8이 위의 cipher과 반대 순서입니다.)
result2 += decipher.final('utf8'); // 암호화할문장 (여기도 base64대신 utf8)

createCipher로 암호화 객체를 만들어줍니다. 첫 인자가 암호화 알고리즘이고 두 번째 인자가 우리가 사용할 키입니다. 그 후 update final 메소드를 호출해주면 암호화가 됩니다. update에 암호화할 문장을 넣고, utf8(입력값의 인코딩), base64(출력값의 인코딩)로 인코딩해줍니다. 그래야 buffer가 base64 모양이 문자열이 됩니다. 주의할 점은 final을 꼭 해줘야 한다는 겁니다. 안 그러면 final block이 빠져있기 때문에 복호화를 할 수 없습니다.

decipher은 cipher랑 비슷한데 인자의 순서가 조금 다릅니다. 일단 createDecipher로 복호화 객체를 만들고요. updatefinal로 똑같이 실행해줍니다. update 메소드에는 아까 암호화했던 result를 넣고요. 대신 base64와 utf8의 순서가 반대입니다. 재밌는 점은 final을 호출하지 않아도 결과의 일부가 나옵니다. 암호화할문까지는 복호화가 되었네요. 그래도 final을 호출해야 완전한 문장이 나옵니다.

이렇게 간단하게 암호화 복호화를 할 수 있습니다.

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

13개의 댓글이 있습니다.
3년 전
파일객체도 암호화가 가능 한가요?? 키생성하는 함수는 없을까요?
3년 전
나도 참 오래 했소 이제 부자가 될 일만 남았지 내 능력 will be recognized
3년 전
님아 이걸 읽었으니 난 부자가 될 거요
4년 전
기존salt 에 어떤걸 대입해야하는걸까요~? 저대로 넣으면 계속 false가 나옵니다~
4년 전
아무값이나 넣어서 암호화하시면 되는데 복호화할 때는 기존에 넣었던 salt를 다시 사용하셔야 합니다.
4년 전
앗 해결했습니다 감사합니다 :) ㅎㅎ
4년 전
파일객체도 암호화가 가능한가요?
5년 전
좋은글감사합니다
5년 전
위에 댓글 중 "https 암호화 통신으로 비밀번호를 서버로 전달"이라 말씀하셨는데, 그러면 기술적으로 어드민이 서버로 넘어온 비밀번호를 crypto 등으로 암호화 하기 전에 비밀번호 탈취가 가능하기는 하다는 말씀이실까요? 어디까지 어드민의 잠재적인 능력(?)을 인정(??)해야 하는지 잘 모르겠습니다...
5년 전
네 서버에 접근할 수 있는 사람들은 비밀번호 탈취가 가능합니다.
5년 전
감사합니다!
6년 전
result += cipher.final('base64'); 코드에서 final() 의 결과를 저장하는게 아니라 이전 update() 의 결과와 합치는 이유가 뭘까요?
6년 전
이게 암호화를 블록별로 하기 때문에 final은 final 블록만을 암호화합니다. 따라서 앞에서 update에서 암호화한 부분과 final에서 암호화한 부분을 합쳐주어야합니다.
6년 전
https://gist.github.com/drakestone/dd074ec54df9b51807852de7162db71d - 이 코드 좀 봐주시겠어요. update() 는 아무런 값도 리턴하지 않네요. :)
6년 전
왜냐면 문자열이 너무 짧아서 final block에 다 들어가기 때문입니다.
6년 전
네에, 그렇네요. 암호화 할 내용이 16자가 넘어가면 update, final 로 암호 내용이 분리가 되네요. 고맙습니다. :)
6년 전
https://gist.github.com/drakestone/dd074ec54df9b51807852de7162db71d - 최종 코드입니다. 교과서 잘 읽고 있습니다. 자주 질문 드리겠습니다. :)
6년 전
salt를 비밀번호와 같이 저장해야 한다는 것은 꼭 기억하세요! - 라고 적으신 부분이 이해가 되지 않아서 여쭤봅니다. salt값을 db에 저장해야되는건 알겠지만 비밀번호도 저장을 해야 하나요? 아니면 salt값과 key값을 저장 해야하는건가요?
6년 전
salt와 key입니다.
6년 전
DB에 salt도 같이 저장해놓으면 db가 털렸을때 문제가 없나요??? 결국 해쉬된 값을 변형 할때 주요 키인 salt가 노출되니까 위험할 것 같은데 괜찮은 이유가 뭔가요ㅠㅠ
6년 전
salt가 털려도 비밀번호를 찾아내는데는 한세월이 걸립니다.
6년 전
글 올려주셔서 감사합니다! 혹시 클라이언트 단에서 처음부터 암호화를 해서 보내는 것도 괜찮은가요? 제가 Axios를 써서 로그인 POST API를 구현했는데, 브라우저 콘솔로 응답을 확인해 보니까 response.config에 (노드 환경 서버입니당) 비밀번호가 그대로 떠있더라구요..;; 이게 첨부터 암호화를 안 해서 생긴 결과라고 할 수 있을까요?
6년 전
네 SHA256같은 단방향 해시는 클라이언트에서 하셔도 됩니다. 그런데 실무에서 그렇게 하는 경우는 거의 없고요. 그냥 HTTPS 암호화 통신으로 비밀번호를 서버로 전달합니다.
6년 전
감사합니다 HTTPS암호화 통신이라고 하시면 브라우저 WebCrypto api 사용하는 것도 포함이 될까요? 지금 리액트 웹앱을 만들고 있는데, 소셜 로그인할 때 받는 access_token을 클라이언트 단에서 webCrypto로 암호화한 다음에 서버에서 다시 복호화하려고 하는데 이런 방법이 맞는지 궁금합니다
6년 전
아뇨. 그냥 주소창에 보이는 HTTPS입니다. 그냥 HTTPS만 적용하면 암호화없이 평문으로 전송해도 암호화가 된 효과를 얻을 수 있습니다.
6년 전
감사합니다ㅜ WebCrypto api 적용하느라 헤매고 있었는데,,,https도메인 설정법을 찾아봐야겠습니다ㅜ 이런 api를 직접 쓰는 일은 드문 건가요? 마지막 질문입니다ㅋㅋ;;;
6년 전
네네 저도 들어만 본 API입니다.
7년 전
단방향 암호화라서, 만약에 해커가 salt랑 Hashed 된 비밀번호를 알고있어도,, 이걸 이용해서 거꾸로 원래 비밀번호는 못알아내는게 맞나요?
7년 전
양자컴퓨터가 나오지 않는이상 불가능합니다. 다만 이터레이션 숫자가 충분히 커야합니다.
7년 전
감사합니다 덕분에 제 웹에도 패스워드를 암호화 할수 있었네요