Bewahren Sie Autorisierungstoken sicher auf

Hallo% Benutzername%. Unabhängig vom Thema des Berichts wird mir auf den Konferenzen ständig die gleiche Frage gestellt: "Wie können Token sicher auf dem Gerät des Benutzers gespeichert werden?". Normalerweise versuche ich zu antworten, aber die Zeit erlaubt es nicht, das Thema vollständig zu enthüllen. Mit diesem Artikel möchte ich dieses Problem vollständig schließen.

Ich habe ein Dutzend Anwendungen analysiert, um zu sehen, wie sie mit Token funktionieren. Alle von mir analysierten Anwendungen verarbeiteten kritische Daten und ermöglichten es mir, als zusätzlichen Schutz einen PIN-Code für die Eingabe festzulegen. Schauen wir uns die häufigsten Fehler an:

  • Senden eines PIN-Codes an die API zusammen mit RefreshToken, um die Authentifizierung zu bestätigen und neue Token zu erhalten. - Schlecht, das RefreshToken ist im lokalen Speicher unsicher. Mit physischem Zugriff auf das Gerät oder die Sicherung können Sie es extrahieren und die Malware kann es tun.
  • Speichern des PIN-Codes in der Nachricht mit RefreshToken, anschließende lokale Überprüfung des PIN-Codes und Senden des RefreshToken an die API. - RefreshToken ist ein Albtraum und zusammen mit dem Pin unsicher. Dadurch können sie extrahiert werden. Außerdem wird ein weiterer Vektor angezeigt, der darauf hindeutet, die lokale Authentifizierung zu umgehen.
  • Schlechte RefreshToken-Verschlüsselung mit einem PIN-Code, mit dem Sie den PIN-Code und RefreshToken aus dem Chiffretext wiederherstellen können. - Ein Sonderfall eines früheren Fehlers, etwas komplizierter ausgenutzt. Beachten Sie jedoch, dass dies der richtige Weg ist.

Nachdem Sie sich die häufigsten Fehler angesehen haben, können Sie die Logik der sicheren Speicherung von Token in Ihrer Anwendung durchdenken. Es lohnt sich, mit den grundlegenden Ressourcen zu beginnen, die mit der Authentifizierung / Autorisierung während des Betriebs der Anwendung verbunden sind, und einige Anforderungen an diese zu stellen:

Anmeldeinformationen - (Benutzername + Passwort) - werden verwendet, um den Benutzer im System zu authentifizieren.
+ Das Passwort wird niemals auf dem Gerät gespeichert und sollte sofort nach dem Senden an die API aus dem RAM gelöscht werden
+ werden von der GET-Methode nicht in Abfrageparametern der HTTP-Anforderung übertragen, stattdessen werden POST-Anforderungen verwendet
+ Der Tastaturcache ist für die Kennwortverarbeitung von Textfeldern deaktiviert
+ Zwischenablage ist für Textfelder deaktiviert, die ein Passwort enthalten
+ Passwort wird nicht über die Benutzeroberfläche bekannt gegeben (sie verwenden Sternchen), auch das Passwort kommt nicht in Screenshots

AccessToken - wird verwendet, um die Benutzerautorisierung zu bestätigen.
+ nie im Langzeitspeicher und nur im RAM gespeichert
+ werden von der GET-Methode nicht in Abfrageparametern der HTTP-Anforderung übertragen, stattdessen werden POST-Anforderungen verwendet

RefreshToken - wird verwendet, um ein neues AccessToken + RefreshToken-Bundle abzurufen.
+ wird in keiner Form im RAM gespeichert und sollte sofort nach dem Empfang von der API und dem Speichern im Langzeitspeicher oder nach dem Empfang aus dem Langzeitspeicher und der Verwendung aus dem RAM entfernt werden
+ nur in verschlüsselter Form im Langzeitgedächtnis gespeichert
+ mit einer PIN unter Verwendung von Magie und bestimmten Regeln verschlüsselt (die Regeln werden unten beschrieben). Wenn die PIN nicht festgelegt wurde, wird sie überhaupt nicht gespeichert
+ werden von der GET-Methode nicht in Abfrageparametern der HTTP-Anforderung übertragen, stattdessen werden POST-Anforderungen verwendet

PIN - (normalerweise eine 4- oder 6-stellige Nummer) - wird zum Ver- / Entschlüsseln des RefreshToken verwendet.
+ Niemals irgendwo auf dem Gerät gespeichert und sollte nach Gebrauch sofort aus dem RAM gelöscht werden
+ verlässt nie die Anwendungsgrenzen, diese werden nirgendwo übertragen
+ wird nur zur Ver- / Entschlüsselung von RefreshToken verwendet

OTP ist ein einmaliger Code für 2FA.
+ OTP wird niemals auf dem Gerät gespeichert und sollte sofort nach dem Senden an die API aus dem RAM gelöscht werden
+ werden von der GET-Methode nicht in Abfrageparametern der HTTP-Anforderung übertragen, stattdessen werden POST-Anforderungen verwendet
+ Tastatur-Cache für Textfelder, die OTP verarbeiten, deaktiviert
+ Zwischenablage für Textfelder, die OTP enthalten, deaktiviert
+ OTP kommt nicht in Screenshots
+ Die Anwendung entfernt OTP vom Bildschirm, wenn es in den Hintergrund geht

Kommen wir nun zur Magie der Kryptographie. Die Hauptanforderung besteht darin, dass Sie unter keinen Umständen die Implementierung eines solchen RefreshToken-Verschlüsselungsmechanismus zulassen sollten, mit dem Sie das Entschlüsselungsergebnis lokal validieren können. Das heißt, wenn ein Angreifer den Chiffretext in Besitz genommen hat, sollte er den Schlüssel nicht abholen können. Der einzige Validator sollte die API sein. Dies ist die einzige Möglichkeit, Schlüsselauswahlversuche einzuschränken und Token im Falle eines Brute-Force-Angriffs ungültig zu machen.

Ich werde ein gutes Beispiel geben, sagen wir, wir wollen die UUID verschlüsseln
aec27f0f-b8a3-43cb-b076-e075a095abfe
mit diesem Satz von AES / CBC / PKCS5Padding unter Verwendung einer PIN als Schlüssel. Es scheint, dass der Algorithmus gut ist, alles basiert auf Richtlinien, aber es gibt einen entscheidenden Punkt - der Schlüssel enthält sehr wenig Entropie. Mal sehen, wozu das führt:

  1. Auffüllen - Da unser Token 36 Bytes belegt und AES ein Blockverschlüsselungsmodus mit einem Block von 128 Bit ist, muss der Algorithmus das Token mit bis zu 48 Bytes (was ein Vielfaches von 128 Bit ist) beenden. In unserer Version wird der Schwanz gemäß dem PKCS5Padding-Standard hinzugefügt, d. H. Der Wert jedes hinzugefügten Bytes entspricht der Anzahl der hinzugefügten Bytes
    01
    02 02
    03 03 03
    04 04 04 04
    05 05 05 05 05
    06 06 06 06 06 06
    usw.
    Unser letzter Block wird ungefähr so ​​aussehen:
    ... | 61 62 66 65 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C |
    Und es gibt ein Problem, wenn wir uns diese Auffüllung ansehen, können wir die Daten (nach dem ungültigen letzten Block) herausfiltern, die mit dem falschen Schlüssel entschlüsselt wurden, und dadurch das gültige RefreshToken aus dem verdrehten Heap ermitteln.
  2. Vorhersagbares Format des Tokens - Selbst wenn wir unser Token auf ein Vielfaches von 128 Bit einstellen (z. B. Bindestriche entfernen), um das Hinzufügen von Auffüllungen zu vermeiden, werden wir auf das folgende Problem stoßen. Das Problem ist, dass wir alle denselben verdrehten Heap sammeln können, um festzustellen, welcher unter das UUID-Format fällt. Die UUID in ihrer kanonischen Textform besteht aus 32 Ziffern im Hexadezimalformat, die durch einen Bindestrich in 5 Gruppen 8-4-4-4-12 getrennt sind
    xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
    Dabei ist M die Version und N die Option. All dies reicht aus, um mit dem falschen Schlüssel entschlüsselte Token herauszufiltern und ein geeignetes UUID RefreshToken-Format zu erhalten.

In Anbetracht all der oben genannten Punkte können Sie mit der Implementierung fortfahren. Ich habe eine einfache Option ausgewählt, um 64 zufällige Bytes zu generieren und sie in base64 zu verpacken:

public String createRefreshToken() { byte[] refreshToken = new byte[64]; final SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(refreshToken); return Base64.getUrlEncoder().withoutPadding() .encodeToString(refreshToken); } 
Hier ist ein Beispiel für ein solches Token:
YmI8rF9pwB1KjJAZKY9JzqsCu3kFz4xt4GkRCzXS9-FS_kbN3-CF9RGiRuuGqwqMo-VxFDhgQNmgjlQFD2GvbA
Nun wollen wir sehen, wie es algorithmisch aussieht (unter Android und iOS ist der Algorithmus der gleiche):

 private static final String ALGORITHM = "AES"; private static final String CIPHER_SUITE = "AES/CBC/NoPadding"; private static final int AES_KEY_SIZE = 16; private static final int AES_BLOCK_SIZE = 16; public String encryptToken(String token, String pin) { decodedToken = decodeToken(token); //   rawPin = pin.getBytes(); byte[] iv = generate(AES_BLOCK_SIZE); //      CBC byte[] salt = generate(AES_KEY_SIZE); //       byte[] key = kdf.deriveKey(rawPin, salt, AES_KEY_SIZE); //  -    Cipher cipher = Cipher.getInstance(CIPHER_SUITE); //    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, ALGORITHM), new IvParameterSpec(iv)); return cipher.doFinal(token); } public byte[] decodeToken(String token) { byte[] rawToken = token.getBytes(); return Base64.getUrlDecoder().decode(rawToken); } public final byte[] generate(int size) { byte[] random = new byte[size]; (new SecureRandom()).nextBytes(random); return random; } 

Welche Zeilen sind es wert, beachtet zu werden:

 private static final String CIPHER_SUITE = "AES/CBC/NoPadding"; 

Keine Polsterung, na ja, du erinnerst dich.

 decodedToken = decodeToken(token); //   

Sie können ein Token in der base64-Darstellung nicht einfach nehmen und verschlüsseln, da diese Darstellung ein bestimmtes Format hat (Sie erinnern sich).

 byte[] key = kdf.deriveKey(rawPin, salt, AES_KEY_SIZE); //  -    

Am Ausgang erhalten wir einen Schlüssel der Größe AES_KEY_SIZE, der für den AES-Algorithmus geeignet ist. Jede von Argon2, SHA-3, Scrypt empfohlene Schlüsselableitungsfunktion kann im Falle eines schlechten Lebens als kdf verwendet werden (pbkdf2) (dies entspricht sehr gut den FPGAs).

Das endgültige verschlüsselte Token kann sicher auf dem Gerät gespeichert werden, ohne dass jemand es stehlen muss, sei es eine Malware oder eine Entität, die nicht durch moralische Prinzipien belastet ist.

Einige weitere Empfehlungen:

  • Token von Backups ausschließen.
  • Speichern Sie das Token unter iOS im Schlüsselbund mit dem Attribut kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly.
  • Verteilen Sie die in diesem Artikel beschriebenen Elemente (Schlüssel, PIN, Kennwort usw.) nicht in der gesamten Anwendung.
  • Überschreiben Sie Assets, sobald sie unnötig werden. Speichern Sie sie nicht länger als nötig in Ihrem Speicher.
  • Verwenden Sie SecureRandom unter Android und SecRandomCopyBytes unter iOS, um zufällige Bytes in einem kryptografischen Kontext zu generieren.

Wir haben beim Speichern von Token eine Reihe von Fallstricken untersucht, die meiner Meinung nach jeder Person bekannt sein sollten, die Anwendungen entwickelt, die mit kritischen Daten arbeiten. Dieses Thema, in dem Sie bei jedem Schritt verwirrt werden können, wenn Sie Fragen haben, stellen Sie diese in den Kommentaren. Kommentare zum Text sind ebenfalls willkommen.

Referenzen:

CWE-311: Fehlende Verschlüsselung sensibler Daten
CWE-327: Verwendung eines kaputten oder riskanten kryptografischen Algorithmus
CWE-327: CWE-338: Verwendung eines kryptografisch schwachen Pseudozufallszahlengenerators (PRNG)
CWE-598: Offenlegung von Informationen durch Abfragezeichenfolgen in der GET-Anforderung

Source: https://habr.com/ru/post/de423753/


All Articles