본문으로 건너뛰기
뒤로 가기

AWS KMS Encryption Context로 이더리움 시드 키 보호하기

블록체인 지갑 서비스나 핀테크 앱을 개발할 때 가장 중요한 것은 Seed Key(Private Key)의 관리입니다. 단순히 DB에 암호화해 저장하는 것을 넘어, “특정 맥락(Context)이 일치하지 않으면 AWS 관리자조차 복호화할 수 없게” 만드는 방법을 실습을 통해 알아볼 예정입니다.

Table of contents

Open Table of contents

Part 1. 실전 CLI 핸즈온

준비 사항

Step 1. KMS 키 정책(Key Policy) 설정

먼저 KMS 키가 올바른 꼬리표(Context)를 가진 요청만 허용하도록 정책을 설정해야 합니다. AWS 콘솔에서 해당 KMS 생성 후 KMS의 Key Policy를 편집합니다.

핵심은 Condition 블록입니다. kms:EncryptionContext:Project가 EthWallet일 때만 암호화/복호화를 허용합니다.

{
  "Version": "2012-10-17",
  "Id": "key-policy-with-context",
  "Statement": [
    {
      "Sid": "Enable IAM User Permissions",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:root"
      },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "Allow use of the key only with valid context",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:user/YOUR_KEY_USER"
      },
      "Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:EncryptionContext:Project": "EthWallet"
        }
      }
    }
  ]
}

Step 2. 작업 환경 구성 및 시드 키 생성

터미널을 열고 실습 폴더로 이동한 뒤, 테스트용 시드 키 파일을 생성합니다. 여기서는 이더리움 Private key를 사용했습니다.

0xabf82ff96b463e9d82b83cb9bb450fe87e6166d4db6d7021d0c71d7e960d5abe
echo "0xabf82ff96b463e9d82b83cb9bb450fe87e6166d4db6d7021d0c71d7e960d5abe" > private_key.txt

Step 3. 암호화 (Encrypt)

이제 KMS를 이용해 암호화를 진행합니다. 이때 --encryption-context 옵션을 반드시 포함해야 정책에 의해 승인됩니다.

aws kms encrypt \
  --region us-east-1 \
  --key-id YOUR-KEY-ID \
  --plaintext fileb://private_key.txt \
  --encryption-context Project=EthWallet \
  --output text \
  --query CiphertextBlob | base64 -D > private_key.encrypted

주의: key-id를 각자 여러분의 kms key-id로 변경, region은 **"us-east-1"**인지 확인

체크 포인트:


Step 4. 복호화 테스트 (성공 vs 실패)

이 튜토리얼의 하이라이트입니다. 암호화된 파일을 다시 풀 때, Context 유무에 따른 차이를 확인해 봅시다.

✅ Case 1: 성공 (올바른 Context 입력)

정책에서 요구하는 Project=EthWallet을 명시하면 정상적으로 원문이 출력됩니다.

aws kms decrypt \
  --region us-east-1 \
  --ciphertext-blob fileb://private_key.encrypted \
  --encryption-context Project=EthWallet \
  --output text \
  --query Plaintext | base64 -D

결과: 0xabf82ff96b463e9d82b83cb9bb450fe87e6166d4db6d7021d0c71d7e960d5abe

❌ Case 2: 실패 (Context 누락 또는 불일치)

해커가 파일을 탈취했거나, 내부자가 권한을 오용하려 할 때 Context를 모르면 어떻게 될까요?

aws kms decrypt \
  --region us-east-1 \
  --ciphertext-blob fileb://private_key.encrypted \
  --output text \
  --query Plaintext

결과: An error occurred (AccessDeniedException)…

해설: 키 사용 권한이 있는 Admin이라도, 문맥(Context)이 일치하지 않으므로 KMS가 복호화를 거부합니다.


Part 2. 심화: 클라이언트 사이드 봉투 암호화 (Envelope Encryption)

실제 프로덕션 환경에서는 보안 표준인 클라이언트 사이드 봉투 암호화를 적용해야 합니다.

왜 이 방식이 필요한가요?

AWS로 평문 데이터를 보내지 않고, 클라이언트(브라우저/앱) 내부에서 암호화를 완료한 뒤 결과물만 전송하기 때문입니다. AWS조차도 여러분의 시드 키 원문을 볼 수 없습니다.

전체 아키텍처 흐름

1. 암호화 및 저장 (Client → AWS)

  1. DEK 요청: 클라이언트가 KMS에 GenerateDataKey를 요청합니다.
  2. DEK 수신: KMS는 ‘평문 키(Plaintext DEK)‘와 ‘암호화된 키(Encrypted DEK)’ 두 개를 줍니다.
  3. 로컬 암호화: 클라이언트는 메모리 상에서 ‘평문 키’로 시드 구문을 암호화합니다.
  4. (핵심 요소) 메모리 소거: 암호화 직후, 메모리에서 ‘평문 키’와 ‘시드 구문’을 즉시 삭제합니다.
  5. 전송: ‘암호화된 시드’와 ‘암호화된 키’만 백엔드 DB에 저장합니다.

실습 준비 (Node.js)

npm init -y
npm install @aws-sdk/client-kms

Step 1. 암호화 스크립트 (encrypt.js)

이 스크립트는 KMS에서 키 발급 → 로컬 암호화 → 키 폐기 → 파일 저장 과정을 수행합니다.

encrypt.js 파일을 생성하고 아래 코드를 붙여넣으세요.

const { KMSClient, GenerateDataKeyCommand } = require("@aws-sdk/client-kms");
const fs = require("fs");
const crypto = require("crypto");

// 설정
const KEY_ID = "alias/kms-key";
const REGION = "us-east-1";
const CONTEXT = { Project: "EthWallet" };

const client = new KMSClient({ region: REGION });

async function envelopeEncrypt(plainTextSecret) {
  console.log(
    `🔒 [1단계] KMS (${REGION})에 데이터 키(DEK) 생성을 요청합니다...`
  );

  // 1. 데이터 키 생성 요청
  const command = new GenerateDataKeyCommand({
    KeyId: KEY_ID,
    KeySpec: "AES_256",
    EncryptionContext: CONTEXT,
  });
  const response = await client.send(command);

  const plaintextDek = Buffer.from(response.Plaintext); // 쓰고 버릴 키
  const encryptedDek = Buffer.from(response.CiphertextBlob); // 저장할 키

  console.log("⚡ [2단계] 로컬 메모리에서 AES-256-GCM 암호화를 수행합니다...");

  // 2. 로컬 암호화
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", plaintextDek, iv);
  let encryptedData = cipher.update(plainTextSecret, "utf8");
  encryptedData = Buffer.concat([encryptedData, cipher.final()]);
  const authTag = cipher.getAuthTag();

  // 3. 키 폐기 (가장 중요!)
  plaintextDek.fill(0);
  console.log("🗑️  [3단계] 보안을 위해 평문 키를 메모리에서 삭제했습니다.");

  return {
    EncryptedData: encryptedData.toString("base64"),
    EncryptedDEK: encryptedDek.toString("base64"),
    IV: iv.toString("base64"),
    AuthTag: authTag.toString("base64"),
  };
}

(async () => {
  try {
    const mySeed =
      "0xabf82ff96b463e9d82b83cb9bb450fe87e6166d4db6d7021d0c71d7e960d5abe";
    console.log(`원본 데이터: ${mySeed.substring(0, 10)}...`);
    const result = await envelopeEncrypt(mySeed);
    fs.writeFileSync("mock_database.json", JSON.stringify(result, null, 2));
    console.log(
      "✅ [4단계] 암호화된 데이터를 'mock_database.json'에 저장했습니다."
    );
  } catch (err) {
    console.error(err);
  }
})();
node encrypt.js
원본 데이터: 0xabf82ff9...
🔒 [1단계] KMS (us-east-1)에 데이터 키(DEK) 생성을 요청합니다...
   👉 사용 중인 키 ID: alias/kms-key
⚡ [2단계] 로컬 메모리에서 AES-256-GCM 암호화를 수행합니다...
🗑️  [3단계] 보안을 위해 평문 키를 메모리에서 삭제했습니다.
✅ [4단계] 암호화된 데이터를 'mock_database.json'에 저장했습니다.

2. 복호화 및 사용 (AWS → Client)

  1. 데이터 수신: 클라이언트가 DB에서 암호화된 데이터들을 받아옵니다.
  2. 키 복구: 클라이언트는 ‘암호화된 키’를 KMS에 보내 Decrypt를 요청합니다. (이때 올바른 Encryption Context가 필수입니다.)
  3. 원문 복구: KMS가 돌려준 ‘평문 키’로 시드 구문을 복호화하여 서명에 사용하고, 다시 메모리에서 즉시 삭제합니다.

Step 2. 복호화 스크립트 (decrypt.js)

이 스크립트는 DB 로드 → KMS로 키 복구 → 로컬 복호화 과정을 수행합니다.

decrypt.js 파일을 생성하고 아래 코드를 붙여넣으세요.

const { KMSClient, DecryptCommand } = require("@aws-sdk/client-kms");
const fs = require("fs");
const crypto = require("crypto");

// --- 설정 ---
const REGION = "us-east-1";
const CONTEXT = { Project: "EthWallet" }; // 암호화 시 사용한 것과 동일해야 함

const client = new KMSClient({ region: REGION });

async function envelopeDecrypt() {
  // 1. DB 파일 읽기
  if (!fs.existsSync("mock_database.json")) {
    console.log("❌ DB 파일이 없습니다. encrypt.js를 먼저 실행하세요.");
    return;
  }
  const dbRecord = JSON.parse(fs.readFileSync("mock_database.json", "utf8"));

  // Base64 문자열을 다시 Buffer로 변환
  const encryptedDek = Buffer.from(dbRecord.EncryptedDEK, "base64");
  const encryptedData = Buffer.from(dbRecord.EncryptedData, "base64");
  const iv = Buffer.from(dbRecord.IV, "base64");
  const authTag = Buffer.from(dbRecord.AuthTag, "base64");

  console.log("KEY 🔑 [1단계] KMS에 잠긴 키(DEK)의 해제를 요청합니다...");

  try {
    // 2. KMS에 Decrypt 요청
    const command = new DecryptCommand({
      CiphertextBlob: encryptedDek,
      EncryptionContext: CONTEXT,
    });

    const response = await client.send(command);
    const plaintextDek = Buffer.from(response.Plaintext);

    console.log("🔓 [2단계] 로컬에서 데이터를 복구합니다...");

    // 3. 로컬 복호화 (AES-256-GCM)
    const decipher = crypto.createDecipheriv("aes-256-gcm", plaintextDek, iv);
    decipher.setAuthTag(authTag); // GCM 모드는 Auth Tag 검증이 필수

    let decrypted = decipher.update(encryptedData);
    decrypted = Buffer.concat([decrypted, decipher.final()]);

    // 4. 키 삭제
    plaintextDek.fill(0);

    return decrypted.toString("utf8");
  } catch (err) {
    console.error("❌ 복호화 실패! (Context 불일치 또는 권한 문제)");
    throw err;
  }
}

// --- 실행부 ---
(async () => {
  try {
    const recoveredText = await envelopeDecrypt();
    console.log(`✅ [완료] 복구된 원본 데이터: ${recoveredText}`);
    console.log("🎉 봉투 암호화/복호화 성공!");
  } catch (err) {
    // 에러 처리
  }
})();
node decrypt.js

컨텍스트가 다르다면 아래와 같이 에러 발생:

KEY 🔑 [1단계] KMS에 잠긴 키(DEK)의 해제를 요청합니다...
❌ 복호화 실패! (Context 불일치 또는 권한 문제)

컨텍스트가 같다면 아래와 같이 성공:

KEY 🔑 [1단계] KMS에 잠긴 키(DEK)의 해제를 요청합니다...
🔓 [2단계] 로컬에서 데이터를 복구합니다...
✅ [완료] 복구된 원본 데이터: 0xabf82ff96b463e9d82b83cb9bb450fe87e6166d4db6d7021d0c71d7e960d5abe
🎉 봉투 암호화/복호화 성공!

결론

AWS KMS의 Encryption Context와 봉투 암호화를 결합하면, 데이터 전송 구간(TLS), 저장 공간(DB), 그리고 **키 관리 시스템(KMS)**까지 삼중으로 보호되는 강력한 보안 아키텍처를 구축할 수 있습니다.


이 글 공유:

이전 글
Auto Scaling으로 사라지는 로그, 어떻게 지킬까? Lifecycle Hook과 SSM을 활용한 데이터 보존 전략
다음 글
컨테이너 공격 표면 최소화: Linux Capabilities로 Root 권한 제거하기