AMP

Ochrona subskrybowanych treści za pomocą szyfrowania po stronie klienta

Jeśli jesteś wydawcą internetowym, dochody prawdopodobnie osiągasz z subskrypcji. Możesz blokować treści premium za paywallem na kliencie, stosując zaciemnianie kodu CSS (display: none).

Niestety, bardziej zaawansowani technicznie użytkownicy mogą to obejść.

Zamiast tego, można pokazywać użytkownikom dokument, który nie zawiera treści premium! Chodzi o serwowanie całkowicie nowej strony po zatwierdzeniu użytkownika przez zaplecze. Metoda ta, choć bezpieczniejsza, kosztuje czas, zasoby i zadowolenie użytkownika.

Rozwiąż oba te problemy poprzez wdrożenie walidacji subskrybentów premium i deszyfrowania treści po stronie klienta. Dzięki temu rozwiązaniu użytkownicy z dostępem premium będą mogli odszyfrować treść bez konieczności ładowania nowej strony ani czekania na odpowiedź backendu!

Omówienie ustawień

W celu wdrożenia szyfrowania po stronie klienta należy połączyć szyfrowanie przy użyciu klucza symetrycznego z szyfrowaniem przy użyciu klucza publicznego w następujący sposób:

  1. Utwórz losowy klucz symetryczny dla każdego dokumentu, przydzielając każdemu dokumentowi unikalny klucz.
  2. Zaszyfruj treść premium za pomocą klucza symetrycznego dokumentu.
    Klucz jest symetryczny, aby umożliwić zaszyfrowanie i odszyfrowanie treści za pomocą tego samego klucza.
  3. Zaszyfruj klucz dokumentu przy użyciu klucza publicznego, używając hybrydowego protokołu szyfrowania do szyfrowania kluczy symetrycznych.
  4. Za pomocą składników <amp-subscriptions> i/lub <amp-subscriptions-google> zapisz zaszyfrowany klucz dokumentu w dokumencie AMP wraz z zaszyfrowaną treścią premium.

Dokument AMP przechowuje zaszyfrowany klucz w sobie. Zapobiega to odłączeniu zaszyfrowanego dokumentu od klucza, który go dekoduje.

Jak to działa?

  1. AMP analizuje klucz z zaszyfrowanej treści dokumentu, w którym ląduje użytkownik.
  2. Podczas serwowania treści premium AMP wysyła zaszyfrowany klucz symetryczny z dokumentu do autoryzatora w ramach pobierania uprawnień użytkownika.
  3. Autoryzer decyduje, czy użytkownik ma odpowiednie uprawnienia. Jeśli tak, odszyfrowuje klucz symetryczny dokumentu kluczem prywatnym autoryzera z jego pary kluczy publiczny/prywatny. Następnie autoryzator zwraca klucz dokumentu do logiki składnika amp-subscriptions.
  4. AMP odszyfrowuje treść premium za pomocą klucza dokumentu i pokazuje ją użytkownikowi!

Kroki implementacji

Wykonaj poniższe kroki, aby zintegrować obsługę szyfrowania AMP z wewnętrznym serwerem uprawnień.

Krok 1: utwórz parę kluczy publiczny/prywatny

Do zaszyfrowania klucza symetrycznego dokumentu niezbędna jest własna para kluczy publiczny/prywatny. Klucz publiczny jest szyfrowany za pomocą hybrydowego protokołu szyfrowania, a konkretnie metody szyfrowania asymetrycznego ECIES przy użyciu krzywej eliptycznej P-256 z metodą szyfrowania symetrycznego AES-GCM (128-bitową).

Wymagamy obsługi klucza publicznego za pomocą Tink przy użyciu tego typu klucza asymetrycznego. Aby utworzyć parę kluczy prywatny-publiczny, użyj jednego z poniższych sposobów:

Obie wspierają rotację klucza. Wdrożenie rotacji kluczy ogranicza podatność na złamanie klucza prywatnego.

Aby ułatwić Ci rozpoczęcie tworzenia kluczy asymetrycznych, utworzyliśmy ten skrypt. Służy on do:

  1. Tworzenia nowych schematów ECIES za pomocą klucza AEAD.
  2. Wyprowadzania klucza publicznego w postaci zwykłego tekstu do pliku wyjściowego.
  3. Wyprowadzania klucza prywatnego do innego pliku wyjściowego.
  4. Szyfrowania wygenerowanego klucza prywatnego za pomocą klucza umieszczonego na serwerze Google Cloud (GCP) przed zapisaniem go w pliku wyjściowym, (powszechnie zwane jest to szyfrowaniem koperty).

Wymagamy przechowywania/publikowania Twojego publicznego Tink Keyset w formacie JSON. Pozwala to na bezproblemową pracę innych narzędzi AMP. Nasz skrypt już generuje klucz publiczny w tym formacie.

Krok 2: zaszyfruj artykuły

Zadecyduj, czy będziesz szyfrować treści premium ręcznie, czy automatycznie.

Szyfrowanie ręczne

Do szyfrowania treści premium wymagamy metody symetrycznej AES-GCM 128, wykorzystującej Tink. Klucz symetryczny dokumentu używany do szyfrowania treści premium powinien być unikalny dla każdego dokumentu. Dodaj klucz dokumentu do obiektu JSON, który zawiera klucz w zwykłym tekście o kodowaniu base64, a także jednostki SKU wymagane do uzyskania dostępu do zaszyfrowanej zawartości dokumentu.

Poniższy obiekt JSON zawiera przykładowy klucz w prostym tekście zakodowanym w base64 oraz SKU.

{
  AccessRequirements: ['thenewsynews.com:premium'],
  Key: 'aBcDef781-2-4/sjfdi',
}

Powyższy obiekt JSON należy zaszyfrować przy użyciu klucza publicznego wygenerowanego w kroku Utwórz parę kluczy publiczny/prywatny.

Dodaj zaszyfrowany wynik jako wartość do klucza "local". Umieść parę klucz-wartość w obiekcie JSON w otoce znacznika <script type="application/json" cryptokeys="">. Umieść znacznik w nagłówku dokumentu.

<head>
...
<script type="application/json" cryptokeys="">
{
  "local": ['y0^r$t^ff'], // This is for your environment
  "google.com": ['g00g|e$t^ff'], // This is for Google's environment
}
</script></head>

Musisz zaszyfrować klucz dokumentu za pomocą środowiska lokalnego i klucza publicznego Google. Dodanie klucza publicznego Google umożliwi serwerowi buforującemu Google AMP zaserwowanie dokumentu. Aby zaakceptować klucz publiczny Google z jego adresu URL, musisz utworzyć wystąpienie Tink Keyset:

https://news.google.com/swg/encryption/keys/prod/tink/public\_key

Klucz publiczny Google to Tink Keyset w formacie JSON. Zobacz tutaj przykład pracy z tym zestawem kluczy.

Czytaj dalej: zobacz przykład działającego szyfrowanego dokumentu AMP.

Automatyczne szyfrowanie

Zaszyfruj dokument za pomocą naszego skryptu. Skrypt akceptuje dokument HTML i szyfruje całą zawartość wewnątrz znaczników <section subscriptions-section="content" encrypted>. Przy użyciu kluczy publicznych znajdujących się przekazanych do niego w adresach URL, skrypt szyfruje klucz dokumentu utworzony przez skrypt. Użycie tego skryptu zapewnia, że cała zawartość zostaje zaszyfrowana i prawidłowo sformatowana do serwowania. Dalsze instrukcje dotyczące stosowania tego skryptu znajdują się tutaj.

Krok 3: zintegruj autoryzator

Aby móc odszyfrować klucze dokumentów, gdy użytkownik ma odpowiednie uprawnienia, musisz zaktualizować autoryzator. Składnik amp-subscriptions automatycznie wysyła zaszyfrowany klucz dokumentu do autoryzatora "local" poprzez parametr adresu URL "crypt="". Wykonuje on:

  1. Analizę klucza dokumentu z pola JSON klucza "local".
  2. Odszyfrowanie dokumentu.

Do odszyfrowywania kluczy dokumentów w swoim autoryzatorze musisz używać Tink. Aby odszyfrowywać za pomocą Tink, utwórz wystąpienie klienta HybridDecrypt przy użyciu kluczy prywatnych wygenerowanych w kroku Utwórz parę kluczy publiczny/prywatny. Wykonaj to podczas uruchamiania serwera, aby uzyskać optymalną wydajność.

Wdrożenie klienta HybridDecrypt/autoryzatora powinno być z grubsza zgodne z harmonogramem rotacji kluczy. Dzięki temu wszystkie wygenerowane klucze będą dostępne dla klienta HybridDecrypt.

Tink ma rozbudowaną dokumentację i przykłady w językach C++, Java, Go i JavaScript, ułatwiającą rozpoczęcie implementacji po stronie serwera.

Zarządzanie żądaniami

Gdy do autoryzatora przychodzi żądanie:

  1. Przeanalizuj adres URL pingbacku uprawnień pod względem parametru "crypt=".
  2. Odszyfruj wartość parametru "crypt=" za pomocą base64. Wartością zapisaną w parametrze adresu URL jest zaszyfrowany obiekt JSON zakodowany za pomocą base64.
  3. Gdy klucz szyfrowany ma już postać nieprzetworzonych bajtów, użyj funkcji odszyfrowywania HybridDecrypt, aby odszyfrować klucz za pomocą klucza prywatnego.
  4. Jeśli odszyfrowanie powiedzie się, przetwórz wynik na obiekt JSON.
  5. Zweryfikuj dostęp użytkownika do jednego z uprawnień wymienionych w polu JSON AccessRequirements.
  6. Zwróć klucz dokumentu z pola „Key” odszyfrowanego obiektu JSON w odpowiedzi z uprawnieniami. Dodaj odszyfrowany klucz dokumentu w nowym polu „decryptedDocumentKey” w odpowiedzi z uprawnieniami. Umożliwi to dostęp do frameworku AMP.

Poniższa próbka to fragment pseudokodu, ilustrujący powyższe kroki z opisu:

string decryptDocumentKey(string encryptedKey, List < string > usersEntitlements,
    HybridDecrypt hybridDecrypter) {
    // 1. Base64 decode the input encrypted key.
    bytes encryptedKeyBytes = base64.decode(encryptedKey);
    // 2. Try to decrypt the encrypted key.
    bytes decryptedKeyBytes;
    try {
        decryptedKeyBytes = hybridDecrypter.decrypt(
            encryptedKeyBytes, null /* contextInfo */ );
    } catch (error e) {
        // Decryption error occurred. Handle it how you want.
        LOG("Error occurred decrypting: ", e);
        return "";
    }
    // 3. Parse the decrypted text into a JSON object.
    string decryptedKey = new string(decryptedKeyBytes, UTF_8);
    json::object decryptedParsedJson = JsonParser.parse(decryptedKey);
    // 4. Check to see if the requesting user has the entitlements specified in
    //    the AccessRequirements section of the JSON object.
    for (entitlement in usersEntitlements) {
        if (decryptedParsedJson["AccessRequirements"]
            .contains(entitlement)) {
            // 5. Return the document key if the user has entitlements.
            return decryptedParsedJson["Key"];
        }
    }
    // User doesn't have correct requirements, return empty string.
    return "";
}

JsonResponse getEntitlements(string requestUri) {
    // Do normal handling of entitlements here…
    List < string > usersEntitlements = getUsersEntitlementInfo();

    // Check if request URI has "crypt" parameter.
    String documentCrypt = requestUri.getQueryParameters().getFirst("crypt");

    // If URI has "crypt" param, try to decrypt it.
    string documentKey;
    if (documentCrypt != null) {
        documentKey = decryptDocumentKey(
            documentCrypt,
            usersEntitlements,
            this.hybridDecrypter_);
    }

    // Construct JSON response.
    JsonResponse response = JsonResponse {
        signedEntitlements: getSignedEntitlements(),
        isReadyToPay: getIsReadyToPay(),
    };
    if (!documentKey.empty()) {
        response.decryptedDocumentKey = documentKey;
    }
    return response;
}

Zasoby pokrewne

Zapoznaj się z dokumentacją i przykładami znajdującymi się na stronie Tink na Github.

Wszystkie skrypty pomocnicze znajdują się w repozytorium Github subscriptions-project/encryption.

Dodatkowa pomoc

W przypadku jakichkolwiek pytań, uwag lub wątpliwości wypełnij formularz zgłoszenia do Github.