Sır ile taşıyıcıyı ayırmak
SnapSeal görüntü, dosya veya metnin özgün baytlarını metaverileriyle birlikte şifreler, ardından oluşan zarfı bir kapak görüntüsünün piksellerine yerleştirir. Alıcının korumalı dosyaya ve parolaya ihtiyacı vardır; özgün kapağa veya sunucuya gerek yoktur.
Gizliliği kriptografi sağlar; steganografi ise veriye bir taşıyıcı sunar. İkisi de yerleşik tekniklerdir. Buradaki mühendislik işi, aralarındaki sınırları açıkça belirlemektir. Örnekteki görsel ve test parolası bilerek herkese açık tutulmuştur.
original_bytes + metadataArgon2id → AES-GCMheader + ciphertext + tagRGB → PNG
password = "Public SnapSeal article example"En düşük iki bit nasıl kullanılıyor?
Her RGB pikselinde 8 bitlik üç kanal bulunur. Bu örnekte kodlayıcı kanalları soldan sağa okur, alfa kanalını atlar, üstteki altı biti korur ve en düşük iki biti değiştirir. Böylece dört kanal bir bayt taşıyabilir.
Vurgulanan bitlerin değişimini görmek için kaydırıcıyı hareket ettirin. Yalnızca bu düşük bit yazma işlemi, bir kanalın değerini en fazla 3 değiştirir. İsteğe bağlı JPEG taşıyıcısı ve diğer görsel düzenlemeler kapağı daha fazla değiştirebilir.
R₀10101011170 → 171 Δ +1G₀10110101181 → 181 Δ 0B₀0101110094 → 92 Δ -2R₁11001011203 → 203 Δ 0def embed_byte(channels: bytes, value: int) -> bytes:
if len(channels) != 4 or not 0 <= value <= 255:
raise ValueError("Expected four channels and one byte")
pairs = [(value >> shift) & 3 for shift in (6, 4, 2, 0)]
return bytes((c & 0xFC) | pair
for c, pair in zip(channels, pairs, strict=True))
def read_byte(channels: bytes) -> int:
value = 0
for channel in channels:
value = (value << 2) | (channel & 3)
return value
cover = bytes([170, 181, 94, 203])
stored = embed_byte(cover, 0xD3)
assert stored == bytes([171, 181, 92, 203])
assert read_byte(stored) == 0xD3Kaç bayt sığar?
Üç kanalın her birinde iki bit kullanıldığında piksel başına altı bit elde edilir. 1024×1024 boyutundaki bir kapakta, 16 baytlık piksel paketi başlığından önce 786.432 baytlık yer vardır; başlıktan sonra 786.416 bayt kalır. Kapak görseli için ayrılan pikseller bu kapasiteyi azaltır.
Sabit bir işaret paket düzenini belirtir; uzunluk alanı da zarfın nerede bittiğini gösterir. Paket ve şifreleme sürümleri birbirinden bağımsızdır: Bir PNG2 paketi v3 şifreleme zarfı taşıyabilir.
C = floor(W × H × 3 × 2 / 8) − 16
C(1024, 1024) = 786,416 bytes[0:12] | SNAPSEALPNG2 | 12 bytes |
|---|---|---|
[12:16] | uint32_be(envelope_length) | 4 bytes |
[16:…] | envelope | envelope_length |
Anahtar türetme ve zarfın doğrulanması
SnapSeal’in v3 profili, UTF-8 paroladan ve rastgele üretilen 16 baytlık tuzdan 32 baytlık bir anahtar türetmek için Argon2id v1.3 kullanır. İşlem 64 MiB bellek, üç geçiş ve tek şerit kullanır. Okuyucu, KDF’yi çalıştırmadan önce iş yükü parametrelerinin desteklenen sabit değerlere uyduğunu doğrulamalıdır.
AES-256-GCM özgün baytları ve metaverileri şifreler. 16 baytlık etiketi, şifreli metnin yanı sıra herkese açık 48 baytlık başlığı da doğrular. Kimlik doğrulama başarısız olursa okuyucu açık metni kesinlikle sunmamalıdır.
[0:8] | SNAPSEAL |
|---|---|
[8:12] | version=3 · KDF=1 · Argon2=0x13 (v1.3) · lanes=1 |
[12:16] | memory_KiB=65536 |
[16:20] | passes=3 |
[20:36] | salt · 16 bytes |
[36:48] | nonce · 12 bytes |
from argon2.low_level import Type, hash_secret_raw
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def decrypt_v3(envelope: bytes, password: str) -> bytes:
if not 70 <= len(envelope) <= 50 * 1024 * 1024 + 4096:
raise ValueError("Invalid envelope size")
header = envelope[:48]
if header[:12] != b"SNAPSEAL\x03\x01\x13\x01":
raise ValueError("Expected the v3 Argon2id profile")
memory = int.from_bytes(header[12:16], "big")
passes = int.from_bytes(header[16:20], "big")
if (memory, passes) != (65536, 3):
raise ValueError("Unsupported work factors")
secret = password.encode("utf-8")
if not 1 <= len(password) <= 256 or len(secret) > 1024:
raise ValueError("Invalid password length")
key = hash_secret_raw(
secret, header[20:36], time_cost=passes,
memory_cost=memory, parallelism=1, hash_len=32,
type=Type.ID, version=19,
)
return AESGCM(key).decrypt(
header[36:48], envelope[48:], header,
)plaintext = uint32_be(metadata_length) + metadata_json + original_bytes
envelope = header + AESGCM(key).encrypt(nonce, plaintext, header)Mesajı taşıyan PNG değil, pikseller
Veri PNG metaverisinde değil, çözülmüş RGB değerlerindedir. İki PNG’nin dosya baytları farklı olsa bile çözülmüş RGB değerleri değişmemişse düşük bitlerdeki mesaj aynı kalır. Buna karşılık ekran görüntüsü almak, pikselleri düzenlemek veya boyutu değiştirmek, görüntü hâlâ tanınabilir olsa bile veriyi bozabilir.
Kaynak sıkıştırması açıkça seçilmediği sürece özgün baytlar ve metaveriler aynen geri yüklenir. Dosya adı şifrelidir; ancak zarf doldurulmadığında yaklaşık boyut yine de anlaşılır.
PNG bytes A ≠ PNG bytes B
RGB(A) = RGB(B), α(A) = α(B) = 255
→ envelope(A) = envelope(B)Sıkıştırmaya karşı ikinci yol: JPEG kurtarma
“Sıkıştırmaya karşı koruma”, boyutları değişmemiş JPEG görüntülerinden veriyi kurtarmaya yönelik bir girişimdir; başarısı garanti değildir. Aynı şifreli zarfın bir kopyası PNG’nin düşük RGB bitlerinde bulunur. İsteğe bağlı ikinci kopya ise görüntünün frekans katsayılarına yerleştirilir.
Yöntem sıkıştırmayı engellemez ve biçimi tespit edilemez kılmaz.
envelope → RS(255,223)interleave → spread blocksQIM → luma DCT 8×8repair → CRC → AES-GCM
Piksellerden frekanslara
JPEG, 8×8 parlaklık bloklarına ayrık kosinüs dönüşümü (DCT) uygular ve elde edilen katsayıları nicemler. SnapSeal seçilmiş katsayıların değerlerini bilinçli aralıklarla yerleştirerek veri yazar. Böylece okuyucu, JPEG kodlamasının yol açtığı bazı değişimleri tolere edebilir.
Hata düzeltmeli bir taşıma başlığı kurtarma kanalının düzenini belirtir. Okuyucunun özgün kapağa ihtiyacı yoktur.
RGB → Y → DCT(8×8)
q = round(coefficient / JPEG_quantizer)
coefficient′ = q × JPEG_quantizerSimgeler arasında boşluk bırakmak
QIM, yani nicemleme indisi modülasyonu, simgeleri aralıklı konumlara atar. Tek bitlik öğretici modelde 24’ün katları sırayla 0 ve 1’i temsil eder. Kodlayıcı 1 saklamak için 70 değerini 72’ye taşır. 72’den başlayan +11’lik değişim hâlâ 1 olarak çözülür; +13 ise sınırı aşar ve biti tersine çevirir. Sınırın nerede olduğunu görmek için kaydırıcıyı hareket ettirin.
Gerçek kanal, Gray kodlamasıyla seçilen katsayı başına bir ila üç bit taşır. Python’daki QIM kod parçası bu ilkeyi gösteren bir modeldir; eksiksiz bir JPEG çözücüsü değildir.
from math import floor
def put_bit(coefficient: float, bit: int, step: int = 24) -> int:
cell = floor((coefficient / step - bit) / 2 + 0.5)
return step * (2 * cell + bit)
def read_bit(coefficient: float, step: int = 24) -> int:
return floor(coefficient / step + 0.5) % 2
stored = put_bit(70, 1) # 72
assert read_bit(stored + 11) == 1 # 83: same cell
assert read_bit(stored + 13) == 0 # 85: crossed the boundaryÖnce onarım, sonra kimlik doğrulama
Reed–Solomon (255,223), 223 veri baytına 32 eşlik baytı ekler ve kod sözcüğü başına en fazla 16 hatalı baytı düzeltebilir. Serpiştirme ve görüntüye yayılmış bloklar, hasarın birkaç kod sözcüğünde yoğunlaşmak yerine dağılmasına yardımcı olur.
Okuyucu önce taşıma verisini onarır, sonra CRC’yi kontrol eder ve son olarak zarfı AES-GCM ile doğrular. CRC, taşıma bütünlüğünü kontrol eder; kriptografik kimlik doğrulama değildir. Onarım başarılı olsa bile GCM kontrolü başarısızsa açık metin sunulmaz.
223 + 32 = 255 bytes
t = (255 − 223) / 2 = 16 byte errors
A₀ B₀ C₀ A₁ B₁ C₁ … → A, B, C
repair → CRC → AES-GCM → original_bytesBirleşik taşıyıcıyı sınamak
İki kopya aynı piksellerde bulunduğundan düşük bitlere yazmak DCT katsayılarını da etkiler. Bu yüzden testler, son hâlini almış birleşik görüntü üzerinde yapılır. Yerel tarayıcı testlerinde zarfın JPEG 0.60 kalitesinde ve art arda 0.85→0.60 kodlamasından sonra eksiksiz kurtarılması gerekir. Kod sözcüğü başına en fazla 12 bayt onarılmış olmalıdır; bu, 16 baytlık düzeltme sınırının altındadır. Bunlar yerel testlerdir, her JPEG kodlayıcısı için garanti değildir.
Otomatik denemelerin sınırı, ek yük dâhil 280.000 şifreli bayttır. Artıklık ve daha güçlü sinyaller kapak kalitesi ve kapasitesinden yer; gizli özgün dosya yeniden sıkıştırılmaz. Testler başarısız olursa kayıpsız PNG veya ZIP iletimine dönülür. Kırpma, yeniden boyutlandırma, döndürme veya başka bir kodlayıcı kurtarmayı engelleyebilir. Özgün PNG ya da ZIP’i dosya olarak göndermek daha güvenilir bir yoldur.
exact(PNG) ∧ exact(Q60) ∧ exact(Q85 → Q60)
max(corrected_bytes_per_codeword) ≤ 12 < 16
automatic: envelope_length ≤ 280_000Biçimin açıkta bıraktıkları
Sabit işaretler biçimin tespit edilmesini sağlar. Kapak görseli herkese açıktır ve bir görsel olarak kimliği doğrulanmaz; üst bitlerinin değiştirilmesi gömülü mesajı sağlam bırakabilir. Hata düzeltme ve CRC, taşıma sırasında oluşan hasarla ilgilenir; AES-GCM ise korunan içeriği doğrular.
Bir saldırgan yine de parolaları çevrimdışı deneyebilir. Argon2id her denemenin maliyetini artırır, ancak zayıf bir parolayı kurtaramaz. Parola kurtarma yolu yoktur; burada bağımsız bir güvenlik denetimi yapıldığı da iddia edilmiyor.
Uygulama olmadan okumak
Bu tasarım, yeni bir şifreleme yöntemi icat etmek yerine yerleşik algoritmaları, sınırları açık bir biçimi ve bağımsız bir okuyucuyu bir araya getiriyor. Python 3.12+ okuyucu, JPEG kurtarma kopyası içerenler dâhil özgün PNG’leri, boyutu sınırlandırılmış ve tek PNG içeren SnapSeal ZIP’lerini ve ham v3 zarflarını destekler. Dönüştürülmüş JPEG dosyaları için TypeScript kodeği gerekir.
Burada anlatılan uygulama açık kaynaklı v3 sürümüdür; daha önce kullanıma sunulmuş eski okuyucular dosyalarını reddedebilir. Listelenen bağımlılıkları bir sanal ortama kurun. Tam kaynak kod ve isteğe bağlı indirmeler, örneklerin ardından geliyor.
python -m pip install -r requirements.txtfrom getpass import getpass
from pathlib import Path
from decode_snapseal import decode_path
result = decode_path(
Path("protected.png"),
getpass("Password: "),
)
output = Path("restored")
output.mkdir(exist_ok=True)
with (output / result.filename).open("xb") as target:
target.write(result.data)

