01

ரகசியமும் அதைச் சுமக்கும் படமும்

SnapSeal படங்கள், கோப்புகள் அல்லது உரையின் அசல் பைட்டுகளையும் அவற்றின் மெட்டாடேட்டாவையும் மறையாக்கி, கிடைக்கும் உறையை ஒரு படத்தில் பதிக்கிறது. உள்ளடக்கத்தை மீட்கப் பாதுகாக்கப்பட்ட கோப்பும் கடவுச்சொல்லும் போதும்; சேவையகமோ அசல் பின்னணிப் படமோ தேவையில்லை.

மறையாக்கவியல் உள்ளடக்கத்தின் ரகசியத்தைக் காக்கிறது; படத்தில் தரவை மறைத்தல் அதைச் சுமப்பதற்கான வழியைத் தருகிறது. இரண்டுமே அறியப்பட்ட கருத்துகள். இந்த எடுத்துக்காட்டில் பயன்படுத்தப்படும் ஓவியமும் சோதனைக் கடவுச்சொல்லும் வேண்டுமென்றே வெளியிடப்பட்டவை.

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

கீழ் இரண்டு பிட்களில் எழுதுவது

ஒவ்வொரு RGB பிக்சலிலும் மூன்று 8-பிட் சேனல்கள் உள்ளன. இந்த எடுத்துக்காட்டில், இடமிருந்து வலமாகச் சேனல்களைப் படித்து, alpha-ஐத் தவிர்த்து, மேல் ஆறு பிட்களை அப்படியே வைத்துக்கொண்டு கீழ் இரண்டு பிட்களை மாற்றுகிறோம். எனவே நான்கு சேனல்களில் ஒரு பைட்டைச் சேமிக்கலாம்.

நகர்த்தியை நகர்த்தினால் தேர்ந்தெடுக்கப்பட்ட பிட்கள் மாறுவதைப் பார்க்கலாம். இந்தக் கீழ் பிட் எழுத்து மட்டுமே ஒரு சேனலின் மதிப்பை அதிகபட்சம் 3 மாற்றும். விருப்பமான JPEG தாங்கியும் பிற தோற்ற மாற்றங்களும் படத்தை அதைவிட அதிகமாக மாற்றலாம்.

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

எத்தனை பைட்டுகளுக்கு இடமுண்டு?

மூன்று சேனல்களில் தலா இரண்டு பிட் என்றால், ஒரு பிக்சலுக்கு ஆறு பிட். 1024×1024 பின்னணியில் 16-பைட் pixel-packet header-க்கு முன் 786,432 பைட் இடம் உள்ளது; அதற்குப் பின் 786,416 பைட் மீதமிருக்கும். ஓவியத்துக்காகப் பிக்சல்கள் ஒதுக்கப்படுவதால் கொள்ளளவு குறையும்.

ஒரு நிலையான அடையாளக் குறி packet அமைப்பைக் காட்டுகிறது; நீள மதிப்பு உறையின் முடிவைக் குறிக்கிறது. Packet பதிப்புகளும் மறையாக்கப் பதிப்புகளும் வெவ்வேறு பெயரிடல் வரிசைகளைச் சேர்ந்தவை: PNG2 packet-ல் 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

விசை உருவாக்கமும் உறையின் அங்கீகாரமும்

SnapSeal-ன் v3 அமைப்பு, UTF-8 கடவுச்சொல் மற்றும் சீரற்ற 16-பைட் salt ஆகியவற்றிலிருந்து Argon2id v1.3 மூலம் 32-பைட் விசையை உருவாக்குகிறது. இதற்கு 64 MiB நினைவகம், மூன்று சுற்றுகள், ஒரு lane பயன்படுத்தப்படுகின்றன. KDF-ஐ இயக்கும் முன், அதில் குறிப்பிடப்பட்ட வேலை அளவுருக்கள் ஆதரிக்கப்படும் நிலையான மதிப்புகள்தாமா என்பதை வாசிப்பான் சரிபார்க்க வேண்டும்.

AES-256-GCM அசல் பைட்டுகளையும் மெட்டாடேட்டாவையும் மறையாக்குகிறது. அதன் 16-பைட் tag, மறையுரையையும் பொதுவாகத் தெரியும் 48-பைட் header-ஐயும் அங்கீகரிக்கிறது. அங்கீகாரம் தோல்வியுற்றால், வாசிப்பான் எந்தச் சூழலிலும் தெளிவுரையை வெளியிடக் கூடாது.

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

செய்தியைச் சுமப்பவை பிக்சல்களே

தரவு PNG மெட்டாடேட்டாவில் இல்லை; decode செய்யப்பட்ட RGB மதிப்புகளில் உள்ளது. இரண்டு PNG கோப்புகளின் பைட்டுகள் வேறுபட்டாலும், decode செய்யப்பட்ட RGB மதிப்புகள் மாறாமல் இருந்தால் கீழ் பிட்களில் உள்ள செய்தியும் மாறாது. ஆனால் திரைப்பிடிப்பு, பிக்சல் திருத்தம் அல்லது அளவுமாற்றம் செய்தியை அழிக்கலாம்; அப்போதும் படம் பார்ப்பதற்கு அடையாளம் தெரியும் நிலையில் இருக்கலாம்.

மூலத் தரவைச் சுருக்கும் விருப்பத்தை வெளிப்படையாகத் தேர்ந்தெடுக்காத வரை, மீட்டெடுப்பு அசல் பைட்டுகளையும் மெட்டாடேட்டாவையும் துல்லியமாகத் திருப்பித் தரும். கோப்புப் பெயர் மறையாக்கப்பட்டிருக்கும்; எனினும் padding இல்லாதபோது உறையின் தோராயமான அளவு தெரியவரும்.

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

சுருக்கத்திற்குப் பின் மீட்பு: JPEG வழியாக இரண்டாம் பாதை

சுருக்க எதிர்ப்பு என்பது அதே பரிமாணங்களைத் தக்கவைத்த JPEG படங்களிலிருந்து மீட்டெடுக்க மேற்கொள்ளப்படும் உத்தரவாதமற்ற முயற்சி. ஒரே மறையாக்க உறையின் இழப்பற்ற நகல் PNG-யின் கீழ் RGB பிட்களில் இருக்கும்; விரும்பினால், மற்றொரு மீட்பு நகல் படத்தின் அதிர்வெண் குணகங்களிலும் இடப்படும்.

இது சுருக்கத்தைத் தடுக்காது. எந்த messaging சேவை வழியாக அனுப்பினாலும் தரவு தப்பிப் பிழைக்கும் என்றும் உறுதியளிக்காது. வடிவத்தைக் கண்டறிய முடியாததாகவும் ஆக்காது.

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

பிக்சல்களிலிருந்து அதிர்வெண்களுக்கு

JPEG, 8×8 ஒளிர்வுத் தொகுதிகளை DCT எனப்படும் discrete cosine transform மூலம் அதிர்வெண் குணகங்களாக மாற்றி, பின்னர் அவற்றை quantize செய்கிறது. SnapSeal தேர்ந்தெடுத்த குணகங்களில் தரவை வேண்டுமென்றே இடைவெளியுள்ள மதிப்புகளில் பதிக்கிறது. இதனால் JPEG encoding ஏற்படுத்தும் சில மாற்றங்களைத் தாங்க வாசிப்பானுக்கு இடம் கிடைக்கிறது.

பிழைதிருத்த வசதியுள்ள transport header, மீட்பு அமைப்பை அடையாளப்படுத்துகிறது. அது பொதுவாகக் கிடைக்கும் பின்னணி ஓவியத்தை அங்கீகரிப்பதில்லை; வாசிப்பானுக்கு அசல் பின்னணிப் படமும் தேவையில்லை.

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

குறியீடுகளுக்கு இடையே இடைவெளி

QIM அல்லது quantization-index modulation, குறியீடுகளை இடைவெளியுள்ள நிலைகளுடன் இணைக்கிறது. ஒரு பிட் கற்பித்தல் மாதிரியில் 24-ன் மடங்குகள் மாறிமாறி 0, 1 ஆகியவற்றைக் குறிக்கின்றன. 1-ஐச் சேமிக்க 70 என்பது 72 ஆக மாற்றப்படுகிறது. 72-லிருந்து +11 மாறினாலும் 1 என்றே வாசிக்கப்படும்; +13 எல்லையைக் கடந்து பிட்டைப் புரட்டும். அந்த எல்லையைக் காண நகர்த்தியை நகர்த்திப் பாருங்கள்.

உண்மையான சேனல், தேர்ந்தெடுக்கப்பட்ட ஒவ்வொரு குணகத்திலும் Gray coding மூலம் ஒன்று முதல் மூன்று பிட்களைச் சேமிக்கிறது. Python QIM துணுக்கு இந்தக் கொள்கையை விளக்குவது மட்டுமே; அது முழுமையான JPEG decoder அல்ல.

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

முதலில் திருத்தம், இறுதியில் அங்கீகாரம்

Reed–Solomon (255,223), 223 தரவு பைட்டுகளுடன் 32 parity பைட்டுகளைச் சேர்க்கிறது. ஒவ்வொரு codeword-லிருந்தும் அதிகபட்சம் 16 தவறான பைட்டுகளைத் திருத்த முடியும். Interleaving-உம் படம் முழுவதும் பரவிய தொகுதிகளும் சேதம் சில codeword-களில் மட்டும் குவியாமல் பகிர உதவுகின்றன.

வாசிப்பான் முதலில் transport தரவைத் திருத்தி, அதன் 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 குணகங்களை மாற்றுகிறது. எனவே சுயசோதனைகள் இறுதியாக இணைக்கப்பட்ட தாங்கியிலேயே நடத்தப்படுகின்றன. உள்ளூர் browser JPEG சோதனையில் quality 0.60-லும், தொடர்ந்து 0.85→0.60 என encode செய்த பின்னரும் உறை துல்லியமாக மீண்டால் மட்டுமே மீட்பு வசதி இருப்பதாகக் காட்டப்படும். ஒவ்வொரு codeword-லிலும் திருத்தப்பட்டவை அதிகபட்சம் 12 பைட்டுகளாக இருக்க வேண்டும்; இது 16-பைட் திருத்த வரம்புக்குக் கீழே. இந்த quality மதிப்புகள் உள்ளூர் சோதனைக்கானவை, எல்லா JPEG-களுக்குமான உத்தரவாதம் அல்ல.

கூடுதல் விவரங்கள் உட்பட 280,000 மறையாக்கப்பட்ட பைட்டுகளுக்கு மேல் தானியங்கி முயற்சிகள் மேற்கொள்ளப்படாது. கூடுதல் மீள்நகல்களும் வலுவான சமிக்ஞைகளும் பின்னணிப் படத்தின் தரத்தையும் கொள்ளளவையும் செலவிடுகின்றன; தனிப்பட்ட அசல் தரவை இந்தச் சேனல் மீண்டும் சுருக்குவதில்லை. சோதனைகள் தோல்வியுற்றால் இழப்பற்ற PNG அல்லது ZIP பயன்படுத்தப்படும். படத்தை வெட்டுதல், அளவு மாற்றுதல், சுழற்றுதல் அல்லது வேறொரு encoder பயன்படுத்துதல் மீட்பைத் தடுக்கலாம். எனவே அசல் PNG அல்லது ZIP-ஐக் கோப்பாக அனுப்புவதே நம்பகமான வழி.

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

வடிவம் வெளிப்படுத்துவது என்ன?

நிலையான அடையாளக் குறிகளால் இந்த வடிவத்தைக் கண்டறிய முடியும். பின்னணி ஓவியம் பொதுவானது; ஓவியமாக அதன் நம்பகத்தன்மை சரிபார்க்கப்படுவதில்லை. அதன் மேல் பிட்களை மாற்றினாலும் பதிக்கப்பட்ட செய்தி அப்படியே இருக்கலாம். பிழைதிருத்தமும் CRC-யும் பரிமாற்றச் சேதத்தைக் கையாள்கின்றன; AES-GCM பாதுகாக்கப்பட்ட உள்ளடக்கத்தை அங்கீகரிக்கிறது.

தாக்குபவர் இணைய இணைப்பின்றியே கடவுச்சொற்களை ஊகித்துப் பார்க்க முடியும். Argon2id ஒவ்வொரு ஊகத்தின் செலவையும் உயர்த்தும்; ஆனால் பலவீனமான கடவுச்சொல்லைக் காப்பாற்ற முடியாது. கடவுச்சொல்லை மீட்டெடுக்கும் வசதி இல்லை. தனிச் சார்பற்ற பாதுகாப்புத் தணிக்கை நடந்ததாகவும் இங்கு கூறப்படவில்லை.

08

செயலி இல்லாமல் வாசிப்பது

அறியப்பட்ட மறையாக்க முறைகளைப் பயன்படுத்தும் தன்னிறைவான வடிவம், அதன் தெளிவான வரம்புகள், தனித்த வாசிப்பான் ஆகியவை இங்கு முக்கியமானவை. Python 3.12+ வாசிப்பான் அசல் PNG கோப்புகளைப் படிக்கும்; JPEG மீட்பு நகல் உள்ள PNG-களும் இதில் அடங்கும். வரையறுக்கப்பட்ட அளவுள்ள, ஒரே PNG கொண்ட SnapSeal ZIP கோப்புகளையும் raw v3 உறைகளையும் அது ஆதரிக்கிறது. JPEG ஆக மாற்றப்பட்ட கோப்புகளைப் படிக்க TypeScript codec தேவை.

இங்கு விவரிக்கப்பட்டது திறந்த மூல v3 செயலாக்கம்; அதன் வெளியீட்டைப் பழைய வாசிப்பான்கள் நிராகரிக்கலாம். பட்டியலிடப்பட்ட சார்புகளை மெய்நிகர் சூழலில் நிறுவுங்கள். முழு மூல நிரலும் விருப்பப் பதிவிறக்கங்களும் எடுத்துக்காட்டுகளுக்குப் பின் வருகின்றன.

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