써드파티 API 호출을 위한 암호화 삽질기

배경

우리 서비스에 사용되는 sms 발송 업체를 기존 (웹에서 발송하는 카카오톡 및 SMS 기능 제공 서비스 업체)에서 (NAVER에서 운영하는 각종 클라우드 서비스)로 변경하기로 했다. 이를 위해 API 사용법을 알아보고 스웨거(Swagger — API 스펙 확인 및 테스트 가능한 환경)에서 테스트까지 수월하게 마쳤다.

그런데 실제 코드 단에서 테스트를 해보니 계속 401 Unauthorized 에러가 났다. 이유는 헤더에 전송하는 암호화한 데이터가 잘못되었던 것인데, 이를 해결하기 위해 암호화 알고리즘에 대해 많이 찾아보게 되었고 그 과정에서 알게 된 것들과 에러 해결 과정을 정리해 보려고 한다.

암호화 알고리즘?

암호화는 사실 깊게 파면 끝도 없을 주제일 것이다. 큰 분류만 보자면 아래와 같다.

  1. 대칭형 암호 (비밀키 암호)
  2. 비대칭형 암호 (공개키 암호)
  3. 단방향 암호 — 복호화가 불가능한 해시값

단방향 암호 종류

  • SHA-1 (취약점 발견되어 이제 사용 X)
  • SHA-2
  • SHA-3

이러한 대분에 따라 아래 속하는 종류들도 다양한데, 이 중에서 내가 필요로 하는 것은 단방향 암호인 HmacSHA256 알고리즘이다.

HMAC + SHA256

HMAC은 Hash-based Message Authentication의 약자로 해싱기법을 이용해서 메시지의 위변조가 있었는지 체크하는 기법이다.

해싱은 원문 메시지를 일정한 길이의 다른 메시지로 변환하는데, 이 원문을 해싱하여 나온 메시지를 다이제스트라고 하며, 여기서 SHA-2 해시 알고리즘을 사용한 256bit 길이의 다이제스트를 반환하는 것을 가리켜 HmacSHA256 이라고 한다.

동작 방식은 다음과 같다.

  1. 메세지를 보내는 Sender와 메세지를 받는 Receiver는 둘 다 특정 Key와 무슨 알고리즘을 사용할지 알고 있다.
  2. 보내는 쪽에서 보내고자 하는 내용을 특정 Key를 이용해 해시 알고리즘에 적용하여 시그니처 Signature라는 것을 생성한다.
  3. 받는 쪽에서도 Key와 해시 알고리즘을 사용해서 똑같이 Signature를 만들어서 비교한다.

어떻게 적용할까?

암호화 알고리즘의 동작원리를 알았으니 이제 실제 코드에 적용해보자.

ncloud 가이드에서는 아래와 같은 헤더들을 요청시 전송해야 한다고 나와있다.

여기서 x-ncp-apigw-signature-v2 헤더에 앞서 언급한 Signature를 생성해서 보내줘야 했다.

또한 Signature는 아래와 같은 조건으로 생성되어야 하는데 처음에는 이 생성 조건에 대한 이해가 부족해서 띄어쓰기와 같은 오타로 인해 첫 번째 삽질을 했었다. 😅

그리고 해당 문서에는 JavaScript 요청 예시가 브라우저 환경 기준으로 나와있었는데 덕분에 crypto-js 모듈의 존재를 알게 되었지만 버전과 문법에 차이가 있어서 이 부분에서 crypto-js 소스를 뒤져보면서 두 번째 삽질을 했다. Base64로 인코딩 하는 부분도 삽질에 한 몫 했다.

나는 Node 환경에서 진행하기 때문에 crypto-js npm 모듈을 이용해 아래와 같이 작성했다.

import hmacSHA256 from 'crypto-js/hmac-sha256';
import Base64 from 'crypto-js/enc-base64';
const stringToSign = `POST ${SEND_MESSAGE_URL}\n${timestamp}\n${ACCESS_KEY}`;
const hmacDigest = Base64.stringify(hmacSHA256(stringToSign, SECRET_KEY));

그런데 서치를 하다보니 Node에는 Crypto가 내장되어 있어서 별도의 모듈 추가 없이 사용할 수 있다고 한다.

그래서 다시 아래와 같이 작성을 했다.

import crypto from 'crypto';
const hmac = crypto.createHmac('sha256', SECRET_KEY);
const hmacDigest = hmac
.update(`POST ${SEND_MESSAGE_URL}\n${timestamp}\n${ACCESS_KEY}`) .digest('base64');

위 두 코드로 만들어진 다이제스트를 비교해보니 둘 다 같은 값이 나온다. 사용법이 조금 다르지만 무엇을 사용해도 무방할 것 같다.

그러나 이렇게 해도 계속 401에러가 나왔었는데… 정신없는 코드를 정리하고 문서를 다시 잘 살펴보니 Signature를 만들 때 사용되는 URL을 잘못 입력해두었던 것이다. (세 번째 삽질)

// 이렇게 하면 안되고
const SEND_MESSAGE_URL = `/services/${SERVICE_ID}/messages`;
// 이렇게 해야한다.
const SEND_MESSAGE_URL = `/sms/v2/services/${SERVICE_ID}/messages`;

이렇게 하고 호출하니 정상적으로 200응답을 받아 문자도 제대로 발송이 되었다.

느낀 점

알고나서 보니 쉬운 것 같지만, 암호화 관련 작업이 처음이었던 나에게는 도대체 무엇이 원인일까 하고 모르는 단어가 나올 때 마다 검색해보고 뒤져보고 하느라 사소한 실수를 발견하는데에도 많은 시간이 걸렸다.

그래도 이런 삽질 덕분에 많은 자료를 찾아보면서 새롭게 알게된 것들이 많았고, 이번 실수와 같은 일을 반복하지 말자는 점도 배울 수 있었다. 앞으로는 당연하게 생각했던 코드들도 놓치지 말고 항상 의심해봐야겠다!

참고자료

https://juneyr.dev/2019-06-10/spring-hmac

https://jusungpark.tistory.com/34

https://nodejs.org/docs/latest-v14.x/api/crypto.html#crypto_hmac_digest_encoding

https://dalkomit.tistory.com/18

https://effectivesquid.tistory.com/entry/Base64-인코딩이란

레모네이드 개발팀 기술 블로그입니다. This is an engineering blog from Lemonade Engineering.

레모네이드 개발팀 기술 블로그입니다. This is an engineering blog from Lemonade Engineering.