[PHP]sha1関数を完全解説!SHA-1ハッシュ値を生成する方法

PHP

こんにちは!今回は、PHPの標準関数であるsha1()について詳しく解説していきます。文字列から160ビット(40文字の16進数)のハッシュ値を生成できる関数です!

sha1関数とは?

sha1()関数は、SHA-1アルゴリズムを使用して文字列のハッシュ値を計算する関数です。

“Secure Hash Algorithm 1″の略で、任意の長さの文字列から固定長(40文字)のハッシュ値を生成します。ファイルの整合性確認、データの識別などに使用されます!

基本的な構文

sha1(string $string, bool $binary = false): string
  • $string: ハッシュ化する文字列
  • $binary: trueで生のバイナリ出力(20バイト)、falseで16進数文字列(40文字)
  • 戻り値: SHA-1ハッシュ値

基本的な使用例

シンプルなハッシュ生成

// 基本的な使用
$text = "Hello World";
$hash = sha1($text);
echo $hash . "\n";
// 出力: 0a4d55a8d778e5022fab701977c5d840bbc486d0(40文字)

// 空文字列
$hash = sha1("");
echo $hash . "\n";
// 出力: da39a3ee5e6b4b0d3255bfef95601890afd80709

// 数値
$hash = sha1("12345");
echo $hash . "\n";
// 出力: 8cb2237d0679ca88db6464eac60da96345513964

バイナリ出力

// 16進数文字列(デフォルト)
$hash = sha1("Hello", false);
echo $hash . "\n";
echo "長さ: " . strlen($hash) . "\n";
// 長さ: 40

// バイナリ出力
$hash = sha1("Hello", true);
echo bin2hex($hash) . "\n";
echo "長さ: " . strlen($hash) . "\n";
// 長さ: 20

大文字小文字の区別

// 大文字小文字は区別される
$hash1 = sha1("Hello");
$hash2 = sha1("hello");

echo $hash1 . "\n";
echo $hash2 . "\n";

if ($hash1 === $hash2) {
    echo "同じハッシュ\n";
} else {
    echo "異なるハッシュ\n";
}
// 出力: 異なるハッシュ

ファイルのハッシュ

// sha1_file()を使用
// $fileHash = sha1_file('file.txt');

// 手動で読み込む場合
$content = file_get_contents('file.txt');
$hash = sha1($content);
echo $hash . "\n";

実践的な使用例

例1: データ整合性チェック

class IntegrityChecker {
    /**
     * データのチェックサムを生成
     */
    public static function generateChecksum($data) {
        if (is_array($data)) {
            $data = json_encode($data);
        }
        
        return sha1($data);
    }
    
    /**
     * データの整合性を検証
     */
    public static function verify($data, $expectedChecksum) {
        $actualChecksum = self::generateChecksum($data);
        
        return [
            'valid' => $actualChecksum === $expectedChecksum,
            'expected' => $expectedChecksum,
            'actual' => $actualChecksum
        ];
    }
    
    /**
     * 複数データの整合性を一括チェック
     */
    public static function verifyMultiple($items) {
        $results = [];
        
        foreach ($items as $item) {
            $result = self::verify($item['data'], $item['checksum']);
            $result['id'] = $item['id'] ?? 'unknown';
            $results[] = $result;
        }
        
        return $results;
    }
    
    /**
     * データと一緒にチェックサムを保存
     */
    public static function pack($data) {
        $checksum = self::generateChecksum($data);
        
        return [
            'data' => $data,
            'checksum' => $checksum,
            'timestamp' => time()
        ];
    }
    
    /**
     * パックされたデータを検証して取り出す
     */
    public static function unpack($package) {
        if (!isset($package['data']) || !isset($package['checksum'])) {
            return ['valid' => false, 'error' => 'Invalid package format'];
        }
        
        $verification = self::verify($package['data'], $package['checksum']);
        
        if ($verification['valid']) {
            return [
                'valid' => true,
                'data' => $package['data'],
                'timestamp' => $package['timestamp'] ?? null
            ];
        }
        
        return [
            'valid' => false,
            'error' => 'Checksum mismatch'
        ];
    }
}

// 使用例
echo "=== チェックサム生成 ===\n";
$data = ['name' => 'John', 'age' => 30];
$checksum = IntegrityChecker::generateChecksum($data);
echo "チェックサム: {$checksum}\n";

echo "\n=== 整合性検証 ===\n";
$result = IntegrityChecker::verify($data, $checksum);
echo "検証結果: " . ($result['valid'] ? 'OK' : 'NG') . "\n";

// データ改ざん
$tamperedData = ['name' => 'Jane', 'age' => 30];
$result = IntegrityChecker::verify($tamperedData, $checksum);
echo "改ざんデータ: " . ($result['valid'] ? 'OK' : 'NG') . "\n";

echo "\n=== パック/アンパック ===\n";
$package = IntegrityChecker::pack($data);
print_r($package);

$unpacked = IntegrityChecker::unpack($package);
echo "アンパック: " . ($unpacked['valid'] ? 'OK' : 'NG') . "\n";

例2: キャッシュシステム

class CacheManager {
    private $cacheDir;
    
    public function __construct($cacheDir = '/tmp/cache') {
        $this->cacheDir = $cacheDir;
        
        if (!is_dir($cacheDir)) {
            mkdir($cacheDir, 0755, true);
        }
    }
    
    /**
     * キャッシュキーを生成
     */
    private function getCacheKey($identifier) {
        return sha1($identifier);
    }
    
    /**
     * キャッシュファイルパスを取得
     */
    private function getCacheFilePath($key) {
        // ディレクトリ分散(最初の2文字でサブディレクトリ)
        $subdir = substr($key, 0, 2);
        $dir = $this->cacheDir . '/' . $subdir;
        
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        
        return $dir . '/' . $key . '.cache';
    }
    
    /**
     * キャッシュに保存
     */
    public function set($identifier, $data, $ttl = 3600) {
        $key = $this->getCacheKey($identifier);
        $filepath = $this->getCacheFilePath($key);
        
        $cacheData = [
            'data' => $data,
            'expires' => time() + $ttl,
            'checksum' => sha1(serialize($data))
        ];
        
        file_put_contents($filepath, serialize($cacheData));
        
        return $key;
    }
    
    /**
     * キャッシュから取得
     */
    public function get($identifier) {
        $key = $this->getCacheKey($identifier);
        $filepath = $this->getCacheFilePath($key);
        
        if (!file_exists($filepath)) {
            return null;
        }
        
        $cacheData = unserialize(file_get_contents($filepath));
        
        // 有効期限チェック
        if ($cacheData['expires'] < time()) {
            unlink($filepath);
            return null;
        }
        
        // 整合性チェック
        $actualChecksum = sha1(serialize($cacheData['data']));
        if ($actualChecksum !== $cacheData['checksum']) {
            unlink($filepath);
            return null;
        }
        
        return $cacheData['data'];
    }
    
    /**
     * キャッシュを削除
     */
    public function delete($identifier) {
        $key = $this->getCacheKey($identifier);
        $filepath = $this->getCacheFilePath($key);
        
        if (file_exists($filepath)) {
            return unlink($filepath);
        }
        
        return false;
    }
    
    /**
     * すべてのキャッシュをクリア
     */
    public function clear() {
        $count = 0;
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->cacheDir),
            RecursiveIteratorIterator::CHILD_FIRST
        );
        
        foreach ($iterator as $file) {
            if ($file->isFile() && $file->getExtension() === 'cache') {
                unlink($file->getPathname());
                $count++;
            }
        }
        
        return $count;
    }
}

// 使用例
echo "=== キャッシュシステム ===\n";
$cache = new CacheManager();

// データをキャッシュ
$data = ['user_id' => 123, 'name' => 'John', 'email' => 'john@example.com'];
$key = $cache->set('user:123', $data, 300);
echo "キャッシュキー: {$key}\n";

// キャッシュから取得
$cached = $cache->get('user:123');
if ($cached !== null) {
    echo "キャッシュヒット:\n";
    print_r($cached);
} else {
    echo "キャッシュミス\n";
}

// 複雑なキーでもOK
$complexKey = json_encode(['action' => 'search', 'query' => 'php', 'page' => 1]);
$cache->set($complexKey, ['results' => [1, 2, 3]], 600);

例3: 重複検出システム

class DuplicateDetector {
    private $hashes = [];
    
    /**
     * コンテンツを追加
     */
    public function add($id, $content) {
        $hash = sha1($content);
        
        if (!isset($this->hashes[$hash])) {
            $this->hashes[$hash] = [];
        }
        
        $this->hashes[$hash][] = $id;
        
        return $hash;
    }
    
    /**
     * 重複をチェック
     */
    public function check($content) {
        $hash = sha1($content);
        
        if (isset($this->hashes[$hash])) {
            return [
                'is_duplicate' => true,
                'hash' => $hash,
                'original_ids' => $this->hashes[$hash]
            ];
        }
        
        return [
            'is_duplicate' => false,
            'hash' => $hash
        ];
    }
    
    /**
     * 重複グループを取得
     */
    public function getDuplicates() {
        return array_filter($this->hashes, function($ids) {
            return count($ids) > 1;
        });
    }
    
    /**
     * 統計情報を取得
     */
    public function getStatistics() {
        $duplicates = $this->getDuplicates();
        
        return [
            'total_hashes' => count($this->hashes),
            'unique_content' => count($this->hashes),
            'duplicate_groups' => count($duplicates),
            'total_duplicates' => array_sum(array_map('count', $duplicates)) - count($duplicates)
        ];
    }
}

// 使用例
echo "=== 重複検出 ===\n";
$detector = new DuplicateDetector();

// コンテンツを追加
$detector->add('doc1', 'This is the original content');
$detector->add('doc2', 'This is different content');
$detector->add('doc3', 'This is the original content');  // 重複
$detector->add('doc4', 'Another unique content');
$detector->add('doc5', 'This is the original content');  // 重複

// 重複チェック
$result = $detector->check('This is the original content');
if ($result['is_duplicate']) {
    echo "重複が見つかりました:\n";
    echo "  元のID: " . implode(', ', $result['original_ids']) . "\n";
}

echo "\n=== 重複グループ ===\n";
$duplicates = $detector->getDuplicates();
foreach ($duplicates as $hash => $ids) {
    echo "ハッシュ: {$hash}\n";
    echo "  ID: " . implode(', ', $ids) . "\n";
}

echo "\n=== 統計 ===\n";
$stats = $detector->getStatistics();
print_r($stats);

例4: ファイル管理システム

class FileManager {
    /**
     * ファイルのハッシュを計算
     */
    public static function getFileHash($filepath) {
        if (!file_exists($filepath)) {
            return null;
        }
        
        return sha1_file($filepath);
    }
    
    /**
     * ファイルを比較
     */
    public static function compareFiles($file1, $file2) {
        $hash1 = self::getFileHash($file1);
        $hash2 = self::getFileHash($file2);
        
        if ($hash1 === null || $hash2 === null) {
            return null;
        }
        
        return [
            'identical' => $hash1 === $hash2,
            'hash1' => $hash1,
            'hash2' => $hash2
        ];
    }
    
    /**
     * ディレクトリ内の重複ファイルを検出
     */
    public static function findDuplicates($directory) {
        $hashes = [];
        $files = glob($directory . '/*');
        
        foreach ($files as $file) {
            if (is_file($file)) {
                $hash = self::getFileHash($file);
                
                if (!isset($hashes[$hash])) {
                    $hashes[$hash] = [];
                }
                
                $hashes[$hash][] = $file;
            }
        }
        
        // 2つ以上のファイルがあるグループのみ返す
        return array_filter($hashes, function($files) {
            return count($files) > 1;
        });
    }
    
    /**
     * ファイルのメタデータを生成
     */
    public static function generateMetadata($filepath) {
        if (!file_exists($filepath)) {
            return null;
        }
        
        return [
            'path' => $filepath,
            'filename' => basename($filepath),
            'size' => filesize($filepath),
            'hash' => self::getFileHash($filepath),
            'modified' => filemtime($filepath),
            'created' => filectime($filepath)
        ];
    }
    
    /**
     * ファイルインデックスを作成
     */
    public static function createIndex($directory) {
        $index = [];
        $files = glob($directory . '/*');
        
        foreach ($files as $file) {
            if (is_file($file)) {
                $metadata = self::generateMetadata($file);
                $index[$metadata['hash']] = $metadata;
            }
        }
        
        return $index;
    }
}

// 使用例
echo "=== ファイル管理 ===\n";

// テストファイルを作成
file_put_contents('/tmp/test1.txt', 'Hello World');
file_put_contents('/tmp/test2.txt', 'Hello World');  // 同じ内容
file_put_contents('/tmp/test3.txt', 'Different content');

// ファイル比較
$comparison = FileManager::compareFiles('/tmp/test1.txt', '/tmp/test2.txt');
echo "test1.txt vs test2.txt: " . ($comparison['identical'] ? '同一' : '異なる') . "\n";

// メタデータ生成
$metadata = FileManager::generateMetadata('/tmp/test1.txt');
print_r($metadata);

例5: ETags生成

class ETagGenerator {
    /**
     * コンテンツからETagを生成
     */
    public static function generate($content) {
        return sha1($content);
    }
    
    /**
     * ファイルからETagを生成
     */
    public static function fromFile($filepath) {
        if (!file_exists($filepath)) {
            return null;
        }
        
        // ファイル内容とタイムスタンプを組み合わせる
        $content = file_get_contents($filepath);
        $mtime = filemtime($filepath);
        
        return sha1($content . $mtime);
    }
    
    /**
     * 弱いETagを生成(W/付き)
     */
    public static function generateWeak($content) {
        return 'W/"' . sha1($content) . '"';
    }
    
    /**
     * 強いETagを生成
     */
    public static function generateStrong($content) {
        return '"' . sha1($content) . '"';
    }
    
    /**
     * ETagを検証
     */
    public static function validate($content, $etag) {
        $etag = trim($etag, '"');
        $etag = preg_replace('/^W\//', '', $etag);
        
        $currentEtag = self::generate($content);
        
        return $etag === $currentEtag;
    }
    
    /**
     * HTTPレスポンスヘッダーを設定
     */
    public static function setHeader($content, $weak = false) {
        $etag = $weak ? 
            self::generateWeak($content) : 
            self::generateStrong($content);
        
        header("ETag: {$etag}");
        
        // If-None-Matchヘッダーをチェック
        if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
            $clientEtag = $_SERVER['HTTP_IF_NONE_MATCH'];
            
            if (self::validate($content, $clientEtag)) {
                header('HTTP/1.1 304 Not Modified');
                exit;
            }
        }
    }
}

// 使用例
echo "=== ETag生成 ===\n";

$content = "This is my content";
$etag = ETagGenerator::generate($content);
echo "ETag: {$etag}\n";

$strongEtag = ETagGenerator::generateStrong($content);
echo "強いETag: {$strongEtag}\n";

$weakEtag = ETagGenerator::generateWeak($content);
echo "弱いETag: {$weakEtag}\n";

// 検証
$isValid = ETagGenerator::validate($content, $strongEtag);
echo "検証: " . ($isValid ? 'OK' : 'NG') . "\n";

例6: セッションID生成

class SessionManager {
    /**
     * セッションIDを生成
     */
    public static function generateId($userId, $userAgent, $ipAddress) {
        $timestamp = microtime(true);
        $random = random_bytes(16);
        
        $data = $userId . $userAgent . $ipAddress . $timestamp . bin2hex($random);
        
        return sha1($data);
    }
    
    /**
     * セキュアなトークンを生成
     */
    public static function generateToken($length = 40) {
        $randomBytes = random_bytes(20);
        $hash = sha1($randomBytes . microtime(true));
        
        return substr($hash, 0, $length);
    }
    
    /**
     * ワンタイムトークンを生成
     */
    public static function generateOneTimeToken($userId, $action) {
        $timestamp = time();
        $random = random_bytes(16);
        
        $data = $userId . $action . $timestamp . bin2hex($random);
        
        return [
            'token' => sha1($data),
            'expires' => $timestamp + 300  // 5分有効
        ];
    }
    
    /**
     * CSRFトークンを生成
     */
    public static function generateCsrfToken($sessionId) {
        $secret = 'your-secret-key';  // 実際は設定ファイルから読み込む
        
        return sha1($sessionId . $secret . time());
    }
}

// 使用例
echo "=== セッション管理 ===\n";

$userId = 123;
$userAgent = 'Mozilla/5.0...';
$ipAddress = '192.168.1.1';

$sessionId = SessionManager::generateId($userId, $userAgent, $ipAddress);
echo "セッションID: {$sessionId}\n";

$token = SessionManager::generateToken();
echo "トークン: {$token}\n";

$oneTimeToken = SessionManager::generateOneTimeToken($userId, 'reset-password');
echo "ワンタイムトークン: {$oneTimeToken['token']}\n";
echo "有効期限: " . date('Y-m-d H:i:s', $oneTimeToken['expires']) . "\n";

$csrfToken = SessionManager::generateCsrfToken($sessionId);
echo "CSRFトークン: {$csrfToken}\n";

例7: データ署名

class DataSigner {
    private $secret;
    
    public function __construct($secret) {
        $this->secret = $secret;
    }
    
    /**
     * データに署名
     */
    public function sign($data) {
        $serialized = is_string($data) ? $data : json_encode($data);
        $signature = sha1($serialized . $this->secret);
        
        return [
            'data' => $data,
            'signature' => $signature
        ];
    }
    
    /**
     * 署名を検証
     */
    public function verify($signedData) {
        if (!isset($signedData['data']) || !isset($signedData['signature'])) {
            return false;
        }
        
        $serialized = is_string($signedData['data']) ? 
            $signedData['data'] : 
            json_encode($signedData['data']);
        
        $expectedSignature = sha1($serialized . $this->secret);
        
        return hash_equals($expectedSignature, $signedData['signature']);
    }
    
    /**
     * 署名付きURLを生成
     */
    public function signUrl($url, $expiresIn = 3600) {
        $expires = time() + $expiresIn;
        $signature = sha1($url . $expires . $this->secret);
        
        $separator = strpos($url, '?') !== false ? '&' : '?';
        
        return $url . $separator . "expires={$expires}&signature={$signature}";
    }
    
    /**
     * 署名付きURLを検証
     */
    public function verifyUrl($signedUrl) {
        $parts = parse_url($signedUrl);
        
        if (!isset($parts['query'])) {
            return false;
        }
        
        parse_str($parts['query'], $params);
        
        if (!isset($params['expires']) || !isset($params['signature'])) {
            return false;
        }
        
        // 有効期限チェック
        if (time() > $params['expires']) {
            return false;
        }
        
        // 署名を再構築
        unset($params['signature']);
        $baseUrl = $parts['scheme'] . '://' . $parts['host'] . 
                   ($parts['path'] ?? '') . '?' . http_build_query($params);
        
        $expectedSignature = sha1($baseUrl . $params['expires'] . $this->secret);
        
        return hash_equals($expectedSignature, $_GET['signature'] ?? '');
    }
}

// 使用例
echo "=== データ署名 ===\n";

$signer = new DataSigner('my-secret-key');

// データに署名
$data = ['user_id' => 123, 'action' => 'delete'];
$signed = $signer->sign($data);
print_r($signed);

// 署名を検証
$isValid = $signer->verify($signed);
echo "\n署名検証: " . ($isValid ? 'OK' : 'NG') . "\n";

// データを改ざん
$tampered = $signed;
$tampered['data']['user_id'] = 999;
$isValid = $signer->verify($tampered);
echo "改ざんデータ: " . ($isValid ? 'OK' : 'NG') . "\n";

// 署名付きURL
echo "\n=== 署名付きURL ===\n";
$url = 'https://example.com/download/file.pdf';
$signedUrl = $signer->signUrl($url, 3600);
echo "署名付きURL:\n{$signedUrl}\n";

md5()との比較

$text = "Hello World";

// MD5(128ビット、32文字)
$md5 = md5($text);
echo "MD5: {$md5}\n";
echo "長さ: " . strlen($md5) . "\n";

// SHA-1(160ビット、40文字)
$sha1 = sha1($text);
echo "SHA-1: {$sha1}\n";
echo "長さ: " . strlen($sha1) . "\n";

// SHA-256(より安全)
$sha256 = hash('sha256', $text);
echo "SHA-256: {$sha256}\n";
echo "長さ: " . strlen($sha256) . "\n";

セキュリティ上の注意

// ⚠️ SHA-1はパスワードハッシュには使用しない
// 悪い例
$badPasswordHash = sha1('password123');

// 良い例: password_hash()を使用
$goodPasswordHash = password_hash('password123', PASSWORD_DEFAULT);

// ⚠️ SHA-1は暗号学的に脆弱
// セキュリティが重要な用途ではSHA-256以上を使用
$secureHash = hash('sha256', $data);

// または
$secureHash = hash('sha512', $data);

まとめ

sha1()関数の特徴をまとめると:

できること:

  • 160ビット(40文字)のハッシュ生成
  • データの整合性確認
  • ユニークIDの生成
  • 重複検出

推奨される使用場面:

  • ファイル整合性チェック
  • キャッシュキー生成
  • ETag生成
  • 重複検出
  • 非暗号化用途の識別子生成

利点:

  • MD5より衝突耐性が高い
  • 高速
  • 広くサポートされている

セキュリティ上の注意:

  • パスワードには使用しない
  • 暗号学的に脆弱(衝突攻撃が可能)
  • セキュリティ用途にはSHA-256以上を推奨

関連関数:

  • md5(): MD5ハッシュ(より脆弱)
  • hash(): 様々なハッシュアルゴリズム
  • password_hash(): パスワード専用
  • sha1_file(): ファイルのSHA-1ハッシュ

使い分け:

// ファイル整合性、キャッシュキー: SHA-1でOK
$cacheKey = sha1($data);

// セキュリティ重要: SHA-256以上
$signature = hash('sha256', $data);

// パスワード: password_hash()
$passwordHash = password_hash($password, PASSWORD_DEFAULT);

sha1()は、整合性確認や識別子生成には便利ですが、セキュリティが重要な用途では使用を避け、より強力なハッシュアルゴリズムを選択しましょう!

タイトルとURLをコピーしました