
WDRUK SECURE
Niewidoczny znak wodny, który przeżywa wydruk i zdjęcie telefonem, a jego autentyczność potwierdza podpis kryptograficzny.
System znakowania zdjęć, w którym autorstwo da się udowodnić matematycznie. Znak nie siedzi w najmłodszych bitach pikseli, tylko w dziedzinie falkowo-kosinusowej, jest chroniony kodem korekcyjnym i podpisany kluczem Ed25519. Docelowo odczyta go aplikacja na Androidzie — aparatem, z wydrukowanej odbitki.
PROBLEM
Zdjęcie produktowe puszczone w obieg przestaje mieć autora. Wędruje między sklepami, hurtowniami i porównywarkami, po drodze jest skalowane, przekompresowywane, kadrowane i drukowane w katalogu. Metadane EXIF giną przy pierwszym zapisie, widoczny znak wodny psuje zdjęcie i daje się wyciąć, a podpis w pliku znika razem z plikiem, gdy ktoś zrobi zrzut ekranu.
Pytanie praktyczne brzmi więc: jak z wydrukowanej kartki odzyskać informację, kto jest autorem zdjęcia — i jak zrobić to tak, żeby nie dało się tego podrobić ani przypisać komuś innemu.
POMYSŁ
Umieścić znak nie w pikselach, tylko w częstotliwościach — tam, gdzie przeżyje skalowanie, kompresję stratną i cykl druk–fotografia. Do tego dołożyć dwie rzeczy, których zwykła steganografia nie ma: nadmiarowość, żeby zniszczenie części obrazu nie kasowało informacji, i podpis kryptograficzny, żeby odczyt cokolwiek znaczył.
Priorytety ustawiono świadomie w tej kolejności: najpierw brak fałszywego rozpoznania, potem autentyczność, odporność, niewidoczność, wydajność, a dopiero na końcu pojemność danych. System, który czasem przypisze zdjęcie niewłaściwej osobie, jest gorszy niż system, który po prostu odmawia odpowiedzi.
PROJEKT
Rdzeń jest wspólny dla obu platform: biblioteka w C++20 kompilowana do DLL na Windows i do .so przez NDK na Androida. Wymóg brzmiał wprost — plik zakodowany na komputerze musi dać się odczytać telefonem, więc przekształcenia falkowe, kosinusowe, modulacja i generator pseudolosowy są napisane ręcznie, bez zależności od bibliotek platformowych.
Obraz trafia najpierw na siatkę kanoniczną, która zachowuje proporcje i nigdy nie powiększa — interpolacja nie tworzy informacji, więc powiększanie tylko udawałoby zapas miejsca. Potem idzie transformata falkowa CDF 9/7, z pasma niskoczęstotliwościowego wycinane są bloki 8×8 poddawane transformacie kosinusowej, a bity zapisuje modulacja kątowa w paśmie średnim: trzy bity na blok, każdy na rozłącznej parze współczynników.
Siłę osadzania reguluje maska oparta na progu widoczności — mocniej tam, gdzie oko nie zauważy, słabiej przy gładkich powierzchniach i krawędziach. Dane układane są w dwóch strefach: rdzeniu i pierścieniu, tak by skadrowanie do środka albo zamalowanie środka nie kasowało całości.
Ochronę przed uszkodzeniami daje kod Reed–Solomona z obsługą wymazań. Na wierzchu jest podpis Ed25519 albo krótszy kod uwierzytelniający, a nad wszystkim suma kontrolna CRC-16.
REALIZACJA
Format ładunku został zamrożony: profil zwięzły ma 144 bity, pełny 248 bitów, oba wyrównane do bajtu, z numerem wersji algorytmu — każda zmiana układu bitów wymusza nową wersję, więc stare pliki pozostają czytelne.
W obrazie zapisywany jest wyłącznie numer autora. Nazwisko, adres i telefon nigdy tam nie trafiają — mapowanie numeru na osobę żyje w lokalnej bazie kodera. To rozwiązanie ma nieoczywistą zaletę: poprawienie literówki w nazwisku działa wstecz na wszystkie zdjęcia, bo w pikselach siedzi tylko liczba. Aliasy pozwalają dodatkowo scalić różne zapisy tej samej osoby.
Klucz prywatny nie trafia ani do repozytorium, ani do pliku wykonywalnego, ani do aplikacji mobilnej. Leży w magazynie zaszyfrowanym XChaCha20-Poly1305, z kluczem wyprowadzonym funkcją Argon2id, a hasło pobierane jest zawsze interaktywnie — nigdy z argumentu polecenia, bo ten byłby widoczny na liście procesów. Do czytnika trafia sam klucz publiczny.
Rzeczywisty materiał wymusił zmianę projektu. Pliki produktowe okazały się w 82–88 procentach przezroczyste, więc realnym nośnikiem w obrazie 5000×5000 jest około 4,5 miliona pikseli, a nie 25 milionów. Powstała trzecia klasa treści obok zdjęcia i logotypu, a estymator pojemności liczy wyłącznie piksele nieprzezroczyste.
EFEKT
Osadzanie działa od końca do końca na prawdziwych plikach. Jakość mieści się w przedziale 41,7–42,2 dB według miary PSNR przy podobieństwie strukturalnym 0,985–0,987, bez błędów resztkowych. Pojemność rdzenia rośnie od 2448 bitów przy małych plikach do 7173 bitów przy packshotach 5000 px, co daje od pięciu do czternastu kopii ładunku.
Odporność sprawdzono destrukcyjnie: przy zniszczeniu 10, 25 i 40 procent obrazu odczyt pozostaje poprawny. Skadrowanie do środka, zamalowanie środka i usunięcie wszystkiego poza rdzeniem również kończą się poprawnym odczytem. Próba odczytu niewłaściwym kluczem daje 51,8 procent błędnych bitów, czyli wynik nieodróżnialny od losowania.
Załączona mapa różnicy pokazuje, gdzie znak faktycznie siedzi — po wzmocnieniu widać strukturę bloków i wzór synchronizacji, choć w samym zdjęciu nie da się ich dostrzec.
GALERIA
02 ZDJĘĆWNIOSKI
Projekt ma udokumentowaną wadę blokującą i to jest jego najważniejsza informacja. Osadzanie działa, ale wykrywanie znaku na prawdziwych fotografiach — nie. Wzór synchronizacji leży w niskich częstotliwościach, bo tam najlepiej znosi pomniejszanie i druk, tyle że rzeczywiste zdjęcie ma widmo typu jeden przez f kwadrat i niesie w tym paśmie ogromną, uporządkowaną energię. Wzór w niej ginie: pomiar na konkretnym pliku dał wyrazistość 4,65 z wzorem wobec 4,67 bez niego. Podnoszenie siły nie pomaga — psuje jakość, nie poprawiając wykrywalności.
Istotne jest to, że koder wykrywa problem sam. Samokontrola po osadzeniu zgłasza ostrzeżenie, więc nie da się wyprodukować pliku, który w raporcie wygląda dobrze, a w praktyce jest nieczytelny. Podobnie działa ostrzeżenie o zbyt małej rozdzielczości: poniżej 1200 pikseli dłuższej krawędzi koder nie obniża po cichu nadmiarowości, tylko mówi wprost, że odczyt po wydruku przestaje być gwarantowany.
Dlatego czytnik na Androida — aparat, prostowanie perspektywy, demodulacja i weryfikacja podpisu — świadomie nie został jeszcze zaczęty. Budowanie interfejsu do funkcji, która nie działa, dałoby tylko ładną atrapę. Najpierw musi ruszyć synchronizacja.


