일회용 암호를 서버 측과 클라이언트 측에서 만드는 샘플

일회용 비밀번호 로직을 만들어 보았습니다.



2020/09/25 현재는 무언가와 은행이 크랙 되고 있어도, 모두 원타임 패스워드와 같은 인증이 들어 있지 않다. 물론 원타임 패스워드도 '그 원타임 사이에' 훔치면 아웃이지만, 상시 통신 내용을 빼앗는 것은 훔치는 분도 힘들다.

다만, 일회용 비밀번호의 구조가 Google Authenticator 등의 외부의 구조에 의존하는 것은, 다른 의미로 리스크(갑자기 사양이 바뀌는 등).

그래서, 자기 앞에서 어디까지 할 수 있는지를 검증.

어떻게 움직이는가?



다음 요소를 조합하여 다이제스트(sha256)를 생성하고 다이제스트에서 숫자 6자리를 검색합니다.
  • 각 사용자마다 다른 개인 키 (클라이언트와 서버에서 동일한 개인 키가 있음)
  • 타임스탬프(이번 샘플에서는 60초로 나눈 정수부를 사용합니다)
  • 솔트 (서버 측에 보관하는 긴 문자열)

  • 샘플 소스



    단말측



    onetime.js
    
    async function digestMessage(message) {
      const encoder = new TextEncoder();
      const data = encoder.encode(message);
      const hash = crypto.subtle.digest('SHA-256', data);
      return hash;
    }
    
    function buf2hex(buffer) { // buffer is an ArrayBuffer
      return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
    }
    
    async function onetimePass(secret_key){
      const ts_min = parseInt( Date.now() / 60000 ).toString();
      const buffer = await digestMessage(secret_key + '_and_sault' + ts_min);
      const hexString = '0x' + await buf2hex(buffer) ;
      const digitString = BigInt(hexString).toString(10);
      return digitString.slice(-6);
    }
    

    onetime.html
    <html>
    <script type="text/javascript" src="./onetime.js"></script>
    <body>
    <script>
    (async () => {
    document.write(await onetimePass('secret_key'));
    })();
    </script>
    </body>
    </html>
    

    서버측



    onetime.rb
    require 'digest/sha1'
    
    def onetime_pass(secret_key, next_min = false)
      # タイムスタンプを秒から分に丸める
      ts_min = Time.now.to_i / 60
      ts_min += 1 if next_min # 次の分までカバーしたい場合(チェックを受ける側)
      text = "#{secret_key}_and_sault"
      Digest::SHA256.hexdigest("#{text}#{ts_min}").to_i(16).to_s[-6, 6]
    end
    
    # ちょうど一分をまたがることを想定して、一分後のパスワードも取得できるようにしてる
    puts onetime_pass('secret_key')
    puts onetime_pass('secret_key', true)
    

    실행 결과



    프런트 측과 서버 측에서 같은 6자리수가 되어 있는 것을 알 수 있다.
    (서버측의 니행째는, 1분 후의 패스워드)



    마음 나머지


  • 오랜만에 JS 쓰면 무엇 실행해도 Promise만 돌아온다. 시행 착오로, async, await 를 써 왔지만, 별로 자신이 없다.
  • RFC에도 제대로 일회용 패스워드의 로직은 있는 것 같지만 읽지 않고 「반드시 이런 것일 것이다」로 끝나고 있다
  • 단말측은 JS로 써 버렸지만, 중요한 「비밀키를 건네주는 방법」이 이 기사에서는 접하지 않았다. QR코드로 건네주는 것이 좋을 것 같지만, QR코드를 읽어 단말측의 로커 스토리지에 비밀열쇠를 보존한다고 하는 프로그램이 상당히 상기의 소스보다 길어질 것 같다. 그런 것까지 생각하면, Google Authenticator를 사용해, Google Authenticator의 서버측의 라이브러리를 사용해 버리므로 해결인가.
  • sha256으로 다이제스트를 만들었지만, 10진수로 하여 마지막 6자리를 꺼내고 있는 시점에서, 충돌 확률은 증가한다.
  • 좋은 웹페이지 즐겨찾기