암호화/복호화 과정에서 발생하는 오류들은 보안과 직결되는 중요한 문제입니다. 안전하고 올바른 암호화 구현 방법들을 알아보겠습니다.
<?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";
}
?>
<?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";
?>
<?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";
}
?>
<?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";
}
?>
<?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";
}
?>
<?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";
}
}
?>
<?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;
}
}
?>
암호화는 한 번 잘못 구현하면 되돌리기 어려우므로 검증된 방법을 사용하는 것이 중요합니다.