해시 함수와 데이터 무결성 검증
해시 함수는 현대 보안의 핵심 기술입니다. 패스워드 보호부터 블록체인까지, 해시를 통한 데이터 무결성 검증과 보안 강화 방법을 체계적으로 알아보겠습니다.
1. 해시 함수의 기본 원리
해시 함수란?
정의와 특성
- 임의 크기의 데이터를 고정 크기의 값으로 변환
- 같은 입력에 대해 항상 같은 출력 생성
- 출력으로부터 원본 데이터 역추산 불가능
- 작은 입력 변화가 완전히 다른 출력 생성
핵심 보안 특성
1. 결정론적 (Deterministic):
- 동일 입력 → 동일 출력
2. 고정 출력 크기:
- MD5: 128비트 (32자리 16진수)
- SHA-1: 160비트 (40자리 16진수)
- SHA-256: 256비트 (64자리 16진수)
3. 일방향성 (Pre-image Resistance):
- 해시값으로부터 원본 복구 불가능
4. 약한 충돌 저항성 (Second Pre-image Resistance):
- 주어진 입력과 같은 해시값을 갖는 다른 입력 찾기 어려움
5. 강한 충돌 저항성 (Collision Resistance):
- 같은 해시값을 갖는 임의의 두 입력 찾기 어려움
주요 해시 알고리즘 비교
MD5 (Message Digest 5)
// 보안상 취약하지만 여전히 사용되는 경우
const crypto = require('crypto');
function md5Hash(data) {
return crypto.createHash('md5').update(data).digest('hex');
}
console.log(md5Hash('Hello World')); // b10a8db164e0754105b7a99be72e3fe5
// 취약점: 충돌 공격 가능
const collision1 = "d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70";
const collision2 = "d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70";
// 두 다른 입력이 같은 MD5 해시 생성
console.log(md5Hash(collision1) === md5Hash(collision2)); // true
SHA-1 (Secure Hash Algorithm 1)
function sha1Hash(data) {
return crypto.createHash('sha1').update(data).digest('hex');
}
console.log(sha1Hash('Hello World')); // 0a4d55a8d778e5022fab701977c5d840bbc486d0
// 2017년 Google에서 충돌 공격 성공 (SHAttered)
// 현재는 보안상 권장되지 않음
SHA-256 (현재 표준)
function sha256Hash(data) {
return crypto.createHash('sha256').update(data).digest('hex');
}
console.log(sha256Hash('Hello World'));
// a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e
// 현재까지 안전한 것으로 알려진 알고리즘
// 비트코인 등에서 사용
SHA-3 (최신 표준)
function sha3Hash(data) {
return crypto.createHash('sha3-256').update(data).digest('hex');
}
console.log(sha3Hash('Hello World'));
// e167f68d6563d75bb25f3aa49c29ef612d41352dc00606de7cbd630bb2665f51
2. 패스워드 해싱과 보안
패스워드 해싱의 문제점과 해결책
단순 해싱의 위험성
// 잘못된 방법: 단순 SHA-256 해싱
const password = 'mypassword123';
const simpleHash = sha256Hash(password);
console.log(simpleHash);
// 같은 비밀번호는 항상 같은 해시 생성 → 레인보우 테이블 공격 가능
// 일반적인 비밀번호들의 해시값이 미리 계산되어 공격에 활용됨
const commonPasswords = {
'password': 'ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f',
'123456': 'ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f',
'qwerty': '65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5'
};
솔트(Salt)를 활용한 보안 강화
const crypto = require('crypto');
class SecurePasswordHasher {
// 랜덤 솔트 생성
static generateSalt(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
// 솔트와 함께 해싱
static hashPassword(password, salt = null) {
if (!salt) {
salt = this.generateSalt();
}
const hash = crypto.createHash('sha256')
.update(password + salt)
.digest('hex');
return {
hash: hash,
salt: salt,
algorithm: 'sha256'
};
}
// 비밀번호 검증
static verifyPassword(password, storedHash, storedSalt) {
const { hash } = this.hashPassword(password, storedSalt);
return hash === storedHash;
}
}
// 사용 예시
const password = 'mypassword123';
const result = SecurePasswordHasher.hashPassword(password);
console.log(result);
// { hash: '...', salt: '...', algorithm: 'sha256' }
// 같은 비밀번호라도 다른 솔트로 인해 다른 해시 생성
const result2 = SecurePasswordHasher.hashPassword(password);
console.log(result.hash !== result2.hash); // true
고급 패스워드 해싱 (Key Stretching)
PBKDF2 (Password-Based Key Derivation Function 2)
const crypto = require('crypto');
class PBKDF2Hasher {
static hashPassword(password, iterations = 100000, keyLength = 64) {
const salt = crypto.randomBytes(32);
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, iterations, keyLength, 'sha256', (err, derivedKey) => {
if (err) reject(err);
resolve({
hash: derivedKey.toString('hex'),
salt: salt.toString('hex'),
iterations: iterations,
algorithm: 'pbkdf2'
});
});
});
}
static async verifyPassword(password, storedData) {
const { salt, iterations, hash } = storedData;
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, Buffer.from(salt, 'hex'), iterations, 64, 'sha256', (err, derivedKey) => {
if (err) reject(err);
const newHash = derivedKey.toString('hex');
resolve(newHash === hash);
});
});
}
}
// 사용 예시
async function demonstratePBKDF2() {
const password = 'mypassword123';
const hashedData = await PBKDF2Hasher.hashPassword(password);
console.log('Hashed:', hashedData);
const isValid = await PBKDF2Hasher.verifyPassword(password, hashedData);
console.log('Valid:', isValid); // true
const isInvalid = await PBKDF2Hasher.verifyPassword('wrongpassword', hashedData);
console.log('Invalid:', isInvalid); // false
}
bcrypt (업계 표준)
const bcrypt = require('bcrypt');
class BcryptHasher {
static async hashPassword(password, saltRounds = 12) {
try {
const hash = await bcrypt.hash(password, saltRounds);
return {
hash: hash,
algorithm: 'bcrypt',
cost: saltRounds
};
} catch (error) {
throw new Error('해싱 실패: ' + error.message);
}
}
static async verifyPassword(password, hash) {
try {
return await bcrypt.compare(password, hash);
} catch (error) {
throw new Error('검증 실패: ' + error.message);
}
}
// 해시 비용 분석
static analyzeCost(hash) {
const costMatch = hash.match(/^\$2[aby]\$(\d+)\$/);
if (costMatch) {
const cost = parseInt(costMatch[1]);
const operations = Math.pow(2, cost);
return {
cost: cost,
operations: operations,
estimatedTime: operations / 1000000 // 대략적인 초 단위 시간
};
}
return null;
}
}
// 비용별 성능 테스트
async function testBcryptCosts() {
const password = 'testpassword123';
const costs = [10, 12, 14, 16];
for (const cost of costs) {
const startTime = Date.now();
const result = await BcryptHasher.hashPassword(password, cost);
const endTime = Date.now();
console.log(`Cost ${cost}: ${endTime - startTime}ms`);
console.log(`Hash: ${result.hash}`);
const analysis = BcryptHasher.analyzeCost(result.hash);
console.log(`Operations: ${analysis.operations.toLocaleString()}`);
console.log('---');
}
}
Argon2 (최신 표준)
const argon2 = require('argon2');
class Argon2Hasher {
static async hashPassword(password, options = {}) {
const defaultOptions = {
type: argon2.argon2id, // 하이브리드 방식
memoryCost: 2 ** 16, // 64MB 메모리 사용
timeCost: 3, // 3번 반복
parallelism: 1, // 1개 스레드
};
const finalOptions = { ...defaultOptions, ...options };
try {
const hash = await argon2.hash(password, finalOptions);
return {
hash: hash,
algorithm: 'argon2id',
options: finalOptions
};
} catch (error) {
throw new Error('Argon2 해싱 실패: ' + error.message);
}
}
static async verifyPassword(password, hash) {
try {
return await argon2.verify(hash, password);
} catch (error) {
throw new Error('Argon2 검증 실패: ' + error.message);
}
}
// 맞춤형 파라미터 계산
static calculateOptimalParams(targetTime = 500) { // 500ms 목표
// 시스템 성능에 따른 최적 파라미터 계산
return {
memoryCost: 2 ** 16, // 시스템 메모리에 따라 조정
timeCost: 3, // 목표 시간에 따라 조정
parallelism: 1 // CPU 코어 수에 따라 조정
};
}
}
3. 파일 무결성 검증
파일 체크섬 생성과 검증
대용량 파일의 효율적 해싱
const crypto = require('crypto');
const fs = require('fs');
class FileHasher {
static async hashFile(filePath, algorithm = 'sha256') {
return new Promise((resolve, reject) => {
const hash = crypto.createHash(algorithm);
const stream = fs.createReadStream(filePath);
stream.on('data', (data) => {
hash.update(data);
});
stream.on('end', () => {
const fileHash = hash.digest('hex');
resolve(fileHash);
});
stream.on('error', (error) => {
reject(error);
});
});
}
// 여러 알고리즘으로 동시 해싱
static async hashFileMultiple(filePath, algorithms = ['md5', 'sha1', 'sha256']) {
return new Promise((resolve, reject) => {
const hashes = {};
algorithms.forEach(alg => {
hashes[alg] = crypto.createHash(alg);
});
const stream = fs.createReadStream(filePath);
stream.on('data', (data) => {
algorithms.forEach(alg => {
hashes[alg].update(data);
});
});
stream.on('end', () => {
const results = {};
algorithms.forEach(alg => {
results[alg] = hashes[alg].digest('hex');
});
resolve(results);
});
stream.on('error', reject);
});
}
// 파일 무결성 검증
static async verifyFileIntegrity(filePath, expectedHash, algorithm = 'sha256') {
try {
const actualHash = await this.hashFile(filePath, algorithm);
return {
valid: actualHash === expectedHash,
expected: expectedHash,
actual: actualHash,
algorithm: algorithm
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
// 디렉토리 전체 무결성 검증
static async hashDirectory(dirPath, algorithm = 'sha256') {
const path = require('path');
const files = await this.getAllFiles(dirPath);
const fileHashes = {};
for (const file of files) {
const relativePath = path.relative(dirPath, file);
fileHashes[relativePath] = await this.hashFile(file, algorithm);
}
// 파일 목록과 해시를 포함한 매니페스트 생성
const manifest = JSON.stringify(fileHashes, null, 2);
const manifestHash = crypto.createHash(algorithm).update(manifest).digest('hex');
return {
manifest: fileHashes,
manifestHash: manifestHash,
fileCount: files.length,
algorithm: algorithm
};
}
static async getAllFiles(dir) {
const fs = require('fs').promises;
const path = require('path');
const files = [];
const items = await fs.readdir(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
const subFiles = await this.getAllFiles(fullPath);
files.push(...subFiles);
} else {
files.push(fullPath);
}
}
return files;
}
}
// 사용 예시
async function demonstrateFileHashing() {
try {
// 단일 파일 해싱
const singleHash = await FileHasher.hashFile('./important-file.pdf');
console.log('SHA-256:', singleHash);
// 다중 알고리즘 해싱
const multipleHashes = await FileHasher.hashFileMultiple('./important-file.pdf',
['md5', 'sha1', 'sha256', 'sha512']);
console.log('Multiple hashes:', multipleHashes);
// 무결성 검증
const verification = await FileHasher.verifyFileIntegrity(
'./important-file.pdf',
singleHash
);
console.log('Verification:', verification);
// 디렉토리 해싱
const dirHash = await FileHasher.hashDirectory('./documents');
console.log('Directory manifest hash:', dirHash.manifestHash);
} catch (error) {
console.error('Error:', error);
}
}
블록체인과 머클 트리
머클 트리 구현
class MerkleTree {
constructor(data) {
this.leaves = data.map(item => this.hash(item));
this.tree = this.buildTree(this.leaves);
this.root = this.tree[this.tree.length - 1][0];
}
hash(data) {
return crypto.createHash('sha256').update(data.toString()).digest('hex');
}
buildTree(leaves) {
if (leaves.length === 0) return [];
const tree = [leaves];
let currentLevel = leaves;
while (currentLevel.length > 1) {
const nextLevel = [];
for (let i = 0; i < currentLevel.length; i += 2) {
const left = currentLevel[i];
const right = currentLevel[i + 1] || left; // 홀수 개일 경우 마지막 노드 복제
const parent = this.hash(left + right);
nextLevel.push(parent);
}
tree.push(nextLevel);
currentLevel = nextLevel;
}
return tree;
}
// 특정 데이터의 존재 증명 생성
generateProof(index) {
const proof = [];
let currentIndex = index;
for (let level = 0; level < this.tree.length - 1; level++) {
const isRightNode = currentIndex % 2;
const siblingIndex = isRightNode ? currentIndex - 1 : currentIndex + 1;
if (siblingIndex < this.tree[level].length) {
proof.push({
hash: this.tree[level][siblingIndex],
position: isRightNode ? 'left' : 'right'
});
}
currentIndex = Math.floor(currentIndex / 2);
}
return proof;
}
// 증명 검증
verifyProof(data, proof, root) {
let hash = this.hash(data);
for (const step of proof) {
if (step.position === 'left') {
hash = this.hash(step.hash + hash);
} else {
hash = this.hash(hash + step.hash);
}
}
return hash === root;
}
// 트리 시각화
printTree() {
this.tree.slice().reverse().forEach((level, index) => {
const depth = this.tree.length - 1 - index;
const indent = ' '.repeat(depth);
console.log(`Level ${depth}:`);
level.forEach(hash => {
console.log(`${indent}${hash.substring(0, 8)}...`);
});
});
}
}
// 사용 예시
const documents = ['contract1.pdf', 'contract2.pdf', 'receipt1.jpg', 'invoice1.pdf'];
const merkleTree = new MerkleTree(documents);
console.log('Merkle Root:', merkleTree.root);
// 첫 번째 문서의 존재 증명
const proof = merkleTree.generateProof(0);
console.log('Proof for document 0:', proof);
// 증명 검증
const isValid = merkleTree.verifyProof(documents[0], proof, merkleTree.root);
console.log('Proof valid:', isValid);