PHP에서 암호화/복호화 오류 문제 해결하기

프리터코더·2025년 6월 2일
0

php 문제 해결

목록 보기
54/79

암호화/복호화 과정에서 발생하는 오류들은 보안과 직결되는 중요한 문제입니다. 안전하고 올바른 암호화 구현 방법들을 알아보겠습니다.

1. 안전한 대칭키 암호화

<?php
class SecureEncryption {
    private $cipher = 'aes-256-gcm';
    private $key;
    
    public function __construct($key = null) {
        if ($key === null) {
            $this->key = random_bytes(32); // 256비트 키
        } else {
            $this->key = hash('sha256', $key, true);
        }
    }
    
    public function encrypt($data) {
        $iv = random_bytes(12); // GCM 모드용 12바이트 IV
        $tag = '';
        
        $encrypted = openssl_encrypt(
            $data,
            $this->cipher,
            $this->key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );
        
        if ($encrypted === false) {
            throw new Exception('암호화 실패: ' . openssl_error_string());
        }
        
        // IV + 태그 + 암호화된 데이터를 결합
        return base64_encode($iv . $tag . $encrypted);
    }
    
    public function decrypt($encryptedData) {
        $data = base64_decode($encryptedData);
        
        if ($data === false) {
            throw new Exception('Base64 디코딩 실패');
        }
        
        $iv = substr($data, 0, 12);
        $tag = substr($data, 12, 16);
        $encrypted = substr($data, 28);
        
        $decrypted = openssl_decrypt(
            $encrypted,
            $this->cipher,
            $this->key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );
        
        if ($decrypted === false) {
            throw new Exception('복호화 실패: 데이터가 손상되었거나 키가 잘못됨');
        }
        
        return $decrypted;
    }
}

// 사용 예시
try {
    $crypto = new SecureEncryption('my-secret-key');
    $encrypted = $crypto->encrypt('민감한 데이터');
    $decrypted = $crypto->decrypt($encrypted);
    
    echo "원본: 민감한 데이터\n";
    echo "암호화: " . $encrypted . "\n";
    echo "복호화: " . $decrypted . "\n";
} catch (Exception $e) {
    echo "오류: " . $e->getMessage() . "\n";
}
?>

2. 패스워드 해싱 (단방향 암호화)

<?php
class PasswordManager {
    public function hashPassword($password) {
        // PHP 5.5+ password_hash 사용
        $hash = password_hash($password, PASSWORD_ARGON2ID, [
            'memory_cost' => 65536, // 64MB
            'time_cost' => 4,       // 4번 반복
            'threads' => 3          // 3개 스레드
        ]);
        
        if ($hash === false) {
            throw new Exception('패스워드 해싱 실패');
        }
        
        return $hash;
    }
    
    public function verifyPassword($password, $hash) {
        return password_verify($password, $hash);
    }
    
    public function needsRehash($hash) {
        return password_needs_rehash($hash, PASSWORD_ARGON2ID, [
            'memory_cost' => 65536,
            'time_cost' => 4,
            'threads' => 3
        ]);
    }
    
    // 레거시 MD5/SHA1 해시 마이그레이션
    public function migrateLegacyHash($password, $old_hash, $algorithm = 'md5') {
        $is_valid = false;
        
        switch ($algorithm) {
            case 'md5':
                $is_valid = hash_equals($old_hash, md5($password));
                break;
            case 'sha1':
                $is_valid = hash_equals($old_hash, sha1($password));
                break;
        }
        
        if ($is_valid) {
            return $this->hashPassword($password);
        }
        
        return false;
    }
}

// 사용 예시
$pm = new PasswordManager();

$password = 'user_password123';
$hash = $pm->hashPassword($password);

echo "패스워드: " . $password . "\n";
echo "해시: " . $hash . "\n";
echo "검증: " . ($pm->verifyPassword($password, $hash) ? '성공' : '실패') . "\n";
?>

3. 파일 암호화

<?php
class FileEncryption {
    private $cipher = 'aes-256-cbc';
    private $key;
    
    public function __construct($password) {
        $this->key = hash('sha256', $password, true);
    }
    
    public function encryptFile($inputFile, $outputFile) {
        if (!file_exists($inputFile)) {
            throw new Exception('입력 파일이 존재하지 않습니다');
        }
        
        $iv = random_bytes(16);
        $input = fopen($inputFile, 'rb');
        $output = fopen($outputFile, 'wb');
        
        // IV를 파일 시작 부분에 저장
        fwrite($output, $iv);
        
        $ctx = hash_init('sha256');
        
        while (!feof($input)) {
            $chunk = fread($input, 8192);
            hash_update($ctx, $chunk);
            
            $encrypted = openssl_encrypt(
                $chunk,
                $this->cipher,
                $this->key,
                OPENSSL_RAW_DATA,
                $iv
            );
            
            fwrite($output, $encrypted);
            
            // 다음 블록을 위해 IV 업데이트 (CBC 체이닝)
            $iv = substr($encrypted, -16);
        }
        
        // 파일 해시를 끝에 추가 (무결성 검증용)
        $hash = hash_final($ctx, true);
        fwrite($output, $hash);
        
        fclose($input);
        fclose($output);
    }
    
    public function decryptFile($inputFile, $outputFile) {
        if (!file_exists($inputFile)) {
            throw new Exception('암호화된 파일이 존재하지 않습니다');
        }
        
        $input = fopen($inputFile, 'rb');
        $output = fopen($outputFile, 'wb');
        
        // IV 읽기
        $iv = fread($input, 16);
        
        $ctx = hash_init('sha256');
        $fileSize = filesize($inputFile);
        $processed = 16; // IV 크기
        
        while ($processed < $fileSize - 32) { // 해시 크기(32) 제외
            $chunkSize = min(8192, $fileSize - $processed - 32);
            $chunk = fread($input, $chunkSize);
            
            $decrypted = openssl_decrypt(
                $chunk,
                $this->cipher,
                $this->key,
                OPENSSL_RAW_DATA,
                $iv
            );
            
            if ($decrypted === false) {
                throw new Exception('복호화 실패');
            }
            
            hash_update($ctx, $decrypted);
            fwrite($output, $decrypted);
            
            $iv = substr($chunk, -16);
            $processed += strlen($chunk);
        }
        
        // 해시 검증
        $storedHash = fread($input, 32);
        $calculatedHash = hash_final($ctx, true);
        
        if (!hash_equals($storedHash, $calculatedHash)) {
            throw new Exception('파일 무결성 검증 실패');
        }
        
        fclose($input);
        fclose($output);
    }
}

// 사용 예시
try {
    $fileEnc = new FileEncryption('file-encryption-password');
    
    // 파일 암호화
    $fileEnc->encryptFile('original.txt', 'encrypted.bin');
    echo "파일 암호화 완료\n";
    
    // 파일 복호화
    $fileEnc->decryptFile('encrypted.bin', 'decrypted.txt');
    echo "파일 복호화 완료\n";
    
} catch (Exception $e) {
    echo "오류: " . $e->getMessage() . "\n";
}
?>

4. JWT 토큰 암호화

<?php
class JWTManager {
    private $secret;
    private $algorithm = 'HS256';
    
    public function __construct($secret) {
        $this->secret = $secret;
    }
    
    public function encode($payload, $expiry = 3600) {
        $header = json_encode(['typ' => 'JWT', 'alg' => $this->algorithm]);
        
        $payload['iat'] = time();
        $payload['exp'] = time() + $expiry;
        $payload = json_encode($payload);
        
        $base64Header = $this->base64UrlEncode($header);
        $base64Payload = $this->base64UrlEncode($payload);
        
        $signature = hash_hmac('sha256', $base64Header . '.' . $base64Payload, $this->secret, true);
        $base64Signature = $this->base64UrlEncode($signature);
        
        return $base64Header . '.' . $base64Payload . '.' . $base64Signature;
    }
    
    public function decode($jwt) {
        $parts = explode('.', $jwt);
        
        if (count($parts) !== 3) {
            throw new Exception('잘못된 JWT 형식');
        }
        
        [$header, $payload, $signature] = $parts;
        
        $decodedHeader = json_decode($this->base64UrlDecode($header), true);
        $decodedPayload = json_decode($this->base64UrlDecode($payload), true);
        
        // 서명 검증
        $expectedSignature = hash_hmac('sha256', $header . '.' . $payload, $this->secret, true);
        $providedSignature = $this->base64UrlDecode($signature);
        
        if (!hash_equals($expectedSignature, $providedSignature)) {
            throw new Exception('JWT 서명 검증 실패');
        }
        
        // 만료 시간 확인
        if (isset($decodedPayload['exp']) && $decodedPayload['exp'] < time()) {
            throw new Exception('JWT 토큰이 만료됨');
        }
        
        return $decodedPayload;
    }
    
    private function base64UrlEncode($data) {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }
    
    private function base64UrlDecode($data) {
        return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
    }
}

// 사용 예시
try {
    $jwt = new JWTManager('jwt-secret-key');
    
    $token = $jwt->encode(['user_id' => 123, 'role' => 'admin'], 3600);
    echo "JWT 토큰: " . $token . "\n";
    
    $decoded = $jwt->decode($token);
    echo "디코딩 결과: " . json_encode($decoded) . "\n";
    
} catch (Exception $e) {
    echo "오류: " . $e->getMessage() . "\n";
}
?>

5. 데이터베이스 필드 암호화

<?php
class DatabaseEncryption {
    private $pdo;
    private $encryptionKey;
    
    public function __construct($pdo, $encryptionKey) {
        $this->pdo = $pdo;
        $this->encryptionKey = hash('sha256', $encryptionKey, true);
    }
    
    public function encryptField($data) {
        if (empty($data)) {
            return null;
        }
        
        $iv = random_bytes(16);
        $encrypted = openssl_encrypt($data, 'aes-256-cbc', $this->encryptionKey, 0, $iv);
        
        if ($encrypted === false) {
            throw new Exception('필드 암호화 실패');
        }
        
        return base64_encode($iv . $encrypted);
    }
    
    public function decryptField($encryptedData) {
        if (empty($encryptedData)) {
            return null;
        }
        
        $data = base64_decode($encryptedData);
        $iv = substr($data, 0, 16);
        $encrypted = substr($data, 16);
        
        $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $this->encryptionKey, 0, $iv);
        
        if ($decrypted === false) {
            throw new Exception('필드 복호화 실패');
        }
        
        return $decrypted;
    }
    
    public function insertUser($name, $email, $phone) {
        $stmt = $this->pdo->prepare("
            INSERT INTO users (name, email_encrypted, phone_encrypted) 
            VALUES (?, ?, ?)
        ");
        
        return $stmt->execute([
            $name,
            $this->encryptField($email),
            $this->encryptField($phone)
        ]);
    }
    
    public function getUser($id) {
        $stmt = $this->pdo->prepare("SELECT * FROM users WHERE id = ?");
        $stmt->execute([$id]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if ($user) {
            $user['email'] = $this->decryptField($user['email_encrypted']);
            $user['phone'] = $this->decryptField($user['phone_encrypted']);
            unset($user['email_encrypted'], $user['phone_encrypted']);
        }
        
        return $user;
    }
}

```php:database_encryption.php
// 사용 예시
try {
    $dbEnc = new DatabaseEncryption($pdo, 'database-encryption-key');
    
    // 사용자 데이터 암호화 저장
    $dbEnc->insertUser('홍길동', 'hong@example.com', '010-1234-5678');
    echo "암호화된 사용자 데이터 저장 완료\n";
    
    // 사용자 데이터 복호화 조회
    $user = $dbEnc->getUser(1);
    echo "복호화된 데이터: " . json_encode($user, JSON_UNESCAPED_UNICODE) . "\n";
    
} catch (Exception $e) {
    echo "오류: " . $e->getMessage() . "\n";
}
?>

6. 암호화 키 관리

<?php
class KeyManager {
    private $keyFile;
    
    public function __construct($keyFile = 'encryption.key') {
        $this->keyFile = $keyFile;
    }
    
    public function generateKey() {
        $key = random_bytes(32);
        file_put_contents($this->keyFile, base64_encode($key));
        chmod($this->keyFile, 0600); // 소유자만 읽기/쓰기
        return $key;
    }
    
    public function loadKey() {
        if (!file_exists($this->keyFile)) {
            throw new Exception('암호화 키 파일이 존재하지 않습니다');
        }
        
        $encodedKey = file_get_contents($this->keyFile);
        return base64_decode($encodedKey);
    }
    
    public function rotateKey($oldKey, $newKey = null) {
        if ($newKey === null) {
            $newKey = random_bytes(32);
        }
        
        // 기존 키로 암호화된 데이터를 새 키로 재암호화
        $this->reencryptData($oldKey, $newKey);
        
        // 새 키 저장
        file_put_contents($this->keyFile, base64_encode($newKey));
        
        return $newKey;
    }
    
    private function reencryptData($oldKey, $newKey) {
        // 실제 구현에서는 모든 암호화된 데이터를 재암호화
        echo "키 로테이션: 데이터 재암호화 중...\n";
    }
}
?>

7. 암호화 오류 처리

<?php
class EncryptionErrorHandler {
    public static function validateCipher($cipher) {
        $available = openssl_get_cipher_methods();
        if (!in_array($cipher, $available)) {
            throw new Exception("지원하지 않는 암호화 방식: {$cipher}");
        }
    }
    
    public static function validateKeyLength($key, $requiredLength) {
        if (strlen($key) !== $requiredLength) {
            throw new Exception("키 길이 오류: {$requiredLength}바이트 필요, " . strlen($key) . "바이트 제공됨");
        }
    }
    
    public static function secureCompare($a, $b) {
        if (strlen($a) !== strlen($b)) {
            return false;
        }
        return hash_equals($a, $b);
    }
    
    public static function clearMemory(&$variable) {
        if (is_string($variable)) {
            $variable = str_repeat("\0", strlen($variable));
        }
        $variable = null;
    }
}
?>

핵심 포인트

  • AES-256-GCM 또는 AES-256-CBC 사용으로 강력한 암호화
  • random_bytes() 함수로 안전한 IV 생성
  • password_hash() 함수로 패스워드 해싱 (MD5, SHA1 금지)
  • hash_equals() 함수로 타이밍 공격 방지
  • 키 관리키 로테이션 체계 구축
  • 오류 처리메모리 정리로 보안 강화

암호화는 한 번 잘못 구현하면 되돌리기 어려우므로 검증된 방법을 사용하는 것이 중요합니다.

profile
일용직 개발자. freetercoder@gmail.com

0개의 댓글