రహస్యం వేరు, దాన్ని మోసే చిత్రం వేరు
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 ప్రొఫైల్లో Argon2id v1.3, UTF-8 పాస్వర్డ్తో పాటు యాదృచ్ఛిక 16-బైట్ సాల్ట్ను ఉపయోగించి 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 అనే discrete cosine transform ద్వారా ఫ్రీక్వెన్సీ గుణకాలుగా మార్చి, వాటిని క్వాంటైజ్ చేస్తుంది. 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లకు హామీ కాదు.
ఓవర్హెడ్తో సహా ఎన్క్రిప్ట్ చేసిన డేటా 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)

