[텀프로젝트] 파일 압축&암호화 유틸리티 개발 - 4 (복호화&압축 해제 기능 구현)
안녕하세요. 오늘 구현해볼 코드는 파일 복호화 및 압축 해제 부분입니다.
순서를 압축&암호화와 반대로 하면 되기 때문에, 한번에 묶어서 진행해보겠습니다.
일단 어제 작성한 코드들에 대한 피드백을 조금 생각해봤습니다.
먼저, 암호화된 파일의 구조입니다.
현재 압축 및 암호화하면 파일의 맨 첫부분에 Salt값 + IV + Tag 값이 저장이 되는데 복호화할때 비밀번호 일치 여부를 확인하려면 복호화과정을 진행하고, Tag값의 일치 여부를 확인해야 하기 때문에 속도가 느려질 수 있을 것 같습니다.
제가 만들 프로그램이 높은 보안성,무결성보단 간단하게 개인 노트북에서 돌리는 정도의 레벨이기 때문에 조금 더 가벼운 수준에서의 비밀번호 체크를 구현하면 좋지 않을까 싶습니다.
두번째로는 프로그램의 목적성입니다.
개인 노트북에서 압축은 이미 윈도우자체에서 지원을 해주기 때문에 큰 의미가 없고, 암호화기능도 개인 노트북에서는 의미가 없습니다. 암호화의 의의는 파일을 통신하는 과정에서 snoofing을 방지하는데 의미가 있기에, 파일의 정적 링킹을 통한 배포가 중요하다고 느꼈습니다.
이제 복호화 기능을 구현해보겠습니다. 복호화 함수는 perform_decrypt로 만들겠습니다. 순서는 암호화의 반대로 다음과 같습니다.
- 헤더 파싱 : 암호화된 파일에서 Salt. Iv, Tag를 떼어낸다.
- 키 재생성 : Salt와 사용자가 입력한 비밀번호를 섞어 암호화 키를 다시 생성한다
- 검증 및 복호화 : 비밀번호가 맞는지 확인하고 원본 데이터를 다시 써넣는다
해당 순서는 큰 틀이고 상세한 세부 과정들이 필요합니다.
제일 먼저 데이터 파싱입니다.
// 1. 데이터 파싱 (포인터 연산으로 위치 잡기)
long header_len = SALT_LEN + IV_LEN + TAG_LEN;
const unsigned char* salt = enc_data;
const unsigned char* iv = enc_data + SALT_LEN;
const unsigned char* tag = enc_data + SALT_LEN + IV_LEN;
const unsigned char* ciphertext = enc_data + header_len;
long ciphertext_len = enc_len - header_len;
salt, IV, tag의 위치를 포인터 연산으로 잡아뒀습니다.
그 다음으로 복호화 키 유도입니다.
//2. 키 유도
unsigned char key[KEY_LEN];
unsigned char* key_material = (unsigned char*)malloc(strlen(password) + SALT_LEN);
if (!key_material) return NULL;
// 비밀번호 + 파일에있던Salt
memcpy(key_material, password, strlen(password));
memcpy(key_material + strlen(password), salt, SALT_LEN);
// SHA256 해시
SHA256(key_material, strlen(password) + SALT_LEN, key);
free(key_material);
아까 포인터 위치로 자아둔 salt, iv 그리고 사용자에게 입력받은 password로 복호화 키를 추출해냅니다.
그 뒤 OpenSSL 설정이 필요합니다. 해당 부분 코드에 대한 설명은 스킵하겠습니다. 다음은 복호화 수행입니다.
unsigned char* plaintext = (unsigned char*)malloc(ciphertext_len);
if (!plaintext) {
EVP_CIPHER_CTX_free(ctx);
return NULL;
}
int len = 0;
int plaintext_len = 0;
// 암호문 넣고 돌리기
if (!EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len)) {
free(plaintext);
EVP_CIPHER_CTX_free(ctx);
return NULL;
}
plaintext_len = len;
먼저 복호화를 진행한 이후에 비밀번호 검증이 필요합니다. 이는 비밀번호 검증에 필요한 정보를 헤더에 따로 저장한게 아닌 전체 암호화 후 나오는 Tag값으로 비밀번호 검증을 수행하기 때문인데, 조금 비효율적으로 다가오는 부분이긴 합니다.
// 5.비밀번호 확인
// 여기서 비밀번호가 틀렸거나 파일이 변조되었으면 ret 값이 0 이하가 나옴
int ret = EVP_DecryptFinal_ex(ctx, plaintext + len, &len);
EVP_CIPHER_CTX_free(ctx);
if (ret > 0) {
// 성공: 비밀번호 일치 & 데이터 무결성 확인됨
plaintext_len += len;
*out_len = plaintext_len;
return plaintext;
}
else {
// 실패: 비밀번호 틀림
printf("[Fail] 복호화 실패! (비밀번호 오류 또는 파일 손상)\n");
free(plaintext); // 잘못 복구된 데이터는 즉시 삭제
return NULL;
}
}
이제 해당 함수를 함수 메인 로직인 process_file에 추가하면 됩니다.
다음과 같이 추가했습니다.
// 2. 확장자에 따른 분기 처리
if (ext == ".poir") {
printf(" >> 복호화 & 압축 해제 실행\n");
//복호화 함수 먼저
//비밀번호 입력받아 저장
std::string password;
std::cout << " 비밀번호를 입력하세요: ";
std::cin >> password;
long file_len = 0;
unsigned char* file_data = read_file_content(path, &file_len);
if (file_data != NULL) {
printf("파일 읽기 성공. 크기: %ld bytes\n", file_len);
long decrypted_len = 0;
// 파일에서 읽은 데이터(file_data)를 복호화 -> decrypted_data에 저장
unsigned char* decrypted_data = perform_decrypt(file_data, file_len, password.c_str(), &decrypted_len);
if (decrypted_data != NULL) {
printf("복호화 성공(복호화된 데이터 크기: %ld bytes)\n", decrypted_len);
// 현재 decrypted_data에는 압축된 데이터가 들어감
// TODO: 추후 이곳에 압축 해제로직 구현
이제 압축 해제 함수를 구현해보겠습니다.
압축 해제는 perform_decompress 라는 함수로 구현해보겠습니다.
압축 해제는 크게 어려운 내용이 없습니다.
unsigned char* perform_decompress(const unsigned char* src, long src_len, long out_len) {
// 1.압축 해제될 데이터가 들어갈 넉넉한 메모리 할당
uLongf dest_len = (uLongf)(src_len * 10 + 10240);
unsigned char* dest = (unsigned char*)malloc(dest_len);
// 2. zlib 압축 해제 수행
int res = uncompress(dest, &dest_len, src, (uLong)src_len);
// 3. 결과 확인
if (res != Z_OK) {
printf("zlib 압축 해제 실패. Error code: %d\n", res);
// Z_BUF_ERROR가 나면 버퍼가 작은 것이므로 더 크게 할당해야 함 (여기선 생략)
free(dest);
return NULL;
}
//4. 실제 크기 반환
*out_len = (long)dest_len;
return dest;
}
해당 코드 확인하시고 주석 보시면 될 것 같습니다.
해당 코드도 process_file의 복호화 이후 넣겠습니다.
decompress를 통해 압축을 해제하고, 압축 해제한 데이터를 또 원본 데이터에 작성해야 하기 때문에 저번 압축때 사용했던 save_file 함수를 쓰도록 하겠습니다.
또한 .poir를 제거하는 로직도 필요합니다.
// 압축해제 로직 구현
long original_len = 0;
unsigned char* original_data = perform_decompress(decrypted_data, decrypted_len, &original_len);
if (original_data != NULL) {
printf(" 압축 해제 성공 (원본 크기: %ld bytes)\n", original_len);
//이제 파일 저장을 해야함
//파일명에서 .poir를 제거
fs::path save_path = p;
save_path.replace_extension(""); // 확장자 제거
// 만약 확장자가 아예 없었다면 replace_extension이 그냥 비워버릴 수 있으니 체크
save_file(save_path.string().c_str(), original_data, original_len, path);
free(original_data);
}
else {
printf("압축 해제 실패 (데이터 손상 가능성)\n");
}
이제 코드를 테스트해 보도록 하겠습니다. 저번에 압축&암호화를 수행했었던 파일이 있으니 복호화를 시도해보겠습니다.
틀린 비밀번호를 입력해보겠습니다.

이번엔 제대로된 비밀번호를 입력해주었습니다.


압축해제와 크기 모두 정상적으로 설정이 완료되었습니다.
하지만

안에 글자가 다 깨져있더라고요 ,,? 다른 몇가지 파일들로 테스트를 해보았습니다.
먼저 ELF 파일이랑 PPTX로 시도를 해봤는데 암호화,복호화가 정상적으로 잘 되었습니다.
이미 날려먹은 txt파일을 복구하긴 힘드니, 문제가 될만한 부분을 찾아서 고쳐주었습니다.
암호화, 복호화시 메모리를 할당할 때 너무 타이트하게 malloc을 하면 오버플로우가 발생하면서 덮어질 수도 있다고 해서 해당 할당을 넉넉하게 해주었습니다.
이후에 여러번 테스트를 해보니까 성공적으로 돌아가는 것 같습니다.
그래도 전체적으로 기능이 완성이 된 느낌인데 뿌듯하네요 ..
이제 과제에 해당하는 부분은 완료가 됐으니 과제 보고서만 작성한 뒤, 파일의 링킹이나 배포에 관한 고민을 해봐야 할 것 같습니다.
이상으로 포스팅을 마치겠습니다 읽어주셔서 감사합니다.