രഹസ്യവും കവർ ചിത്രവും
SnapSeal ചിത്രങ്ങളുടെയോ ഫയലുകളുടെയോ ടെക്സ്റ്റിന്റെയോ യഥാർഥ ബൈറ്റുകളിൽ നിന്നാണ് തുടങ്ങുന്നത്. കോഡെക് അവയും മെറ്റാഡാറ്റയും എൻക്രിപ്റ്റ് ചെയ്ത ശേഷം ലഭിക്കുന്ന എൻവലപ് ഒരു കവർ ചിത്രത്തിൽ ഉൾച്ചേർക്കുന്നു. സ്വീകർത്താവിന് സംരക്ഷിത ഫയലും പാസ്വേഡും വേണം; യഥാർഥ കവർ ചിത്രമോ സെർവറോ ആവശ്യമില്ല.
ക്രിപ്റ്റോഗ്രഫി ഉള്ളടക്കത്തിന്റെ രഹസ്യത സംരക്ഷിക്കുന്നു; സ്റ്റേഗനോഗ്രഫി അതിന് ഒരു വാഹകം നൽകുന്നു. രണ്ടും പഴയ ആശയങ്ങളാണ്; അവയുടെ ചുമതലകൾ വ്യക്തമായി വേർതിരിക്കുകയാണ് പ്രധാനം. ഡെമോയിലെ ചിത്രവും പരീക്ഷണ പാസ്വേഡും മനഃപൂർവം പ്രസിദ്ധീകരിച്ചവയാണ്.
original_bytes + metadataArgon2id → AES-GCMheader + ciphertext + tagRGB → PNG
password = "Public SnapSeal article example"താഴത്തെ രണ്ട് ബിറ്റുകൾ
ഓരോ RGB പിക്സലിനും 8 ബിറ്റ് വീതമുള്ള മൂന്ന് ചാനലുകളുണ്ട്. ഈ ഉദാഹരണത്തിൽ എൻകോഡർ ഇടത്തുനിന്ന് വലത്തോട്ട് ചാനലുകൾ വായിക്കുന്നു; ആൽഫ ഒഴിവാക്കി, മുകളിലെ ആറ് ബിറ്റുകൾ നിലനിർത്തി താഴത്തെ രണ്ട് ബിറ്റുകൾ മാറ്റുന്നു. അങ്ങനെ നാല് ചാനലുകൾക്ക് ഒരു ബൈറ്റ് വഹിക്കാം.
അടയാളപ്പെടുത്തിയ ബിറ്റുകൾ മാറുന്നത് കാണാൻ സ്ലൈഡർ നീക്കൂ. ഈ ലോ-ബിറ്റ് എഴുത്ത് മാത്രം ഒരു ചാനലിന്റെ മൂല്യം പരമാവധി 3 മാറ്റും. ഐച്ഛിക JPEG വാഹകവും മറ്റ് സ്റ്റൈലിങ്ങും കവർ ചിത്രത്തെ അതിലേറെ മാറ്റാം.
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) == 0xD3എത്ര ബൈറ്റുകൾക്ക് ഇടമുണ്ട്?
മൂന്ന് ചാനലുകളിൽ രണ്ട് ബിറ്റ് വീതം ഉപയോഗിച്ചാൽ ഒരു പിക്സലിൽ ആറ് ബിറ്റ് സൂക്ഷിക്കാം. 1024×1024 കവർ ചിത്രത്തിൽ 16-ബൈറ്റ് പിക്സൽ പാക്കറ്റ് ഹെഡറിനു മുമ്പ് 786,432 ബൈറ്റിന് ഇടമുണ്ട്; ഹെഡറിനു ശേഷം 786,416 ബൈറ്റ് ശേഷിക്കും. ആർട്ട്വർക്കിനായി പിക്സലുകൾ മാറ്റിവയ്ക്കുന്നത് ശേഷി കുറയ്ക്കും.
ഒരു സ്ഥിര മാർക്കർ പാക്കറ്റ് വിന്യാസം തിരിച്ചറിയിക്കുന്നു; എൻവലപ് എവിടെ അവസാനിക്കുന്നുവെന്ന് നീളം സൂചിപ്പിക്കുന്നു. പാക്കറ്റിന്റെയും എൻക്രിപ്ഷന്റെയും പതിപ്പുകൾ വ്യത്യസ്ത നെയിംസ്പേസുകളിലാണ്: ഒരു PNG2 പാക്കറ്റിന് v3 ക്രിപ്റ്റോഗ്രഫിക് എൻവലപ് വഹിക്കാം.
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 |
കീ നിർമിച്ച് എൻവലപ് പ്രാമാണീകരിക്കുക
SnapSeal-ന്റെ v3 പ്രൊഫൈൽ UTF-8 പാസ്വേഡിൽനിന്നും ക്രമരഹിതമായ 16-ബൈറ്റ് സോൾട്ടിൽനിന്നും Argon2id v1.3 ഉപയോഗിച്ച് 32-ബൈറ്റ് കീ നിർമിക്കുന്നു. ഇതിന് 64 MiB മെമ്മറിയും മൂന്ന് പാസുകളും ഒരു ലെയ്നും ഉപയോഗിക്കുന്നു. KDF പ്രവർത്തിപ്പിക്കുന്നതിനു മുമ്പ്, വർക്ക് ഫാക്ടറുകൾ പിന്തുണയ്ക്കുന്ന സ്ഥിര മൂല്യങ്ങളാണെന്ന് റീഡർ പരിശോധിക്കണം.
AES-256-GCM യഥാർഥ ബൈറ്റുകളും മെറ്റാഡാറ്റയും എൻക്രിപ്റ്റ് ചെയ്യുന്നു. അതിന്റെ 16-ബൈറ്റ് ടാഗ് സൈഫർടെക്സ്റ്റിനെയും പൊതുവായ 48-ബൈറ്റ് ഹെഡറിനെയും പ്രാമാണീകരിക്കുന്നു. പ്രാമാണീകരണം പരാജയപ്പെട്ടാൽ റീഡർ ഒരിക്കലും പ്ലെയിൻടെക്സ്റ്റ് പുറത്തുവിടരുത്.
[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)സന്ദേശം പിക്സലുകളിലാണ്
ഉള്ളടക്കം ഡീകോഡ് ചെയ്ത RGB മൂല്യങ്ങളിലാണ്, PNG മെറ്റാഡാറ്റയിലല്ല. രണ്ട് PNG ഫയലുകളുടെ ബൈറ്റുകൾ വ്യത്യസ്തമായാലും ഡീകോഡ് ചെയ്ത RGB മൂല്യങ്ങൾ മാറിയിട്ടില്ലെങ്കിൽ അവയിലെ ലോ-ബിറ്റ് സന്ദേശം അതേപടി നിലനിൽക്കും. എന്നാൽ സ്ക്രീൻഷോട്ട് എടുക്കുകയോ പിക്സലുകൾ തിരുത്തുകയോ വലിപ്പം മാറ്റുകയോ ചെയ്താൽ ചിത്രം തിരിച്ചറിയാവുന്നതായി തുടർന്നാലും ഡാറ്റ നശിച്ചേക്കാം.
ഉറവിട കംപ്രഷൻ വ്യക്തമായി തിരഞ്ഞെടുത്തിട്ടില്ലെങ്കിൽ യഥാർഥ ബൈറ്റുകളും മെറ്റാഡാറ്റയും കൃത്യമായി തിരിച്ചുകിട്ടും. ഫയൽനാമം എൻക്രിപ്റ്റ് ചെയ്തിരിക്കും; എങ്കിലും പാഡിങ് ഇല്ലെങ്കിൽ എൻവലപ് ഏകദേശ വലിപ്പം വെളിപ്പെടുത്തും.
PNG bytes A ≠ PNG bytes B
RGB(A) = RGB(B), α(A) = α(B) = 255
→ envelope(A) = envelope(B)ആന്റി-കംപ്രഷൻ: JPEGയിലൂടെ മറ്റൊരു വഴി
ആന്റി-കംപ്രഷൻ എന്നത് അതേ അളവുകൾ നിലനിർത്തുന്ന JPEGയിൽനിന്ന് ഡാറ്റ വീണ്ടെടുക്കാനുള്ള ശ്രമമാണ്. ഒരേ എൻക്രിപ്റ്റ് ചെയ്ത എൻവലപ് PNGയിലെ താഴ്ന്ന RGB ബിറ്റുകളിൽ സൂക്ഷിക്കുന്നു; ഐച്ഛികമായി ചിത്രത്തിന്റെ ഫ്രീക്വൻസി ഗുണകങ്ങളിൽ വീണ്ടെടുക്കലിനുള്ള പകർപ്പും ചേർക്കാം. ഇത് കംപ്രഷൻ തടയുകയോ ഫോർമാറ്റ് കണ്ടെത്താനാവാത്തതാക്കുകയോ ചെയ്യുന്നില്ല; വീണ്ടെടുക്കലിന് ഉറപ്പുമില്ല.
envelope → RS(255,223)interleave → spread blocksQIM → luma DCT 8×8repair → CRC → AES-GCM
പിക്സലുകളിൽനിന്ന് ഫ്രീക്വൻസികളിലേക്ക്
JPEG, 8×8 ലൂമിനൻസ് ബ്ലോക്കുകളിൽ ഡിസ്ക്രീറ്റ് കോസൈൻ ട്രാൻസ്ഫോം (DCT) പ്രയോഗിക്കുന്നു. ഇത് പിക്സൽ മൂല്യങ്ങളെ ഫ്രീക്വൻസി ഗുണകങ്ങളാക്കി മാറ്റുന്നു; തുടർന്ന് അവ ക്വാണ്ടൈസ് ചെയ്യുന്നു. തിരഞ്ഞെടുത്ത ഗുണകങ്ങളിൽ ഡാറ്റ വഹിക്കാൻ SnapSeal മനഃപൂർവം ഇടവിട്ട മൂല്യങ്ങൾ ഉപയോഗിക്കുന്നു. JPEG എൻകോഡിങ് മൂലം ചില മാറ്റങ്ങൾ വന്നാലും വായിച്ചെടുക്കാൻ ഈ ഇടവേള സഹായിക്കുന്നു.
പിശകുതിരുത്തൽ സൗകര്യമുള്ള ട്രാൻസ്പോർട്ട് ഹെഡർ വീണ്ടെടുക്കൽ വിന്യാസം തിരിച്ചറിയിക്കുന്നു. അത് പൊതുവായ കവർ ആർട്ട്വർക്കിനെ പ്രാമാണീകരിക്കുന്നില്ല; റീഡറിന് യഥാർഥ കവർ ആവശ്യമില്ല.
RGB → Y → DCT(8×8)
q = round(coefficient / JPEG_quantizer)
coefficient′ = q × JPEG_quantizerചിഹ്നങ്ങൾക്കിടയിൽ ഇടം നൽകുക
QIM അഥവാ ക്വാണ്ടൈസേഷൻ-ഇൻഡക്സ് മോഡുലേഷൻ ചിഹ്നങ്ങൾക്ക് ഇടവിട്ട സ്ഥാനങ്ങൾ നൽകുന്നു. ഒരു ബിറ്റ് ഉപയോഗിച്ചുള്ള പഠന മാതൃകയിൽ 24ന്റെ ഗുണിതങ്ങൾ മാറിമാറി 0യും 1ഉം സൂചിപ്പിക്കുന്നു. 1 സൂക്ഷിക്കാൻ എൻകോഡർ 70നെ 72 ആക്കുന്നു. 72ൽനിന്ന് +11 മാറിയാലും ഫലം 1 ആയി വായിക്കാം; +13 ആയാൽ അതിരു കടന്ന് ബിറ്റ് മാറും. ആ അതിരു കാണാൻ സ്ലൈഡർ നീക്കൂ.
യഥാർഥ ചാനൽ തിരഞ്ഞെടുത്ത ഓരോ ഗുണകത്തിലും Gray coding ഉപയോഗിച്ച് ഒന്നു മുതൽ മൂന്ന് വരെ ബിറ്റുകൾ സൂക്ഷിക്കുന്നു. Python QIM സ്നിപ്പറ്റ് ഈ തത്ത്വം കാണിക്കുന്ന മാതൃക മാത്രമാണ്; പൂർണമായ JPEG ഡീകോഡറല്ല.
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ആദ്യം തിരുത്തൽ, ഒടുവിൽ പ്രാമാണീകരണം
Reed–Solomon (255,223) 223 ഡാറ്റ ബൈറ്റുകൾക്ക് 32 പാരിറ്റി ബൈറ്റുകൾ ചേർക്കുന്നു. ഓരോ കോഡ്വേഡിലും പരമാവധി 16 തെറ്റായ ബൈറ്റുകൾ തിരുത്താൻ ഇതിന് കഴിയും. ഇന്റർലീവിങ്ങും ചിത്രത്തിലുടനീളം ചിതറിച്ച ബ്ലോക്കുകളും കേടുപാടുകൾ ഏതാനും കോഡ്വേഡുകളിൽ കേന്ദ്രീകരിക്കാതെ വിതരണം ചെയ്യാൻ സഹായിക്കുന്നു.
റീഡർ ആദ്യം ട്രാൻസ്പോർട്ട് ഡാറ്റ തിരുത്തി CRC പരിശോധിക്കുന്നു; തുടർന്ന് AES-GCM ഉപയോഗിച്ച് എൻവലപ് പ്രാമാണീകരിക്കുന്നു. CRC ട്രാൻസ്പോർട്ട് പരിശോധനയാണ്, ക്രിപ്റ്റോഗ്രഫിക് പ്രാമാണീകരണമല്ല. തിരുത്തൽ വിജയിച്ചാലും GCM പരിശോധന പരാജയപ്പെട്ടാൽ പ്ലെയിൻടെക്സ്റ്റ് പുറത്തുവിടരുത്.
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_bytesരണ്ട് വാഹകങ്ങളും ചേർത്ത ശേഷം പരിശോധന
രണ്ട് പകർപ്പുകളും ഒരേ പിക്സലുകളിലാണ്; അതിനാൽ ലോ-ബിറ്റ് എഴുത്ത് DCT ഗുണകങ്ങളെയും മാറ്റുന്നു. സ്വയം പരിശോധനകൾ നടത്തുന്നത് രണ്ടും ചേർത്ത അന്തിമ വാഹകത്തിലാണ്. ബ്രൗസറിൽ JPEG quality 0.60 ഉപയോഗിച്ചുള്ള പരിശോധനയിലും തുടർച്ചയായ 0.85→0.60 എൻകോഡിങ്ങിലും എൻവലപ് കൃത്യമായി വീണ്ടെടുക്കുമ്പോൾ മാത്രമാണ് വീണ്ടെടുക്കൽ ലഭ്യമെന്ന് അറിയിക്കുന്നത്. ഓരോ കോഡ്വേഡിലും പരമാവധി 12 ബൈറ്റ് മാത്രമേ തിരുത്താവൂ; ഇത് 16-ബൈറ്റ് തിരുത്തൽ പരിധിയിലും താഴെയാണ്. ഈ quality ക്രമീകരണങ്ങൾ പ്രാദേശിക പരിശോധനകളാണ്, സാർവത്രിക JPEG ഉറപ്പുകളല്ല.
ഓവർഹെഡ് ഉൾപ്പെടെ 280,000 എൻക്രിപ്റ്റ് ചെയ്ത ബൈറ്റുകൾക്കു മുകളിൽ സ്വയമേവയുള്ള ശ്രമങ്ങൾ തുടരില്ല. ആവർത്തന ഡാറ്റയും ശക്തമായ സിഗ്നലുകളും കവർ ചിത്രത്തിന്റെ ഗുണനിലവാരവും ശേഷിയും ചെലവഴിക്കുന്നു; സ്വകാര്യ ഒറിജിനലിനെ വീണ്ടും കംപ്രസ് ചെയ്യുന്നില്ല. പരിശോധന പരാജയപ്പെട്ടാൽ നഷ്ടരഹിത PNG അല്ലെങ്കിൽ ZIP ഉപയോഗിക്കുന്നു. ക്രോപ്പിങ്, വലിപ്പം മാറ്റൽ, തിരിക്കൽ, മറ്റ് എൻകോഡറുകൾ എന്നിവ വീണ്ടെടുക്കൽ തടഞ്ഞേക്കാം. അതിനാൽ യഥാർഥ PNG അല്ലെങ്കിൽ ZIP ഫയലായി അയയ്ക്കുന്നതാണ് വിശ്വസനീയമായ മാർഗം.
exact(PNG) ∧ exact(Q60) ∧ exact(Q85 → Q60)
max(corrected_bytes_per_codeword) ≤ 12 < 16
automatic: envelope_length ≤ 280_000ഫോർമാറ്റ് വെളിപ്പെടുത്തുന്നത്
സ്ഥിര മാർക്കറുകൾ കാരണം ഈ ഫോർമാറ്റ് കണ്ടെത്താനാകും. കവർ ആർട്ട്വർക്ക് പൊതുവായതാണ്; ചിത്രം എന്ന നിലയിൽ അത് പ്രാമാണീകരിക്കപ്പെടുന്നില്ല. അതിന്റെ മുകളിലെ ബിറ്റുകൾ മാറിയാലും ഉള്ളിലെ സന്ദേശം നിലനിൽക്കാം. പിശകുതിരുത്തലും CRCയും കൈമാറ്റത്തിനിടയിലെ കേടുപാടുകൾ കൈകാര്യം ചെയ്യുന്നു; AES-GCM സംരക്ഷിത ഉള്ളടക്കം പ്രാമാണീകരിക്കുന്നു.
ആക്രമിക്ക് പാസ്വേഡുകൾ ഓഫ്ലൈനായി ഊഹിച്ചുനോക്കാൻ കഴിയും. Argon2id ഓരോ ശ്രമത്തിന്റെയും ചെലവ് കൂട്ടുമെങ്കിലും ദുർബലമായ പാസ്വേഡിനെ രക്ഷിക്കില്ല. പാസ്വേഡ് വീണ്ടെടുക്കാൻ വഴിയില്ല; സ്വതന്ത്ര സുരക്ഷാ ഓഡിറ്റ് നടത്തിയെന്ന അവകാശവാദവുമില്ല.
സ്വതന്ത്ര റീഡർ
അറിയപ്പെടുന്ന ക്രിപ്റ്റോഗ്രഫിക് പ്രിമിറ്റീവുകൾ ഉപയോഗിക്കുന്ന ഈ ഫോർമാറ്റ് അതിന്റെ പരിധികൾ വ്യക്തമാക്കുകയും സ്വതന്ത്ര റീഡർ നൽകുകയും ചെയ്യുന്നു. Python 3.12+ റീഡർ യഥാർഥ PNGകൾ വായിക്കും; JPEG വീണ്ടെടുക്കൽ പകർപ്പുള്ള PNGകളും ഇതിൽപ്പെടുന്നു. നിശ്ചിത വലിപ്പപരിധിക്കുള്ളിൽ ഒരു PNG മാത്രമുള്ള SnapSeal ZIPകളും raw v3 എൻവലപ്പുകളും അത് പിന്തുണയ്ക്കുന്നു. JPEGയിലേക്ക് മാറ്റിയ ഫയലുകൾക്ക് TypeScript കോഡെക് വേണം.
ഇവിടെ വിവരിക്കുന്നത് ഓപ്പൺ സോഴ്സ് v3 ഇംപ്ലിമെന്റേഷനാണ്; മുമ്പ് വിന്യസിച്ച പഴയ റീഡറുകൾ അതിന്റെ ഫയലുകൾ നിരസിച്ചേക്കാം. ലിസ്റ്റ് ചെയ്ത ഡിപൻഡൻസികൾ ഒരു വെർച്വൽ എൻവയോൺമെന്റിൽ ഇൻസ്റ്റാൾ ചെയ്യുക. ഉദാഹരണങ്ങൾക്കു ശേഷം പൂർണ സോഴ്സും ഐച്ഛിക ഡൗൺലോഡുകളും ലഭ്യമാണ്.
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)

