C# Tips | 文字列処理:文字列ハッシュ化

C# C#
スポンサーリンク

はじめに 「文字列ハッシュ化」は“中身を見せずに、同じかどうかだけ知る”技

文字列ハッシュ化は、一言でいうと、

「文字列の中身を、一定長の“指紋”に変換すること」

です。

元の文字列がどれだけ長くても、
ハッシュ値は決まった長さのバイト列(や16進文字列)になります。

業務では例えば、こういう場面で使われます。

同じデータかどうかを高速に判定したい
ログやIDをそのまま残したくないが、同一性だけは追跡したい
キャッシュのキーを短く・固定長にしたい

ここで大事なのは、

「.NETの GetHashCode() は使ってはいけない場面が多い」
「用途によって“軽いハッシュ”と“暗号学的ハッシュ”を使い分ける」

ということです。

ここから、C#での文字列ハッシュ化を、初心者向けに丁寧に分解していきます。


まず押さえるべきこと:GetHashCode() は“使いどころが違う”

GetHashCode() は「辞書用のハッシュ」であって「永続用・比較用」ではない

C#の string には GetHashCode() メソッドがあります。

string s = "Hello";
int h = s.GetHashCode();
C#

これを見ると、「これでハッシュ取れるじゃん」と思いがちですが、
業務で「ファイルに保存する」「他システムと比較する」「ログに残す」といった用途には 使ってはいけません

理由は主に2つあります。

同じ文字列でも、.NETのバージョンやプロセスごとに GetHashCode() の結果が変わる可能性がある
セキュリティ目的(改ざん検知・匿名化など)には全く向いていない

GetHashCode() は、DictionaryHashSet の内部で「同じプロセス内での高速な検索」のために使うものです。
「永続化」「外部連携」「セキュリティ」を意識する場面では、
必ず System.Security.Cryptography のハッシュアルゴリズムを使います。

ここを最初にしっかり区別しておくのが、とても重要です。


実務でよく使うのは「SHA-256」などの暗号学的ハッシュ

SHA-256 のイメージ

暗号学的ハッシュ関数(SHA-256 など)は、

同じ入力 → 必ず同じハッシュ値
少しでも違う入力 → 全く違うハッシュ値
ハッシュ値から元の入力を復元するのは現実的に不可能

という性質を持っています。

例えば、次の2つの文字列は似ていますが、
SHA-256 の結果はまったく違う値になります。

"Hello"
"hello"

この「ちょっと違うだけで、ハッシュは全然違う」という性質が、
改ざん検知や同一性チェックに向いています。


C#での基本:SHA-256で文字列をハッシュ化するユーティリティ

バイト列にしてからハッシュを取り、16進文字列にする

文字列ハッシュ化の基本パターンは、

文字列 → エンコーディングでバイト配列に変換
バイト配列 → ハッシュアルゴリズムでハッシュ値(バイト配列)に変換
ハッシュ値(バイト配列) → 表示用の16進文字列に変換

という3ステップです。

まずは、SHA-256 を使ったユーティリティを作ってみます。

using System;
using System.Security.Cryptography;
using System.Text;

public static class HashUtil
{
    public static string ComputeSha256Hex(string? text)
    {
        if (string.IsNullOrEmpty(text))
        {
            return string.Empty;
        }

        byte[] bytes = Encoding.UTF8.GetBytes(text);

        using var sha = SHA256.Create();
        byte[] hashBytes = sha.ComputeHash(bytes);

        return ConvertToHex(hashBytes);
    }

    private static string ConvertToHex(byte[] bytes)
    {
        var sb = new StringBuilder(bytes.Length * 2);

        foreach (byte b in bytes)
        {
            sb.Append(b.ToString("x2"));
        }

        return sb.ToString();
    }
}
C#

ここでの重要ポイントを整理します。

文字列を Encoding.UTF8.GetBytes でバイト配列にしている
SHA256.Create() でハッシュアルゴリズムのインスタンスを作り、ComputeHash でハッシュを計算している
結果のバイト配列を、"x2" 形式で16進文字列にしている(よく見るハッシュ表現)

動作例

Console.WriteLine(HashUtil.ComputeSha256Hex("Hello"));
Console.WriteLine(HashUtil.ComputeSha256Hex("hello"));
Console.WriteLine(HashUtil.ComputeSha256Hex("こんにちは"));
C#

出力されるのは、こんな感じの長い16進文字列です(実際の値は環境に依存しません)。

Hellohello で、まったく違う値になることが確認できます。


エンコーディングを固定することの大事さ

「同じ文字列」でもエンコーディングが違うとハッシュも変わる

ハッシュは「バイト列」に対して計算されます。
つまり、同じ "こんにちは" でも、

UTF-8 でバイト列にした場合
UTF-16(Encoding.Unicode)でバイト列にした場合

では、ハッシュ値が変わります。

業務で「ハッシュ値をファイルに保存する」「他システムと比較する」といった用途では、
エンコーディングを必ず固定することが重要です。

一般的には、UTF-8 を使っておけばまず困りません。

byte[] bytes = Encoding.UTF8.GetBytes(text);
C#

この1行を、ユーティリティの中で“固定ルール”としてしまうことで、
呼び出し側がエンコーディングを意識しなくて済むようになります。


SHA-1 や MD5 はどうなのか

もう新規では使わないほうがいい

昔は MD5SHA1 がよく使われていましたが、
今は「衝突耐性が弱い(安全性が足りない)」とされていて、
新規の設計では基本的に SHA-256 以上 を使うのが推奨です。

C#では、次のようなクラスがあります。

MD5
SHA1
SHA256
SHA384
SHA512

業務で「とりあえず安全側に寄せたい」なら、
SHA256 を選んでおけばまず問題ありません。


注意:パスワードには「そのままハッシュ」は使わない

パスワードは専用の仕組み(ストレッチング・ソルト)が必要

ここで一つ、かなり重要な注意点です。

ユーザーのパスワードを扱うときに、

var hash = HashUtil.ComputeSha256Hex(password);
C#

のように「そのままSHA-256にかける」のは、
セキュリティ的には不十分です。

パスワードには、

ソルト(salt)を加える
ストレッチング(何度もハッシュを繰り返す)
専用のアルゴリズム(PBKDF2, bcrypt, Argon2 など)

といった仕組みが必要になります。

ここは「文字列ハッシュ化ユーティリティ」の範囲を超えるので深掘りしませんが、

「パスワードは“ただのハッシュ”では扱わない」

ということだけは、強く意識しておいてください。

文字列ハッシュ化ユーティリティは、
ログID、トラッキング用の匿名化キー、改ざん検知用のチェックサムなど、
「パスワード以外」の用途に使うのが基本です。


バイト配列のまま扱いたい場合のユーティリティ

16進文字列ではなく、バイト配列で返す版

用途によっては、「16進文字列にせず、バイト配列のまま扱いたい」こともあります。
その場合は、こういうユーティリティも用意できます。

public static byte[] ComputeSha256Bytes(string? text)
{
    if (string.IsNullOrEmpty(text))
    {
        return Array.Empty<byte>();
    }

    byte[] bytes = Encoding.UTF8.GetBytes(text);

    using var sha = SHA256.Create();
    return sha.ComputeHash(bytes);
}
C#

これを使えば、
「ハッシュ値をそのままバイナリとしてファイルに書く」
「さらに別の処理に渡す」
といったことがしやすくなります。


業務ユーティリティとしてどうまとめるか

「アルゴリズム固定」と「表現形式」を決めておく

プロジェクト全体で使うユーティリティとしては、

アルゴリズム(例:SHA-256)を固定する
エンコーディング(例:UTF-8)を固定する
返り値の形式(16進文字列か、バイト配列か)を決める

という3点を、ユーティリティ側で“ルール化”しておくと、
呼び出し側が迷わなくて済みます。

例えば、こんな感じのまとめ方が考えられます。

public static class TextHash
{
    public static string Sha256Hex(string? text)
        => HashUtil.ComputeSha256Hex(text);

    public static byte[] Sha256Bytes(string? text)
        => HashUtil.ComputeSha256Bytes(text);
}
C#

呼び出し側は、

ログ用のIDを作りたい → TextHash.Sha256Hex(rawId)
バイナリでチェックサムを持ちたい → TextHash.Sha256Bytes(content)

というように、意図に応じて選べます。

null や空文字の扱いを決めておく

今回の実装では、

null や空文字 → 空文字(または空配列)を返す

という方針にしています。

これにより、呼び出し側で毎回

if (text != null) { ... }
C#

といったガードを書く必要がなくなり、
業務コードがすっきりします。

「nullは例外にしたい」などの方針があるなら、
そこもユーティリティ側で統一しておくとよいです。


まとめ 「文字列ハッシュ化ユーティリティ」は“中身を隠しつつ、同一性を追える安全な指紋”

文字列ハッシュ化は、
「中身をそのまま持ちたくないけれど、同じものかどうかは知りたい」
というニーズに応えるための、実務でとても使えるテクニックです。

押さえておきたいポイントは、

GetHashCode() は辞書用であり、永続化・外部連携・セキュリティ用途には使わない
暗号学的ハッシュ(SHA-256など)を使うのが基本
文字列 → UTF-8バイト列 → ハッシュ → 16進文字列、という流れをユーティリティ化しておく
エンコーディングとアルゴリズムを固定しておくことで、結果が安定する
パスワードには“ただのハッシュ”を使わず、専用の仕組みが必要

ここまで理解できれば、「なんとなくハッシュを取っている」段階から一歩進んで、
“用途と安全性を意識した、業務で使える文字列ハッシュ化ユーティリティ”を、
自分のC#コードの中にしっかり組み込めるようになっていきます。

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