October 27, 20243 minutes
안녕하세요?
최근에 Cloudflare Workers를 사용하여 API의 인증 엔드포인트를 구축하면서 비밀번호를 해싱해야 할 필요가 있었습니다.
아래에 제 전체 코드를 포함했는데, 이렇게 준비된 예제를 한 곳에서 찾기가 어려웠기 때문입니다.
Cloudflare Workers는 서버리스 환경에서 JavaScript 코드를 실행할 수 있는 플랫폼입니다.
이 환경에서는 일반적인 Node.js 환경과는 다른 제약이 있는데, 특히 특정 Node.js 라이브러리를 사용할 수 없습니다.
예를 들어, 널리 사용되는 비밀번호 해싱 라이브러리인 bcrypt는 Node.js에서 C++로 작성된 추가 모듈을 사용하기 때문에, Cloudflare Workers와 같은 서버리스 환경에서는 사용할 수 없습니다.
대신, Cloudflare Workers는 브라우저 환경과 유사한 crypto API를 제공하여 암호화 및 해싱을 위한 기본 기능을 사용할 수 있게 합니다.
따라서 비밀번호 해싱을 위해서는 이 API를 활용하여 직접 구현해야 합니다.
PBKDF2(Password-Based Key Derivation Function 2)는 비밀번호를 안전하게 해싱하기 위해 설계된 알고리즘입니다.
이 알고리즘은 입력된 비밀번호를 바탕으로 고유한 키를 생성하는 데 사용되며, 다음과 같은 주요 요소를 포함합니다:
이러한 요소들 덕분에 PBKDF2는 비밀번호 보안에 매우 효과적입니다.
export async function hashPassword(
  password: string,
  providedSalt?: Uint8Array
): Promise<string> {
  const encoder = new TextEncoder();
  // 제공된 소금이 있을 경우 사용하고, 그렇지 않으면 새로 생성
  const salt = providedSalt || crypto.getRandomValues(new Uint8Array(16));
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    encoder.encode(password),
    { name: "PBKDF2" },
    false,
    ["deriveBits", "deriveKey"]
  );
  const key = await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: salt,
      iterations: 100000,
      hash: "SHA-256",
    },
    keyMaterial,
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );
  const exportedKey = (await crypto.subtle.exportKey(
    "raw",
    key
  )) as ArrayBuffer;
  const hashBuffer = new Uint8Array(exportedKey);
  const hashArray = Array.from(hashBuffer);
  const hashHex = hashArray
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  const saltHex = Array.from(salt)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  return `${saltHex}:${hashHex}`;
}keyMaterial와 AES-GCM 설명keyMaterial은 비밀번호를 바탕으로 생성된 원시 키 데이터를 의미합니다.
이 데이터는 PBKDF2 알고리즘을 통해 안전한 키로 변환되기 전에, 입력된 비밀번호를 바탕으로 생성됩니다.
비밀번호와 함께 사용되는 salt와 iterations는 이 키를 더욱 안전하게 만드는 데 중요한 요소입니다.
{ name: "AES-GCM", length: 256 }는 생성된 키를 AES-GCM(Advanced Encryption Standard in Galois/Counter Mode) 암호화 알고리즘에 사용할 것임을 나타냅니다.
AES는 대칭키 암호화 방식 중 하나로, 256비트 길이의 키를 사용하여 데이터를 암호화합니다.
GCM 모드는 인증과 암호화를 동시에 수행하여 데이터 무결성을 보장합니다.
이 조합은 비밀번호 해싱 및 저장 과정에서 보안을 강화하는 데 중요한 역할을 합니다.
export async function verifyPassword(
  storedHash: string,
  passwordAttempt: string
): Promise<boolean> {
  const [saltHex, originalHash] = storedHash.split(":");
  const matchResult = saltHex.match(/.{1,2}/g);
  if (!matchResult) {
    throw new Error("Invalid salt format");
  }
  const salt = new Uint8Array(matchResult.map((byte) => parseInt(byte, 16)));
  const attemptHashWithSalt = await hashPassword(passwordAttempt, salt);
  const [, attemptHash] = attemptHashWithSalt.split(":");
  return attemptHash === originalHash;
}사용자가 비밀번호를 입력하면 이를 해싱하여 저장할 수 있습니다.
예를 들어:
async function registerUser(password: string) {
  const hashedPassword = await hashPassword(password);
  console.log("Hashed Password:", hashedPassword);
  // 이 hashedPassword를 데이터베이스에 저장하세요.
}사용자가 로그인할 때 입력한 비밀번호가 저장된 해시와 일치하는지 확인할 수 있습니다.
예를 들어:
async function loginUser(storedHash: string, passwordAttempt: string) {
  const isMatch = await verifyPassword(storedHash, passwordAttempt);
  if (isMatch) {
    console.log("Password is valid!");
    // 로그인 성공 로직
  } else {
    console.log("Invalid password.");
    // 로그인 실패 로직
  }
}