PHP에서 문자열을 암호화해 DB나 쿠키에 넣었다가 다시 꺼내 써야 할 때가 있습니다. 비밀번호처럼 되돌릴 필요가 없는 값은 password_hash 로 해시하면 되지만, 개인 연락처나 API 토큰처럼 나중에 원래 값이 필요한 데이터는 양방향 암호화가 필요하죠. PHP에서는 openssl 확장의 openssl_encrypt / openssl_decrypt 로 처리합니다.
아래는 비밀번호 문자열 하나로 암호화·복호화하는 함수 한 쌍입니다.
function encrypt($plaintext, $password) {
$method = "AES-256-ECB";
$key = hash('sha256', $password, true);
$encrypt_iv = openssl_random_pseudo_bytes(16);
$ciphertext = openssl_encrypt($plaintext, $method, $key, OPENSSL_RAW_DATA, $encrypt_iv);
$hash = hash_hmac('sha256', $ciphertext . $encrypt_iv, $key, true);
return $encrypt_iv . $hash . $ciphertext;
}
function decrypt($ivHashCiphertext, $password) {
$method = "AES-256-ECB";
$decrypt_iv = substr($ivHashCiphertext, 0, 16);
$hash = substr($ivHashCiphertext, 16, 32);
$ciphertext = substr($ivHashCiphertext, 48);
$key = hash('sha256', $password, true);
if (!hash_equals(hash_hmac('sha256', $ciphertext . $decrypt_iv, $key, true), $hash)) return null;
return openssl_decrypt($ciphertext, $method, $key, OPENSSL_RAW_DATA, $decrypt_iv);
}
결과 문자열은 IV + HMAC + 암호문
hash('sha256', $password, true) 는 비밀번호를 32바이트 바이너리로 만들어 줍니다. AES-256 키 길이가 정확히 32바이트라서 어떤 길이의 비밀번호든 키로 쓸 수 있게 맞추는 역할입니다.
encrypt 가 돌려주는 값은 앞에서부터 IV 16바이트, HMAC 32바이트, 그 뒤가 암호문입니다. decrypt 는 이 순서대로 substr 으로 잘라 냅니다. 0~15가 IV, 16부터 32바이트가 HMAC, 48 이후가 암호문이죠. 저장 형식을 바꾸면 양쪽을 함께 고쳐야 합니다.
HMAC은 데이터가 중간에 바뀌지 않았는지 확인하는 서명입니다. 복호화하기 전에 같은 키로 HMAC을 다시 계산해서 저장된 값과 비교하고, 다르면 바로 null 을 돌려줍니다. 비밀번호가 틀렸거나 누군가 데이터를 건드렸다는 뜻입니다. 비교에 == 대신 hash_equals 를 쓴 건 비교 시간 차이로 정보가 새는 것(타이밍 공격)을 막기 위해서입니다.
OPENSSL_RAW_DATA 를 줬기 때문에 결과는 바이너리입니다. 그대로 DB의 VARCHAR 컬럼에 넣거나 URL에 붙이면 깨지니, 저장할 때는 base64_encode(encrypt(...)), 꺼낼 때는 decrypt(base64_decode(...), ...) 처럼 감싸서 쓰세요.
ECB 모드라서 IV가 의미 없다
이 코드에서 짚어 둘 부분이 있습니다. 방식이 AES-256-ECB 로 되어 있는데, ECB는 IV를 쓰지 않는 모드입니다. 그래서 랜덤 IV를 만들어 넘겨도 암호화 결과에 반영되지 않고, PHP 버전에 따라 "IV가 예상보다 길다"는 경고가 납니다. IV가 의미 없으니 같은 평문은 같은 비밀번호로 언제나 같은 암호문이 되고, 긴 데이터에서는 반복되는 블록 패턴이 드러납니다.
IV를 제대로 쓰려면 두 함수의 $method 를 CBC로 바꾸면 됩니다. IV를 16바이트로 만들어 함께 저장하는 구조가 이미 갖춰져 있어서 나머지는 고칠 게 없습니다.
$method = "AES-256-CBC";
다만 이미 ECB로 암호화해 저장해 둔 데이터가 있다면 모드를 바꾸는 순간 복호화가 안 됩니다. 기존 데이터를 ECB로 한 번 풀어서 CBC로 다시 암호화하는 이관 작업을 먼저 해야 합니다. 그리고 $password 를 소스 코드에 직접 적어 두기보다는 설정 파일이나 환경 변수로 빼 두는 게 좋습니다. 소스가 노출되면 암호화한 의미가 없어지니까요.