브라우저에 키를 안전하게 저장하는 법

negu63·2022년 8월 12일
2
post-thumbnail

읽으면 얻을 수 있는 것

  • 브라우저에 암호 키를 안전하게 저장하는 방법
  • 약간의 webcrypto API 지식
  • 약간의 indexedDB 지식

발단

종단 간 암호화를 구현하는 과정에서 클라이언트에 암호 키를 저장해야 할 필요가 생겼습니다.

그런데 웹에는 안전한 키 저장소가 없다고 생각해서 앱으로 다시 개발하고 있었습니다.

그러다가 브라우저에서 작동하는 암호 화폐 지갑들은 개인 키를 어떻게 저장하는지에 의문이 생겼고 이에 대해 검색하던 중에 괜찮은 방법을 찾게 되었습니다.

지갑들이 이 방식을 사용하는지는 모르겠지만 좋은 방법인 것 같아서 공유하고자 글을 썼습니다!

실마리

검색하면서 여러 가지 방법들을 봤는데 '이거다!' 하는 방법은 딱히 없었습니다.

그런데 이 글의 답변에서 딱 느낌이 왔습니다.

요약하자면 webcrypto API로 키를 추출할 수 없는 키 객체를 만들고 IndexedDB에 해당 키 객체를 저장해서 사용하라는 것입니다.

webcrypto API..?

IndexedDB...?

뒤로가기 멈춰!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

간단하게 알아봅시다.

webcrypto API

해싱, 서명 생성 및 확인, 암호화 및 암호 해독과 같은 기본 암호화 작업을 웹에서 수행하기 위한 자바스크립트 API - 위키피디아

대칭 키 암호화, 비대칭 키 암호화, 타원 곡선 서명 등 암호화에 관련된 다양한 기능을 지원합니다.

localhost 환경과 https에서 사용할 수 있습니다.

예전에 이걸 몰라서 http에서 쓰다가 고생했던 기억이 나네요...

IndexedDB API

파일이나 블롭 등 많은 양의 구조화된 데이터를 클라이언트에 저장하기 위한 로우 레벨 API - MDN

IndexedDB를 사용하면 웹 스토리지 (세션 & 로컬 스토리지)보다 많은 양의 데이터를 저장할 수 있고 구조화된 데이터 객체(!!)를 저장할 수 있습니다.

사용자의 브라우저에 데이터를 영구적(!!)으로 저장할 수 있습니다.

모든 작업은 비동기로 이루어지며 쿼리(!!)가 가능합니다.

실행

퍼즐 조각들을 모았으니 맞춰볼 시간입니다.
실제로 구현해봅시다.

목표

클라이언트 사이드에서 webcrypto api로 대칭 키를 생성해서 IndexedDB에 저장하고 키 노출 없이 암/복호화를 하는 것입니다.

UI 구성

간단하게 필요한 것만으로 구성했습니다.

암호화할 텍스트, 암호화된 텍스트, 복호화된 텍스트 상태도 함께 만들었습니다.

dexie 사용

IndexedDB에 편하게 접근하기 위해서 dexie라는 라이브러리를 사용하겠습니다.

사용할 데이터베이스 구조에 맞게 db.ts 파일을 작성하고

아래와 같이 불러와서 사용하면 됩니다.

import { db } from "./db";

코드 작성

블록 암호화 중 AES-CBC 알고리즘을 사용하여 암/복호화를 하도록 하겠습니다.

webcrypto API

webcrypto API는 전역 객체(window)의 프로퍼티이기 때문에 그냥 crypto로 불러와서 바로 사용할 수 있습니다.

// 예시 - 16비트 랜덤 바이트 생성
crypto.getRandomValues(new Uint8Array(16))

iv

iv는 초기화 벡터(initial vector)의 줄임말입니다.

iv는 AES-CBC 암/복호화에 필요한 값이며 각 암호문을 고유하게 만드는 데 사용됩니다.

동일한 키로 동일한 평문을 암호화한다고 해도 iv가 다르면 다른 결과가 생성됩니다.

iv는 공개되어도 상관없지만 동일한 키와 함께 다른 평문을 암호화할 때 재사용하는 것은 좋지 않다고 합니다.

암/복호화에 공통으로 사용되기 때문에 useRef를 이용해 리렌더링 되어도 값이 유지되게 해주겠습니다.

const iv = useRef(crypto.getRandomValues(new Uint8Array(16)));

키 생성

AES-CBC 256비트 키를 생성하고 IndexdedDB에 저장하는 함수입니다.

async function generateAESKey() {
  const key = await crypto.subtle.generateKey(
    { name: "AES-CBC", length: 256 },
    false, // 중요! extractable 옵션
    ["encrypt", "decrypt"]
  );

  // indexedDB에 키 객체 저장
  await db.key.clear();
  await db.key.add({ key });
}

webcrypto 스펙의 14.2.10 The exportKey() method 부분을 보면 키 객체의 내부 슬롯 [[extractable]]의 값(boolean)에 따라 키 추출 여부가 결정됩니다.

그래서 extractable이 false면 어떤 방식으로도 키를 얻을 수 없습니다.

하지만 이 키 객체를 이용해서 암/복호화는 할 수 있습니다.

▲ IndexedDB에 저장되어 있는 키 객체의 모습

암호화

IndexedDB에서 키 객체를 가져와서 암호화하는 함수입니다.

  async function encryptWithAESKey() {
    iv.current = crypto.getRandomValues(new Uint8Array(16)); // create new iv
    const encoder = new TextEncoder();
    const key = (await db.key.limit(1).toArray())[0].key as CryptoKey;
    const encrypted: ArrayBuffer = await crypto.subtle.encrypt(
      {
        name: "AES-CBC",
        iv: iv.current,
      },
      key,
      encoder.encode(text) // string to bytes
    );
    setCryptogram(Buffer.from(new Uint8Array(encrypted)).toString("base64")); // bytes to base64 string
  }

string인 텍스트를 바이트 배열로 바꾸고 AES-CBC 키와 초기화 벡터로 암호화한 다음 base64 문자열로 바꿔서 암호화 텍스트 state에 넣어줍니다.

복호화

IndexedDB에서 키 객체를 가져와서 복호화하는 함수입니다.

  async function decryptWithAESKey(cryptogram: string) {
    const decoder = new TextDecoder();
    const key = (await db.key.limit(1).toArray())[0].key as CryptoKey;
    const encrypted = Buffer.from(cryptogram, "base64"); // base64 string to bytes
    const decrypted: ArrayBuffer = await crypto.subtle.decrypt(
      { name: "AES-CBC", iv: iv.current },
      key,
      encrypted
    );
    setPlainText(decoder.decode(decrypted));
  }

base64 문자열을 바이트 배열로 바꾸고 AES-CBC 키와 초기화 벡터로 복호화한 다음 string으로 바꿔서 복호화 텍스트 state에 넣어줍니다

전체 코드

이 곳에서 전체 코드를 볼 수 있습니다.

데모

이 곳에서 위 기능들로 이루어진 암/복호화를 직접 해볼 수 있습니다.

개발자 도구(F12) > Application > IndexedDB를 보면 키 객체가 들어있는 것을 확인할 수 있습니다.

결과

webcrypto API로 생성한 키 추출이 불가능한 키 객체를 만들어 IndexedDB에 저장함으로써 키를 노출하지 않고 암/복호화를 할 수 있음을 확인했습니다.

pwa와 궁합이 좋은 것 같아 앞으로 활용성이 매우 높을 것으로 생각됩니다.

마치며

webcrypto API가 저에게 필요한 secp256k1 타원 곡선은 지원을 안 해서 아쉬웠습니다.

앞으로 지원할 예정도 없다고 합니다.. . ... ....

또 다른 방법을 찾아야겠네요.

읽어주셔서 감사합니다 ~

profile
No matter how long it take

1개의 댓글

comment-user-thumbnail
2024년 3월 8일

감사합니다 도움이되었습니다!

답글 달기