ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 문자 인증 API 구현을 위한 암호화 방식 이해하기
    Develop/Web & Framework 2021. 10. 17. 17:30

    진행 중인 사이드 프로젝트에서 문자 인증 기능이 필요했고, Naver Cloud의 Simple&Easy Notification Service를 이용하기로 하였다.

    프로젝트를 생성하고, 계정 인증 키를 발급받는 것까지는 어렵지 않았다.

    serviceId, accessKey, secretKey 세 종류의 key가 필요한데 헷갈리기 아주 딱 좋다.
    serviceId는 요청URL에 포함시킬 path parameter로서 사용되며, 서비스 프로젝트를 생성하는 즉시 발급된다.
    accessKey, secretKey는 네이버 클라우드 계정에 대한 인증키 값으로, 메뉴 > 마이페이지 > 인증키 관리 메뉴에서 발급받고 관리할 수 있다.

     

    내가 지금까지 호출하여 사용해본 API들은 대부분 인증 key값만 raw하게 header에 포함시키면 바로 요청을 보낼 수 있었다.

    하지만 해당 API의 경우, x-ncp-apigw-signature-v2 라는 암호화된 서명값을 header에 포함시켜 요청을 전송하도록 구성되어 있었다.

    POST https://sens.apigw.ntruss.com/sms/v2/services/{serviceId}/messages
    
    Content-Type: application/json; charset=utf-8
    x-ncp-apigw-timestamp: {Timestamp}
    x-ncp-iam-access-key: {Sub Account Access Key}
    x-ncp-apigw-signature-v2: {API Gateway Signature}

    naver cloud sms api 호출을 위한 authparams


    암호화 알고리즘은 크게 3가지로 구분될 수 있다.

    1. 단방향 암호(One-way cryptography)

    평문을 hash처리하는 것으로, 복호화가 불가능한 단방향 암호화 방식이다. 주로 패스워드 암호화에 사용된다.
    데이터가 약간만 달라져도 hashing이 적용되면 전혀 다른 암호문이 생성되며, 이를 '눈사태 효과'라고 부른다.
    (장고에서도 user의 password는 SHA256 알고리즘을 사용하여 암호화된 값으로 DB에 저장되며 관리자도 raw한 password값은 알 수 없다)

    2. 대칭형 암호 (비밀키 암호, Symmetric key cryptography)

    평문이 대칭키로 암호화되며, 암호문 또한 해당 대칭키로 복호화가 가능하다.
    즉 암호화를 수행하는 측과 복호화를 수행하는 측이 동일한 대칭키를 공유해야 한다.
    가장 보편적으로 사용되는 알고리즘은 AES 알고리즘이며, 대칭키 암호는 암호화 단위에 따라 스트림 암호, 블록 암호로 나눌 수 있다.

    • 스트림 암호: 연속적인 bit/byte를 입력받아, 그에 대응하는 암호화된 bit/byte를 생성
    • 블록 암호: 한 단위(block)를 입력받아, 그에 대응하는 암호화된 block을 생성

    3. 비대칭형 암호(공개키 암호, Non-Symmetric key cryptography)

    공개키(public), 개인키(private)의 쌍의 개념으로 동작한다. 암호화와 복호화에 서로 다른 키를 사용한다.
    A의 공개키로 암호화된 암호문은 A의 개인키로만 복호화가 가능하다.
    즉, 내가 A에게 메세지를 전달하고 싶다면, A의 공개키로 평문을 암호화해서 전송하면 된다. 그 후로 A는 자신이 가지고 있는 개인키로 암호문을 복호화하여 수신할 수 있을 것이다. 


    이 중 HmacSHA256은 단방향 암호화에 속하는 알고리즘이다.

    HMAC(Hash-based Message Authentication Code)와 SHA(Secure Hash Algorithm)256을 조합한 것으로
    해싱기법을 이용하여 메세지의 위변조 여부를 체크하는 알고리즘이다.

    HMAC은 sender와 receiver가 공유하고 있는 키와 메세지(대칭형 암호)를 사용하여 hash value를 만들어 내는 것이다. 

    sender가 데이터와 데이터를 hashing한 값인 HMAC을 전송하면, receiver는 수신한 메세지에 대하여 hash값을 계산하여 해당 HMAC과 수신받은 HMAC의 일치 여부를 확인한다.

    HMAC-256은 secret key를 메세지 데이터와 결합하여 hashing한 후, 그 값을 secret key와 다시 혼합하여 hash함수를 한 번 더 적용하여 길이가 256비트인 hash sequence를 생성한다.


    실제 서명값을 만들어내는 함수는 다음과 같이 작성할 수 있다.

    1. 요청 method, serviceId를 포함한 uri, timestamp, accessKey값으로 message를 생성한다.
    2. message와 secretKey값에 Hmac256 알고리즘을 적용하여 암호화된 값을 생성한다.
    3. 암호화된 값을 base64로 인코딩한 값을 서명으로 사용한다.
    base64 인코딩은 binary data를 text로 변경하는 encoding 방식이다
    binary data를 6bit 단위로 잘라서, 색인표에 해당하는 value로 치환한다.


    import base64
    import hashlib
    import hmac
    import requests
    import time
    
    from Troy.settings import base
    
    class AuthSMSService(object):
        def __init__(self):
            self.naver = getattr(base, 'auth')['naver']
            self.url = 'https://sens.apigw.ntruss.com'
            self.uri = f'/sms/v2/services/{self.naver["service_id"]}/messages'
            
    	def make_signature(self, timestamp):
            secret_key = bytes(self.naver['secret_key'], 'UTF-8')
    
            message = "POST "+self.uri+"\n"+timestamp+"\n"+self.naver['access_key']
            message = bytes(message, 'UTF-8')
            signingKey = base64.b64encode(hmac.new(secret_key, message, digestmod=hashlib.sha256).digest())
    
            return signingKey

     📌 개발 중인 서비스에서는 회원가입 시 휴대폰 인증을 수행한다. 하지만 문자 인증의 경우 다른 기능에서도 활용될 가능성이 있다고 생각되어 별도의 service class로 분리하였다

    생성한 서명값을 header에 넣어서 sms 발송을 위한 url에 request를 보내면,

        def send_sms(self, phone_number, auth_number):
            timestamp = str(int(time.time()*1000))
            context = {
                'type': 'SMS',
                'contentType': 'COMM',
                'countryCode': '82',
                'from': self.sender,
                'content': f'Troy 인증번호 [{auth_number}]를 입력해주세요.',
                'messages': [{
                    'to': phone_number
                }]
            }
            headers = {
                'Content-Type': 'application/json; charset=utf-8',
                'x-ncp-apigw-timestamp': timestamp,
                'x-ncp-iam-access-key': self.naver['access_key'],
                'x-ncp-apigw-signature-v2': self.make_signature(timestamp=timestamp)
            }
            response = requests.post(self.url+self.uri, headers=headers, json=context)
    
            if not response.ok:
                raise ValidationError(response.reason)
    
            return True

    문자 발송이 된 것을 확인할 수 있다!!

     

    API 가이드에 따라 인코딩한 서명값을 생성하고 요청을 보냈는데, 계속해서 401(Unauthorized) 에러가 발생하였다.
    서명값, header값, url값, body 하나하나 디버깅하며 확인해보니 body data를 잘못 구성했었다... 삽질...

     

    댓글

Designed by Tistory.