Text'i Doğru İşlemek: Normalization, Mojibake Teşhisi ve Base64
Aynı görünen iki string eşit olmayabilir çünkü "ş" hem tek code point (NFC) hem s + birleşen işaret (NFD) olarak yazılabilir; çözüm text'i sistemin sınırında NFC'ye getirmek. NFKC ile arama anahtarı, culture'a göre değişen arama, "ÅŸ" gibi mojibake'i hex dump ile teşhis edip onarmak, CSV/Excel BOM'u, HTTP charset, SQL Server'da N prefix'i ve UTF-8 collation, Base64/Base64Url ve JWT'nin neden şifreli olmadığı.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (C# 14, ASP.NET Core 10), TypeScript 7.0, Node.js 24 ve SQL Server 2025 ile denenmiştir; C# örneklerinin testleri xUnit v3 4.0.3 ile yazılmıştır. Base64 için RFC (Request for Comments; internet standartlarının yayımlandığı doküman serisi) 4648'i esas alır.
Önce Tahmin Et
Bookstore'un yazar tablosunda yazar adına unique index var. Bir editör "Ayşe" adlı yazarı Windows'taki panelden, bir diğeri Mac'ten yüklediği bir dosyanın adından ekliyor. İki kayıt da ekranda birebir aynı görünüyor ve unique index ikisini de kabul ediyor. Nasıl?
İkinci soru: Bookstore'un raporlama ekranından indirilen bir CSV (Comma-Separated Values; değerleri virgülle ayıran düz metin tablo formatı) dosyası Excel'de çift tıklanınca "Kürk Mantolu Madonna" yerine "Kürk Mantolu Madonna" görünüyor. Dosya mı bozuk, Excel mi yanlış okuyor? "ü" sana hangi harfin hangi encoding ile okunduğunu söylüyor?
Üçüncü soru: bir geliştirici "token'ı Base64 ile şifreledik, güvenli" diyor. Doğru mu?
Serinin önceki yazısında bir karakterin UTF (Unicode Transformation Format; Unicode dönüşüm biçimi) encoding'leriyle byte'lara nasıl çevrildiğini gördük. Bu son yazıda iş tersine dönüyor: byte'lardan doğru text'i geri almak, aynı text'i aynı şekilde karşılaştırmak ve binary veriyi text olarak taşımak.
Normalization: Aynı Görünen İki String
Önceki yazıda "ş"nin iki şekilde yazılabildiğini gördük: tek code point U+015F ya da "s" ve ardından birleşen çengel işareti U+0327. Unicode bu iki yazımı "canonically equivalent" (anlam ve görünüş olarak aynı) sayar. Ama string karşılaştırması code unit'lere bakar:
static string Hex(string s) => string.Join(" ", s.Select(c => ((int)c).ToString("X4")));
string nfc = "Ay\u015Fe";
string nfd = "Ays\u0327e";
Console.WriteLine($"{nfc} {nfd} {nfc == nfd} {nfc.Length} {nfd.Length}"); // Ayşe Ayşe False 4 5
Console.WriteLine(Hex(nfc)); // 0041 0079 015F 0065
Console.WriteLine(Hex(nfd)); // 0041 0079 0073 0327 0065
Console.WriteLine(nfd.Normalize(NormalizationForm.FormC) == nfc); // True
Console.WriteLine($"{nfc.IsNormalized(NormalizationForm.FormC)} {nfd.IsNormalized(NormalizationForm.FormC)}"); // True False
Console.WriteLine(Hex("\u0130".Normalize(NormalizationForm.FormD))); // 0049 0307
Unicode'un UAX (Unicode Standard Annex; standardın ek dokümanları) serisindeki 15 numaralı doküman dört normalization form tanımlar. NFC (Normalization Form C; birleşebilen karakterleri tek code point'e toplayan form) "ş"yi U+015F'e çevirir. NFD (Normalization Form D; karakterleri temel harf ve birleşen işaretlere ayıran form) ise "s" + U+0327'ye ayırır. "İ" bile NFD'de "I" ve üstündeki noktaya (U+0307) ayrılır. Web'de, Windows'ta ve çoğu klavyeden gelen metinde yaygın form NFC'dir.
NFD nereden gelir? macOS'un eski dosya sistemi (HFS+) dosya adlarını NFD olarak saklıyordu. Bugünkü APFS (Apple File System; macOS'un güncel dosya sistemi) adı nasıl yazıldıysa öyle saklıyor, ama HFS+'tan taşınmış dosyalar ve bazı uygulamalar hâlâ NFD adlar üretiyor. Sonuç: Mac kullanan birinin yüklediği "Ayşe.docx" dosyasının adı sunucuya NFD olarak gelebilir. Önce Tahmin Et'teki sorunun cevabı bu: iki kayıt ekranda aynı görünüyor ama byte'ları farklı, unique index de byte'lara bakıyor.
NFKC ve NFKD: Uyumluluk Formları
Diğer iki form, NFKC (birleştirmenin yanında uyumluluk eşlemesi de yapan normalization form) ve NFKD (ayrıştırmanın yanında uyumluluk eşlemesi de yapan normalization form), bir adım daha ileri gider ve "aynı anlama gelen ama farklı görünen" karakterleri de birleştirir. Bunlara compatibility (uyumluluk) formları denir:
foreach (string s in new[] { "\uFB01ction", "\u2460", "\uFF21\uFF22\uFF23", "x\u00B2", "\u00BD" })
{
Console.WriteLine($"{s,-8} NFC={s.Normalize(NormalizationForm.FormC)} NFKC={s.Normalize(NormalizationForm.FormKC)}");
}
fiction NFC=fiction NFKC=fiction
① NFC=① NFKC=1
ABC NFC=ABC NFKC=ABC
x² NFC=x² NFKC=x2
½ NFC=½ NFKC=1⁄2
"fi" bağlı harfi, daire içindeki 1, tam genişlikte yazılmış Asya klavyesi harfleri ve üst simge NFKC'de sade karşılıklarına döner. Bu arama için çok işe yarar: PDF'ten kopyalanmış "fiction" araması "fiction"ı bulur. Ama bilgi kaybeder: "x²" ile "x2" aynı şey değil. Bu yüzden NFKC'yi ekranda gösterilecek metin için değil, arama ve eşleştirme anahtarı için kullan.
public static class TextKeys
{
// Karşılaştırmadan veya unique key olarak saklamadan önce tek bir kanonik form
public static string Canonical(string value) =>
value.Normalize(NormalizationForm.FormC).Trim();
// Arama anahtarı: uyumluluk formları birleştirilir, büyük/küçük harf farkı kalkar
public static string SearchKey(string value) =>
value.Normalize(NormalizationForm.FormKC).ToLowerInvariant();
}
Karşılaştırma: Ordinal, Culture ve Arama
StringComparison.Ordinal code unit'leri karşılaştırır ve NFC ile NFD'yi farklı sayar. Culture'a duyarlı karşılaştırma ise canonical equivalence'ı bilir:
Console.WriteLine(string.Equals(nfc, nfd, StringComparison.Ordinal)); // False
Console.WriteLine(string.Equals(nfc, nfd, StringComparison.InvariantCulture)); // True
var authors = new HashSet<string>(StringComparer.Ordinal) { nfc, nfd };
Console.WriteLine(authors.Count); // 2
var canonical = new HashSet<string>(StringComparer.Ordinal) { TextKeys.Canonical(nfc), TextKeys.Canonical(nfd) };
Console.WriteLine(canonical.Count); // 1
Ordinal karşılaştırma hızlı ve öngörülebilir olduğu için unique key, dictionary key ve veritabanı index'leri için doğru seçim; yeter ki veri içeri girerken tek bir forma getirilmiş olsun. Çözüm basit: dış dünyadan gelen her metni (form alanı, dosya adı, API isteği) sistemin sınırında Normalize(NormalizationForm.FormC) ile normalize et, ondan sonra ordinal karşılaştırma güvenle çalışır.
Arama ise başka bir ihtiyaç. "aysegul" yazan bir kullanıcının "Ayşegül"ü bulmasını istersin. CompareOptions.IgnoreNonSpace birleşen işaretleri yok sayar, ama sonuç culture'a göre değişir:
CompareInfo tr = CultureInfo.GetCultureInfo("tr-TR").CompareInfo;
CompareInfo inv = CultureInfo.InvariantCulture.CompareInfo;
string shelf = "Ay\u015Feg\u00FCl Kitap\u00E7\u0131";
CompareOptions loose = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace;
Console.WriteLine($"{inv.IndexOf(shelf, "aysegul", loose)} {inv.IndexOf(shelf, "kitapci", loose)} {inv.IndexOf(shelf, "kitapc\u0131", loose)}");
// 0 -1 8
Console.WriteLine($"{tr.IndexOf(shelf, "aysegul", loose)} {tr.IndexOf(shelf, "kitapci", loose)} {tr.IndexOf(shelf, "kitapc\u0131", loose)}");
// -1 -1 -1
Invariant culture'da "ş" bir "s" ile çengelden, "ü" bir "u" ile iki noktadan oluşur ve işaretler yok sayılınca "aysegul" eşleşir. "ı" ise ayrışan bir karakter değil, kendi başına bir harf; "kitapci" bu yüzden bulunamaz. Türkçe culture'da ş, ç, ü, ğ alfabenin ayrı harfleri olduğu için hiçbiri işaret olarak görülmez ve üç arama da başarısız olur. Kullanıcıların Türkçe karakter yazmadan arama yapabilmesini istiyorsan arama anahtarını kendin üret (ş → s, ı → i gibi açık bir eşleme tablosuyla) ve hem içeriği hem sorguyu aynı fonksiyondan geçir. Türkçe I'nın culture'daki davranışı C# Temelleri serisinin "C# String'ler: Interpolation, StringBuilder ve Türkçe I Tuzağı" yazısında anlatıldı.
Mojibake Teşhisi: "ÅŸ" Ne Anlatıyor
Mojibake, text'in yazıldığı encoding'den farklı bir encoding'le okunmasıyla ortaya çıkan bozuk karakterlerdir. Rastgele görünür ama değildir; her bozuk karakter hangi hatanın yapıldığını söyler. .NET'te eski code page'leri kullanmak için önce provider'ı kaydetmen gerekiyor:
public static class Mojibake
{
static Mojibake() => Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
public static Encoding Windows1252 => Encoding.GetEncoding(1252);
public static Encoding Windows1254 => Encoding.GetEncoding(1254);
}
string original = "Kitap\u00E7\u0131 \u015Fiir \u011F\u00FC\u00F6 \u0130 \u00C7";
byte[] utf8 = Encoding.UTF8.GetBytes(original);
Console.WriteLine(original); // Kitapçı şiir ğüö İ Ç
Console.WriteLine(Mojibake.Windows1252.GetString(utf8)); // Kitapçı şiir ğüö İ Ç
Console.WriteLine(Encoding.UTF8.GetString(Mojibake.Windows1254.GetBytes(original))); // Kitap�� �iir ��� � �
İki farklı hata, iki farklı görüntü. Birincisinde UTF-8 byte'ları tek byte'lık bir code page ile okunmuş: her Türkçe karakter UTF-8'de 2 byte olduğu için ekranda 2 karaktere dönüşüyor. "ş"nin UTF-8 byte'ları C5 9F; Windows-1252'de C5 "Å", 9F "Ÿ". Bu yüzden "ÅŸ" gördüğün an teşhis hazır: "UTF-8 yazılmış, Latin code page ile okunmuş". Sık görülen eşleşmeler:
ç=C3A7=ç ğ=C49F=ÄŸ ı=C4B1=ı ö=C3B6=ö ş=C59F=ÅŸ ü=C3BC=ü
Ç=C387=Ç Ğ=C49E=Äž İ=C4B0=İ Ö=C396=Ö Ş=C59E=Åž Ü=C39C=Ü
İkinci hatada tersi olmuş: Windows-1254 ile yazılmış byte'lar UTF-8 olarak okunmuş. Windows-1254'te "ş" tek byte, FE; bu byte UTF-8'de hiçbir geçerli dizinin parçası olamaz ve yerine U+FFFD yani � konur. "�" görüyorsan veri UTF-8 değil ve asıl byte'lar o noktada kaybolmuş olabilir.
Ekrandaki karaktere değil byte'lara bakarak teşhis etmek daha güvenilir. Bozuk görüntü bir kez daha UTF-8 olarak kaydedilirse "çift encode" edilmiş veri oluşur ve hex dump'ta açıkça görünür:
Console.WriteLine(Convert.ToHexString(Mojibake.Windows1254.GetBytes(original)));
// 4B69746170E7FD20FE69697220F0FCF620DD20C7
Console.WriteLine(Convert.ToHexString(Encoding.UTF8.GetBytes(Mojibake.Windows1252.GetString(utf8))));
// 4B69746170C383C2A7C384C2B120C385C5B869697220C384C5B8C383C2BCC383C2B620C384C2B020C383E280A1
İlk satırda her Türkçe karakter tek byte (E7, FD, FE): bu bir code page dosyası. İkinci satırda "ç" için C3 83 C2 A7 var: C3 A7'nin iki byte'ı ayrı ayrı UTF-8'e çevrilmiş. Bir dosyada C3 83 ve C2 ile başlayan çiftler sık geçiyorsa çift encode edilmiş UTF-8'e bakıyorsun.
Bu tür veri kurtarılabilir: bozuk metni yanlış okunduğu code page'e geri çevirip doğru şekilde, UTF-8 olarak oku. Ama bunu yalnızca dönüşümün kayıpsız olduğundan emin olduğunda yap:
// Windows-1252 olarak okunmuş UTF-8 byte'ları: geri çevir, doğru oku
public static bool TryRepair(string garbled, out string repaired)
{
Encoding strict1252 = Encoding.GetEncoding(1252, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
Encoding strictUtf8 = new UTF8Encoding(false, throwOnInvalidBytes: true);
try
{
repaired = strictUtf8.GetString(strict1252.GetBytes(garbled));
return true;
}
catch (Exception ex) when (ex is EncoderFallbackException or DecoderFallbackException)
{
repaired = garbled;
return false;
}
}
True Kitapçı şiir ğüö İ Ç
False Kitapçı şiir ğüö İ Ç
İlk çağrı bozuk metni onarıyor, ikincisi zaten doğru olan metne dokunmuyor çünkü "ı" Windows-1252'de yok ve strict encoder exception fırlatıyor. Exception fallback'ı neden açıkça seçtiğimizi şu satır gösteriyor: varsayılan Windows-1252 encoder'ı "ş"yi hata vermeden "s"ye çeviriyor (best-fit mapping):
Console.WriteLine(Convert.ToHexString(Mojibake.Windows1252.GetBytes("\u015F"))); // 73
Encoding'in Yolculuğu: CSV, HTTP ve SQL Server
Text bir sistemden diğerine her geçtiğinde encoding bilgisi ya byte'larla birlikte taşınır ya da kaybolur. Üç yaygın durak:
CSV dosyalarının encoding'i yazan bir metadata alanı yok. Windows'ta Excel, çift tıklamayla açılan CSV'yi başında UTF-8 BOM'u (Byte Order Mark; dosyanın başına encoding'i belli etmek için konan byte'lar) yoksa UTF-8 olarak tanımayıp sistemin varsayılan code page'iyle okuyor; Türkçe Windows'ta bu Windows-1254. İkinci sorunun cevabı bu: dosya doğru, Excel yanlış encoding'le okuyor ve "ü"nün iki byte'ı "ü" olarak görünüyor. Excel'in kendi "CSV UTF-8" kayıt seçeneği de bu yüzden BOM yazıyor. Çözüm, export'u BOM'lu yazmak:
string csv = "Id,Title,Author\n1,K\u00FCrk Mantolu Madonna,Sabahattin Ali\n";
File.WriteAllText("books-nobom.csv", csv);
File.WriteAllText("books-bom.csv", csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
Console.WriteLine(Mojibake.Windows1254.GetString(File.ReadAllBytes("books-nobom.csv")).Split('\n')[1]);
// 1,Kürk Mantolu Madonna,Sabahattin Ali
$ xxd -l 16 books-bom.csv
00000000: efbb bf49 642c 5469 746c 652c 4175 7468 ...Id,Title,Auth
$ xxd -l 16 books-nobom.csv
00000000: 4964 2c54 6974 6c65 2c41 7574 686f 720a Id,Title,Author.
BOM'un bedeli de var: BOM'u beklemeyen araçlar onu verinin parçası sanar. Python'un csv modülü BOM'lu dosyayı utf-8 ile açınca ilk kolonun adı '\ufeffId' olur, utf-8-sig ile açınca 'Id'. Dosyanın kimin tarafından okunacağını bilerek karar ver; Excel kullanıcıları için yapılan export'ta BOM'lu, makineler arası veri alışverişinde BOM'suz.
HTTP'de encoding Content-Type header'ının charset parametresiyle taşınır. ASP.NET Core Results.Text ile bunu kendisi ekler:
app.MapGet("/export/books.csv", () => Results.Text(csv, "text/csv", Encoding.UTF8));
app.MapGet("/legacy/books.csv", () => Results.Bytes(Encoding.GetEncoding(1254).GetBytes(csv), "text/csv; charset=windows-1254"));
app.MapGet("/broken/books.csv", () => Results.Bytes(Encoding.UTF8.GetBytes(csv), "text/csv; charset=iso-8859-1"));
$ curl -i http://localhost:5732/export/books.csv
HTTP/1.1 200 OK
Content-Length: 33
Content-Type: text/csv; charset=utf-8
İstemci header'daki charset'e göre çözer. .NET'in HttpClient'ı ile üç endpoint'i okuyunca:
/export/books.csv 1,Kürk Mantolu Madonna
/legacy/books.csv 1,Kürk Mantolu Madonna
/broken/books.csv 1,Kürk Mantolu Madonna
Windows-1254 byte'ları doğru etiketlendiği için sorunsuz okunuyor; UTF-8 byte'ları yanlış etiketlendiğinde ise mojibake çıkıyor. Bozuk karakter, byte'lardan değil yanlış etiketten geliyor.
SQL Server'da encoding kolon tipine ve collation'a bağlı. varchar kolonu collation'ın code page'ini kullanır. Örnekteki veritabanının collation'ı SQL_Latin1_General_CP1_CI_AS, yani Windows-1252. Windows'ta kurulum sırasındaki varsayılan collation sistem diline göre seçilir; Türkçe bir sistemde bu Turkish_CI_AS (Windows-1254) olur ve Türkçe harfler varchar'a sığar, ama aynı sorun Windows-1254'te olmayan her karakter için devam eder. nvarchar UTF-16 tutar. SQL Server 2019'dan beri _UTF8 ile biten collation'larla varchar UTF-8 de tutabilir. Aynı ismi üç kolona, bir kez N prefix'iyle bir kez prefix'siz yazdım:
CREATE TABLE dbo.Authors
(
Id int IDENTITY PRIMARY KEY,
NameVarchar varchar(20) NOT NULL,
NameNvarchar nvarchar(20) NOT NULL,
NameUtf8 varchar(20) COLLATE Latin1_General_100_CI_AS_SC_UTF8 NOT NULL
);
INSERT INTO dbo.Authors (NameVarchar, NameNvarchar, NameUtf8)
VALUES (N'Ayşe Kılıç', N'Ayşe Kılıç', N'Ayşe Kılıç'),
('Ayşe Kılıç', 'Ayşe Kılıç', 'Ayşe Kılıç');
Id|NameVarchar|NameNvarchar|NameUtf8|VarcharBytes|NvarcharBytes|Utf8Bytes
--|-----------|------------|--------|------------|-------------|---------
1|Ayse Kiliç|Ayşe Kılıç|Ayşe Kılıç|10|20|14
2|Ayse Kiliç|Ayse Kiliç|Ayse Kiliç|10|20|11
İlk satırda Windows-1252 varchar kolonu "ş" ve "ı"yı tutamadığı için best-fit ile "s" ve "i" yazmış; "ç" Windows-1252'de olduğu için kalmış. nvarchar ve UTF-8 kolon doğru. İkinci satır daha sinsi: N prefix'i olmayan string literal, kolona gelmeden önce veritabanının code page'ine çevrildiği için nvarchar ve UTF-8 kolon da bozuk değeri almış. Kolon tipi doğru olsa bile literal yanlışsa veri kaybolur. Uygulama kodundan parametreyle (ADO.NET, Entity Framework Core, Dapper) gönderilen string'ler nvarchar parametre olarak gittiği için bu sorun genellikle elle yazılmış script'lerde ve migration'larda çıkar.
Base64: Binary Veriyi Text Olarak Taşımak
Text kanalları (JSON, URL, e-posta, HTTP header) her byte değerini taşıyamaz. Base64 her 3 byte'ı (24 bit) dörder dörder 6 bitlik gruplara böler ve her grubu 64 karakterlik bir alfabeden (A–Z, a–z, 0–9, +, /) bir karaktere çevirir. 3 byte 4 karakter olduğu için boyut yüzde 33 büyür; son grup eksikse = ile tamamlanır (padding):
Console.WriteLine(Convert.ToBase64String("Bookstore"u8)); // Qm9va3N0b3Jl
foreach (string s in new[] { "a", "ab", "abc", "abcd" })
{
Console.WriteLine($"{s,-4} {Convert.ToBase64String(Encoding.ASCII.GetBytes(s))}");
}
// a YQ==
// ab YWI=
// abc YWJj
// abcd YWJjZA==
+ ve / URL'lerde ve dosya adlarında özel anlam taşıdığı için RFC 4648 bir de URL-safe alfabe tanımlar: + yerine -, / yerine _, padding genellikle yok. .NET 9 ile gelen System.Buffers.Text.Base64Url bunu doğrudan yapıyor:
byte[] tricky = [0xFB, 0xFF, 0xBF, 0x3E];
Console.WriteLine(Convert.ToBase64String(tricky)); // +/+/Pg==
Console.WriteLine(Base64Url.EncodeToString(tricky)); // -_-_Pg
Console.WriteLine(Convert.ToHexString(Base64Url.DecodeFromChars("-_-_Pg"))); // FBFFBF3E
byte[] cover = File.ReadAllBytes("cover.png");
string dataUri = $"data:image/png;base64,{Convert.ToBase64String(cover)}";
Console.WriteLine($"{cover.Length} {dataUri.Length - "data:image/png;base64,".Length} {(double)(dataUri.Length - 22) / cover.Length:F3}");
// 58708 78280 1.333
Data URI (resmi veya dosyayı doğrudan adres metninin içine gömen data: adresi), küçük bir resmi HTML veya CSS'in içine gömmenin yolu. Ama 58 KB'lık PNG (kayıpsız sıkıştırma kullanan bir resim formatı) dosyası 78 KB'lık bir metne dönüşüyor ve tarayıcı onu ayrı bir dosya olarak cache'leyemiyor; yalnızca çok küçük ikonlar için mantıklı.
TypeScript tarafında btoa ve atob yalnızca Latin-1 aralığındaki karakterleri kabul eder; "ı" gibi bir karakter görünce hata verir. Text'i önce UTF-8 byte'larına çevirmek gerekiyor. Node.js'te Buffer bunu ve Base64Url'i destekliyor:
console.log(btoa("Bookstore"), atob("Qm9va3N0b3Jl")); // Qm9va3N0b3Jl Bookstore
try {
btoa("Kitap\u00E7\u0131");
} catch (e) {
console.log((e as Error).name); // InvalidCharacterError
}
console.log(Buffer.from("Kitap\u00E7\u0131").toString("base64")); // S2l0YXDDp8Sx
console.log(Buffer.from([0xfb, 0xff, 0xbf, 0x3e]).toString("base64url")); // -_-_Pg
Base64 Şifreleme Değildir: JWT Örneği
Base64 bir encoding; anahtarı yok, herkes geri çevirebilir. Üçüncü sorunun cevabı bu: Base64 ile "şifrelenmiş" bir token, okuyan herkes için düz metindir. En sık karşılaşılan örnek JWT (JSON Web Token; header, payload ve imzanın Base64Url ile birleştirildiği token formatı):
string header = Base64Url.EncodeToString("{\"alg\":\"HS256\",\"typ\":\"JWT\"}"u8);
string payload = Base64Url.EncodeToString("{\"sub\":\"42\",\"name\":\"Ada\",\"role\":\"customer\"}"u8);
byte[] key = RandomNumberGenerator.GetBytes(32);
string signature = Base64Url.EncodeToString(HMACSHA256.HashData(key, Encoding.ASCII.GetBytes($"{header}.{payload}")));
string token = $"{header}.{payload}.{signature}";
string[] parts = token.Split('.');
Console.WriteLine(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[1])));
// {"sub":"42","name":"Ada","role":"customer"}
string forged = Base64Url.EncodeToString("{\"sub\":\"42\",\"name\":\"Ada\",\"role\":\"admin\"}"u8);
byte[] expected = HMACSHA256.HashData(key, Encoding.ASCII.GetBytes($"{parts[0]}.{forged}"));
Console.WriteLine(CryptographicOperations.FixedTimeEquals(expected, Base64Url.DecodeFromChars(parts[2])));
// False
Payload'ı okumak için hiçbir anahtar gerekmedi. JWT'nin güvenliği gizlilikten değil imzadan geliyor: rolü "admin" yapan sahte payload, HMAC (Hash-based Message Authentication Code; gizli bir anahtarla hesaplanan, verinin değiştirilmediğini kanıtlayan değer) imzasını tutturamıyor. Token'ı decode edip "çözdüm" demek ile imzasını doğrulamak farklı şeyler; production'da token'ı her zaman doğrulayan bir kütüphaneyle işle. Encoding, hashing ve encryption ayrımı Web Güvenliği (OWASP Top 10) serisinin "OWASP A04: Cryptographic Failures (1) — Encoding, Hashing, Encryption ve HTTPS" yazısında anlatıldı; HMAC ve imzaların ayrıntıları da Geliştirici için Kriptografi serisinde.
Üretimde Dikkat
Text'i sistemin sınırında bir kez normalize et (NFC) ve içeride hep o formla çalış. Unique key, arama anahtarı ve dosya adı karşılaştırmaları bu tek kurala dayanır.
Encoding bilgisini metadata ile taşı: HTTP'de charset, dosyada BOM veya dokümante edilmiş bir sözleşme, veritabanında nvarchar ya da UTF-8 collation. Bir dış sistemle dosya alışverişi yapıyorsan encoding'i entegrasyon dokümanına yaz.
Elle yazılan SQL'de Unicode literal'lerine N prefix'i koy. Migration ve seed script'lerini code review'da bu gözle kontrol et; bozulan veri sonradan geri getirilemez.
Eski bir sistemden gelen veriyi içeri alırken strict decoder kullan. DecoderFallback.ExceptionFallback ile yanlış encoding'i ilk satırda yakalarsın; varsayılan davranış ise � ile devam eder ve sorunu raporlara kadar taşır.
Sık Düşülen Tuzaklar
Encoding'i tahmin etmek. Windows-1252'de her byte değeri bir karaktere karşılık gelir, yani her byte dizisi "geçerli" bir Windows-1252 metnidir. UTF-8 için geçerli olan bir dosya aynı zamanda geçerli bir Windows-1252 dosyasıdır; hangisi olduğuna byte'lar karar veremez. Encoding detection kütüphaneleri istatistikle tahmin yapar ve kısa metinlerde sık yanılır. Encoding'i kaynaktan öğren, tahmin etme.
JWT'yi Base64 decode edip içindeki bilgiye güvenmek. Payload'daki role veya sub değerini imzayı doğrulamadan kullanmak, isteği gönderen herkesin kendine istediği rolü vermesi demek.
Standart Base64'ü URL'de kullanmak. Query string'deki +, form encoding kurallarına göre boşluk olarak çözülür:
var query = HttpUtility.ParseQueryString("token=" + Convert.ToBase64String(tricky));
Console.WriteLine(query["token"]); // / /Pg==
+/+/Pg== sunucuya / /Pg== olarak ulaştı ve decode edilemiyor. URL'de, cookie'de ve dosya adında Base64Url kullan.
Code page'e çevirirken sessiz veri kaybı. Windows-1252 encoder'ının "ş"yi "s" yapması gibi best-fit dönüşümler exception fırlatmaz. Eski bir sisteme veri gönderiyorsan exception fallback'lı bir encoder kullan ve dönüştürülemeyen karakterleri açıkça raporla.
Küçük Bir Deneme
★☆☆ Bir dosyayı adı NFD olacak şekilde oluştur ("Ays\u0327e.txt"), sonra Directory.GetFiles ile okuyup NFC hâliyle == karşılaştır. Aynı karşılaştırmayı Normalize ile tekrarla.
★★☆ Mojibake.TryRepair'i iki kez bozulmuş (iki kez yanlış okunmuş) metni de onaracak şekilde genişlet: onarım başarılı olduğu ve sonuç değiştiği sürece tekrar et. "ÅŸ" ve "ÅŸ" girdileri için test yaz.
★★★ Bookstore'un CSV export endpoint'ine bir ?target=excel parametresi ekle: Excel için BOM'lu UTF-8, diğer durumlarda BOM'suz UTF-8 dönsün ve iki durumda da Content-Type'ta doğru charset olsun. İki yanıtın ilk byte'larını curl ve xxd ile kontrol eden bir script yaz.
Bu yazıyla Number System'ler ve Data Representation serisi bitiyor. Binary ve hex'ten başlayıp bitwise işlemler, integer ve floating point temsili, Unicode ve text işlemeye kadar bilgisayarın veriyi bit düzeyinde nasıl tuttuğunu gördük. Bu bitlerin CPU'da nasıl işlendiğini merak ediyorsan sıradaki durak Bilgisayar Nasıl Çalışır serisi: register'lar, pipeline ve cache, burada gördüğümüz byte'ları gerçek donanımın üzerinde gösteriyor.