01

Siri na picha ya jalada vina kazi tofauti

SnapSeal huanza na baiti asili za picha, faili au maandishi. Huzisimba fiche pamoja na metadata, kisha hupachika bahasha inayopatikana ndani ya picha ya jalada. Mpokeaji anahitaji faili lililolindwa na nenosiri, lakini hahitaji seva wala picha asili ya jalada.

Kriptografia hulinda usiri wa maudhui; steganografia hutoa njia ya kuyabeba. Mbinu zote mbili zimezoeleka. Changamoto ni kuweka wazi mipaka ya kila moja. Mchoro wa mfano huu na nenosiri la majaribio vimewekwa hadharani kwa makusudi.

  1. original_bytes + metadata
  2. Argon2id → AES-GCM
  3. header + ciphertext + tag
  4. RGB → PNG
password = "Public SnapSeal article example"
GitHub · snapseal-codec
02

Tumia biti mbili za chini

Kila pikseli ya RGB ina chaneli tatu, kila moja ikiwa na biti 8. Katika mfano huu, kisimbaji husoma chaneli kutoka kushoto kwenda kulia, huruka alpha, huhifadhi biti sita za juu na kubadilisha mbili za chini. Hivyo chaneli nne zinaweza kubeba baiti moja.

Sogeza kitelezi uone biti zilizoangaziwa zikibadilika. Uandishi huu wa biti za chini pekee hubadilisha thamani ya chaneli kwa kiwango kisichozidi 3; kibebaji cha hiari cha JPEG na mabadiliko mengine ya mwonekano yanaweza kubadilisha jalada zaidi.

R₀10101011170 → 171 Δ +1
G₀10110101181 → 181 Δ 0
B₀0101110094 → 92 Δ -2
R₁11001011203 → 203 Δ 0
def 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
03

Hesabu nafasi inayopatikana

Chaneli tatu zenye biti mbili kila moja hutoa biti sita kwa pikseli. Jalada la 1024×1024 lina nafasi ya baiti 786,432 kabla ya kichwa cha pakiti ya pikseli chenye baiti 16, na baiti 786,416 baada yake. Mchoro huhifadhi baadhi ya pikseli kwa matumizi yake, hivyo kupunguza uwezo huo.

Alama ya kudumu hutambulisha muundo wa pakiti, na urefu huonyesha mwisho wa bahasha. Matoleo ya pakiti na ya usimbaji fiche yana mifumo tofauti ya majina: pakiti ya PNG2 inaweza kubeba bahasha ya kriptografia ya v3.

C = floor(W × H × 3 × 2 / 8) − 16
C(1024, 1024) = 786,416 bytes
SNAPSEALPNG2
[0:12]SNAPSEALPNG212 bytes
[12:16]uint32_be(envelope_length)4 bytes
[16:…]envelopeenvelope_length
pixel-packet.tsstyled-packet.ts
04

Tengeneza ufunguo na uthibitishe bahasha

Wasifu wa v3 wa SnapSeal hutumia Argon2id v1.3 kutengeneza ufunguo wa baiti 32 kutoka kwenye nenosiri la UTF-8 na chumvi nasibu ya baiti 16. Hutumia kumbukumbu ya 64 MiB, hupitia data mara tatu na hutumia njia moja ya uchakataji. Kisomaji lazima kwanza kikague kuwa vigezo vya kazi ni vile vilivyowekwa na vinavyotumika, kabla ya kuendesha KDF.

AES-256-GCM husimba fiche baiti asili na metadata. Lebo yake ya baiti 16 huthibitisha data iliyosimbwa fiche pamoja na kichwa cha umma cha baiti 48. Uthibitishaji ukishindwa, kisomaji hakipaswi kamwe kutoa maudhui yaliyofumbuliwa.

SNAPSEAL v3 · 48 bytes
[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)
RFC 9106 · Argon2AESGCM · cryptography
05

Ujumbe uko kwenye pikseli

Data imo katika thamani za RGB zilizodekodiwa, si kwenye metadata ya PNG. Faili mbili za PNG zinaweza kuwa na baiti tofauti lakini zikahifadhi ujumbe uleule katika biti za chini, mradi thamani zao za RGB zilizodekodiwa hazijabadilika. Kupiga picha ya skrini, kuhariri pikseli au kubadili vipimo kunaweza kuharibu data hata kama picha bado inatambulika.

Isipokuwa mbano wa chanzo umechaguliwa waziwazi, urejeshaji hurudisha baiti asili na metadata kama zilivyokuwa. Jina la faili husimbwa fiche, lakini bila kujaza bahasha kwa data ya ziada, ukubwa wake wa kukadiria bado unaonekana.

PNG bytes A ≠ PNG bytes B
RGB(A) = RGB(B), α(A) = α(B) = 255
→ envelope(A) = envelope(B)
PNG · W3C
06

Kukabiliana na mbano: njia ya pili kupitia JPEG

Njia ya kukabiliana na mbano hujaribu kurejesha data kutoka JPEG yenye vipimo vilevile, bila kuhakikisha mafanikio. Bahasha ileile iliyosimbwa fiche ina nakala isiyopoteza data katika biti za chini za RGB za PNG na, kwa hiari, nakala ya urejeshaji katika vigawo vya masafa vya picha.

Njia hii haizuii mbano wala haihakikishi kuwa data itanusurika kwenye programu yoyote ya kutuma ujumbe. Pia haifanyi umbizo lisigundulike.

  1. envelope → RS(255,223)
  2. interleave → spread blocks
  3. QIM → luma DCT 8×8
  4. repair → CRC → AES-GCM

Kutoka pikseli hadi masafa

JPEG hutumia DCT, yaani discrete cosine transform, kubadilisha vitalu vya mwangaza vya 8×8 kuwa vigawo vya masafa, kisha huvifanyia ukwantishaji. SnapSeal hutumia vigawo vilivyochaguliwa kubeba data katika thamani zilizotenganishwa kwa nafasi maalumu. Nafasi hizo huruhusu kisomaji kuvumilia baadhi ya mabadiliko yanayotokana na usimbaji wa JPEG.

Kichwa cha usafirishaji chenye usahihishaji wa makosa hutambulisha mpangilio wa urejeshaji. Hakithibitishi mchoro wa umma wa jalada, na kisomaji hakihitaji jalada asili.

RGB → Y → DCT(8×8)
q = round(coefficient / JPEG_quantizer)
coefficient′ = q × JPEG_quantizer

Acha nafasi kati ya alama

QIM, au quantization-index modulation, huweka alama katika nafasi zilizotenganishwa. Katika mfano wa kufundishia wa biti moja, vizidisho vya 24 huwakilisha 0 na 1 kwa kupishana. Ili kuhifadhi 1, kisimbaji huhamisha 70 hadi 72. Mabadiliko ya +11 kutoka 72 bado husomwa kama 1; +13 huvuka mpaka na kubadilisha biti. Sogeza kitelezi uone mpaka huo.

Chaneli halisi huhifadhi biti moja hadi tatu kwa kila kigawo kilichochaguliwa, kwa kutumia usimbaji wa Gray. Kipande cha Python cha QIM kinaonyesha kanuni hiyo tu; si dekoda ya JPEG.

c′ = 72 + 0 = 72 b′ = ⌊c′ / 24 + ½⌋ mod 2 = 1 = b
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

Rekebisha kwanza, thibitisha mwisho

Reed–Solomon (255,223) huongeza baiti 32 za ulinganifu kwa baiti 223 za data na inaweza kusahihisha hadi baiti 16 zenye makosa katika kila neno la msimbo. Kuchanganya mpangilio wa data na kusambaza vitalu katika picha husaidia kutawanya uharibifu badala ya kuuelekeza kwenye maneno machache ya msimbo.

Kisomaji hurekebisha data ya usafirishaji, hukagua CRC yake, kisha huthibitisha bahasha kwa AES-GCM. CRC hukagua uadilifu wa usafirishaji; si uthibitishaji wa kriptografia. Hata marekebisho yakifaulu, maudhui yaliyofumbuliwa hayatolewi ikiwa ukaguzi wa GCM umeshindwa.

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

Jaribu kibebaji cha mwisho kwa ujumla

Nakala zote mbili zimo katika pikseli zilezile, hivyo kuandika biti za chini pia hubadilisha vigawo vya DCT. Ndiyo maana majaribio hutumia picha ya mwisho yenye nakala zote mbili. Katika majaribio ya ndani ya JPEG kwenye kivinjari, bahasha ilirejeshwa sawasawa baada ya usimbaji kwa ubora wa 0.60 na baada ya usimbaji unaorudiwa wa 0.85→0.60. Baiti zilizorekebishwa hazikuzidi 12 kwa kila neno la msimbo, chini ya kikomo cha 16. Matokeo haya hayahakikishi mafanikio kwa visimbaji vingine vya JPEG.

Majaribio ya kiotomatiki huishia kwenye baiti 280,000 zilizosimbwa fiche, ikijumuisha data ya ziada ya muundo. Nakala za ziada na mawimbi yenye nguvu zaidi hutumia uwezo na kupunguza ubora wa jalada; havibani upya faili asili ya faragha. Majaribio yakishindwa, hutumika PNG au ZIP isiyopoteza data. Kukata mipaka, kubadili vipimo, kuzungusha au kutumia kisimbaji kingine kunaweza kuzuia urejeshaji. Njia ya kuaminika ni kutuma PNG au ZIP asili kama faili.

exact(PNG) ∧ exact(Q60) ∧ exact(Q85 → Q60)
max(corrected_bytes_per_codeword) ≤ 12 < 16
automatic: envelope_length ≤ 280_000
jpeg-channel.tspng-container.tsJPEG · ITU-T T.81QIM · Chen & Wornell (2001)
07

Umbizo linafichua nini?

Alama za kudumu hufanya umbizo litambulike. Mchoro wa jalada ni wa umma na hauthibitishwi kama mchoro; mabadiliko ya biti zake za juu yanaweza kuacha ujumbe uliopachikwa ukiwa salama. Usahihishaji wa makosa na CRC hushughulikia uharibifu wa usafirishaji, huku AES-GCM ikithibitisha maudhui yaliyolindwa.

Mshambuliaji bado anaweza kubashiri manenosiri bila mtandao. Argon2id huongeza gharama ya kila jaribio, lakini haiwezi kuokoa nenosiri dhaifu. Hakuna njia ya kurejesha nenosiri, wala hakuna dai la ukaguzi huru wa usalama.

08

Soma data bila programu ya SnapSeal

Kisomaji huru cha Python 3.12+ husoma PNG asili, ikiwemo PNG yenye nakala ya urejeshaji wa JPEG, ZIP za SnapSeal zenye PNG moja ndani ya mipaka inayotumika, pamoja na bahasha ghafi za v3. Faili zilizobadilishwa kuwa JPEG zinahitaji kodeki ya TypeScript.

Umbizo linaloelezwa hapa ni utekelezaji wa v3 wa chanzo huria; visomaji vya zamani vilivyosambazwa vinaweza kukataa faili zake. Hutumia mbinu za kriptografia zilizopo, umbizo lenye mipaka iliyoelezwa na kisomaji huru. Sakinisha vitegemezi vilivyoorodheshwa katika mazingira pepe. Msimbo kamili na vipakuliwa vya hiari vinapatikana baada ya mifano iliyo kwenye makala.

python -m pip install -r requirements.txt
from 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)
GitHub · decode_snapseal.pyFORMAT.md
↓ decode_snapseal.py↓ requirements.txt