રહસ્ય અને કવરનાં જુદાં કામ
SnapSeal છબીઓ, ફાઇલો અથવા લખાણના મૂળ બાઇટ અને તેમનો મેટાડેટા એન્ક્રિપ્ટ કરે છે. પછી તે બનેલો એન્વલપ કવર છબીમાં મૂકે છે. સુરક્ષિત ફાઇલ ડિકોડ કરવા પ્રાપ્તકર્તાને પાસવર્ડ જોઈએ, પણ સર્વર કે મૂળ કવરની જરૂર નથી.
ક્રિપ્ટોગ્રાફી સામગ્રી ગુપ્ત રાખે છે; છબી સ્ટેગનોગ્રાફી તેને લઈ જવા માટે માધ્યમ આપે છે. બંને જાણીતી પદ્ધતિઓ છે. જાહેર ડેમોનું ચિત્ર અને તેનો ટેસ્ટ પાસવર્ડ ઇરાદાપૂર્વક પ્રકાશિત કરેલા છે.
original_bytes + metadataArgon2id → AES-GCMheader + ciphertext + tagRGB → PNG
password = "Public SnapSeal article example"નીચલા બે બિટમાં લખાણ
દરેક RGB પિક્સેલમાં ત્રણ 8-બિટ ચેનલ હોય છે. આ ઉદાહરણમાં એન્કોડર ચેનલ ડાબેથી જમણે વાંચે છે, alpha છોડે છે, ઉપરના છ બિટ જાળવે છે અને નીચલા બે બિટ બદલે છે. આમ ચાર ચેનલમાં એક બાઇટ સમાય છે.
હાઇલાઇટ કરેલા બિટ કેવી રીતે બદલાય છે તે જોવા સ્લાઇડર ખસેડો. ફક્ત આ નીચલા બિટ લખવાની ક્રિયામાં ચેનલનું મૂલ્ય વધુમાં વધુ 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-બાઇટ પિક્સેલ-પેકેટ હેડર પહેલાં 7,86,432 બાઇટની જગ્યા મળે છે; હેડર પછી 7,86,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-બાઇટ સોલ્ટમાંથી 32-બાઇટ કી બનાવવા Argon2id v1.3 વાપરે છે. તેમાં 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 ડિસ્ક્રીટ કોસાઇન ટ્રાન્સફોર્મ (DCT) વડે તેજસ્વિતાના 8×8 બ્લોકને ફ્રીક્વન્સી ગુણાંકોમાં ફેરવે છે અને પછી તેમનું ક્વૉન્ટાઇઝેશન કરે છે. 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 પરીક્ષણમાં ગુણવત્તા 0.60 પર અને સતત 0.85→0.60 એન્કોડ પછી ચોક્કસ એ જ એન્વલપ પાછો મળવો જોઈએ. દરેક કોડવર્ડમાં વધુમાં વધુ 12 બાઇટ સુધારવા પડવા જોઈએ, જે 16-બાઇટની સુધારણા મર્યાદાથી નીચે છે. આ સ્થાનિક પરીક્ષણો છે, બધા JPEG એન્કોડર માટેની ખાતરી નહીં.
ઓવરહેડ સહિત 2,80,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)

