Skip to content

labyrinth30/hiding-pk

Repository files navigation

🛡️ NestJS Secret PK: Primary Key Hiding Example

"외부에는 암호화된 ID를, 내부에는 숫자 ID를."

NestJS의 AOP(Pipe, Interceptor, Decorator)를 활용하여 비즈니스 로직 침범 없이 Auto Increment PK를 숨기는 예제 프로젝트입니다.

🧐 Background

REST API를 개발할 때 GET /posts/1, GET /users/100 처럼 Auto Increment PK를 그대로 노출하면 다음과 같은 보안 위협과 문제가 발생할 수 있습니다.

  1. Insecure Direct Object References (IDOR): 예측 가능한 패턴으로 인해 권한 없는 리소스에 접근 시도가 쉬워집니다.
  2. Business Intelligence Leak: id: 100 다음에 id: 101이 생성되는 것을 보고 유저가 서비스의 성장 추이나 데이터 개수를 유추할 수 있습니다.
  3. Crawling: 연속된 숫자를 대입하여 손쉽게 데이터를 긁어갈 수 있습니다.

UUID를 사용하는 방법도 있지만, 이는 저장 공간 낭비와 RDBMS의 Clustered Index 성능 저하를 유발합니다.

따라서 "DB 성능을 위해 내부적으로는 Auto Increment(Number)를 유지하되, 외부 통신 시에만 암호화된 문자열(String)을 사용하는" 방법을 고안했습니다.

🚀 Key Features (핵심 기능)

이 프로젝트는 관심사의 분리(Separation of Concerns) 원칙을 지키기 위해 NestJS의 기능을 적극 활용했습니다.

  1. Hybrid CipherService:
    • Node.js crypto 모듈(AES-256-CTR) 기반 암호화.
    • **DI(Dependency Injection)**와 Decorator 양쪽에서 사용하기 위해 InstanceStatic 변수를 모두 활용한 하이브리드 구조 설계.
  2. Transparent Decryption (Request):
    • DecryptIdPipe: 요청이 컨트롤러에 도달하기 전, 암호화된 ID를 자동으로 복호화하여 Controller는 순수한 숫자(Number)로 로직을 수행합니다.
  3. Transparent Encryption (Response):
    • @SecretPk Decorator: 응답이 나갈 때 class-transformer를 통해 ID를 자동으로 암호화합니다.
  4. Deterministic Encryption:
    • URL의 영속성을 위해 고정된 IV(Initialization Vector)를 사용하여, 동일한 ID는 언제나 동일한 암호문을 가집니다.

🛠 Tech Stack

  • Framework: NestJS
  • Language: TypeScript
  • Encryption: Node.js crypto (AES-256-CTR), scryptSync
  • Serialization: class-transformer
  • Validation: class-validator

📦 Installation & Running

# 1. 패키지 설치
$ pnpm install

# 2. 환경 변수 설정 (.env 파일 생성 필요)
# 아래 'Environment Variables' 섹션 참고

# 3. 서버 실행
$ pnpm run start:dev

🔑 Environment Variables

프로젝트 루트에 .env 파일을 생성하고 다음 값을 설정해 주세요. (보안을 위해 강력한 비밀번호를 사용하세요.)

# 암호화에 사용될 비밀키 (문자열)
SECRET_PK_KEY=your-super-secret-password-should-be-long

# 키 생성을 위한 솔트 (Salt)
SECRET_PK_SALT=random-salt-value

# 고정된 초기화 벡터 (정확히 16글자여야 함)
SECRET_PK_IV=0123456789012345

📝 Usage Example

1. Controller Code (Clean Architecture)

컨트롤러는 암호화/복호화 로직을 전혀 신경 쓰지 않습니다.

@Get(':id')
// Pipe가 암호문을 숫자로 변환해 줍니다.
findOne(@Param('id', DecryptIdPipe) id: number) {
  // id는 number 타입입니다 (ex: 12345)
  return this.service.findOne(id);
}

2. DTO Code

응답 객체에 데코레이터만 붙이면 됩니다.

export class UserResponseDto {
  @SecretPk() // 나갈 때 자동 암호화
  id: number;
  
  email: string;
}

3. API Response

  • Internal DB: id: 1
  • API Response:
    {
      "id": "32eaa1fe35f6...", 
      "email": "user@example.com"
    }

📂 Project Structure

핵심 로직은 src/common에 모여 있습니다.

src
├── common
│   ├── cipher.service.ts        # 암호화 코어 (Static/Instance Hybrid)
│   ├── decrypt-id.pipe.ts       # 요청 복호화 파이프
│   └── secret-pk.decorator.ts   # 응답 암호화 데코레이터
├── dto
│   └── sample.dto.ts            # @SecretPk 적용 예시
├── app.controller.ts            # 적용된 컨트롤러 예시
└── main.ts                      # 전역 Interceptor/Pipe 설정

🔗 Blog Post

이 프로젝트의 상세한 구현 과정과 고민은 아래 블로그에서 확인할 수 있습니다.

Releases

No releases published

Packages

No packages published