From 3c5266fe689163a2614b300e0b0919cf38efb49f Mon Sep 17 00:00:00 2001
From: Frank Sauerburger <frank@sauerburger.com>
Date: Wed, 9 Jan 2019 14:53:46 +0100
Subject: [PATCH] Implement working TLS tracker version

---
 app/assets/index.html      |  38 +++++---
 app/components/App.jsx     | 172 ++++++++++++++++++++++++++++++++++++-
 app/initialize.js          |   2 +-
 app/styles/application.css | 169 +++++++++++++++++++++++++++++++++---
 brunch-config.js           |   4 +-
 5 files changed, 359 insertions(+), 26 deletions(-)

diff --git a/app/assets/index.html b/app/assets/index.html
index ed657d0..d62efd2 100644
--- a/app/assets/index.html
+++ b/app/assets/index.html
@@ -1,17 +1,31 @@
-<!DOCTYPE html>
+<!doctype html>
+<html lang="en">
 <head>
-  <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width">
-  <title>Brunch</title>
-  <link rel="stylesheet" href="/app.css">
-  <script src="/vendor.js"></script>
-  <script src="/app.js"></script>
+  <title>TLS Tracking</title>
+  <meta charset="utf-8" />
+  <link rel="stylesheet" href="style.css" />
+  <script src="vendor.js"></script>
+  <script src="script.js"></script>
   <script>require('initialize');</script>
+  <meta name="viewport" content="width=device-width, initial-scale=1">
 </head>
 <body>
-  <div class="brunch">
-    <a href="http://brunch.io"><img src="http://brunch.io/images/logo.png" alt="Brunch"></a>
-    <p>Bon Appétit.</p>
-    <div id="app"></div>
-  </div>
+  <header>
+    <h1><a href="/">TLS Tracking</a></h1>
+  </header>
+  <main>
+    <p>
+      This page demonstrates how TLS and be abused to track users employing HTTP Strict Transport Security (HSTS).
+    </p>
+		<div id="react-root"></div>
+	</main>
+  <footer>
+    <p>
+      &copy; 2019 <a href="mailto:frank@sauerburger.com">Frank Sauerburger</a> &bull;
+      <a href="imprint.html">Imprint</a> &bull;
+      <a href="privacy.html">Privacy Policy</a>
+    </p>
+  </footer>
 </body>
+</html>
+
diff --git a/app/components/App.jsx b/app/components/App.jsx
index 637383c..6983765 100644
--- a/app/components/App.jsx
+++ b/app/components/App.jsx
@@ -1,10 +1,180 @@
 import React from 'react';
 
+class HostIdenticator extends React.Component {
+  render() {
+    var label, className;
+    if (this.props.https === null) {
+      className = "unknown";
+      label = "?";
+    } else if (this.props.https) {
+      className = "https";
+      label = "https";
+    } else {
+      className = "http";
+      label = "http";
+    }
+
+    return <div className={className}>{ label }</div>;
+  }
+}
+
+class IndicatorPanel extends React.Component {
+  render() {
+    return (
+      <div className="indicator-panel">
+        {this.props.bits.map((bit, i) => {
+          return (
+              <HostIdenticator key={`host-ind-${i}`} https={ bit } />
+          );
+        })}
+      </div>
+      );
+  }
+}
+
+class ContentIndicator extends React.Component {
+  render() {
+    var className = this.props.ready ? "ready" : "pending";
+    var label = this.props.ready ? "Ready ✔" : "loading...";
+    return (
+      <div className="content-indicator">
+        <div className={className}>{this.props.text}</div>
+        <div className={className}>{label}</div>
+      </div>
+      );
+  }
+
+}
+
+function pad(n, width, z) {
+    z = z || '0';
+      n = n + '';
+        return n.length >= width ? n : new Array(width - n.length + 1).join(z) +
+        n;
+}
+
+var format =  n => pad(n, 3);
+
+class ComplexEncoder extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {bits: new Array(this.props.maxBit).fill(null), inputValue: ""};
+    this.encodeInput = this.encodeInput.bind(this);
+    this.updateInputValue = this.updateInputValue.bind(this);
+    this.handleKeyPress = this.handleKeyPress.bind(this);
+  }
+
+  is_ready() {
+    return this.state.bits.every((b) => (b !== null));
+  }
+
+  has_content() {
+    return this.state.bits.some((b) => (b == true && b !== null));
+  }
+
+  build_callback(index) {
+    return (res => {
+        var d = this.state.bits;
+        if (res.trim() == "http") {
+          d[index] = false;
+        } else if (res.trim() == "https") {
+          d[index] = true;
+          this.setState({all_false: false});
+        } else {
+          d[index] = null;
+        }
+        this.setState({bits: d});
+        for (var i=0; i<this.props.maxBit; i++) {
+          if (d[i] === null) {
+            return;
+          }
+        }
+        this.setState({ready: true});
+    });
+  }
+
+  componentDidMount() {
+    for (var i=0; i<this.props.maxBit; i++) {
+      var key = "b" + format(i);
+      fetch("http://" + key + ".tls-tracking.sauerburger.com")
+        .then(res => res.text())
+        .then(this.build_callback(i));
+    }
+  }
+
+  encode(string) {
+    var binary = "";
+    for (var i=0; i<string.length; i++) {
+      binary += pad(string.charCodeAt(i).toString(2), 8);
+    }
+
+    for (var i=0; i<binary.length && i < this.props.maxBit; i++) {
+      if (binary[i] == "1") {
+        var key = "b" + format(i);
+        fetch("https://" + key + ".tls-tracking.sauerburger.com")
+          .then(res => res.text())
+          .then(this.build_callback(i));
+      }
+    }
+
+  }
+
+  decode() {
+    var decoded = "";
+    var binary = this.state.bits.map(x => {return x ? "1" : "0"}).join("");
+    for (var i = 0; i < binary.length / 8; i++) {
+      var charCode = parseInt(binary.substring(i * 8, (i+1) * 8), 2);
+      if (charCode == 0) {
+        decoded += "";
+      } else {
+        decoded += String.fromCharCode(charCode);
+      }
+    }
+    return decoded;
+  }
+
+  encodeInput() {
+    this.encode(this.state.inputValue);
+  }
+
+  updateInputValue(evt) {
+    this.setState({inputValue: evt.target.value});
+  }
+
+  handleKeyPress(evt) {
+    if (evt.key === "Enter") {
+      this.encodeInput();
+    }
+  }
+
+  render() {
+        return (<div id="complexEncoder">
+            <IndicatorPanel bits={this.state.bits} />
+            { this.has_content() ? 
+              (<div>Your browser is tracked with the following string:
+                <ContentIndicator text={this.decode()} ready={this.is_ready()} />
+               </div>) : 
+               (<div><p>Your browser is not yet tracked. Enter an arbitary phrase
+               to be encoded. </p>
+                <p><input type="text" maxLength={this.props.maxBit / 8}
+                onChange={this.updateInputValue}
+                onKeyPress={this.handleKeyPress}/>
+                <input type="button" value="Submit" onClick={this.encodeInput} />
+                </p></div>)
+            }
+             <p> Please note, to remove the tracking tag, you need
+             to clear the recent history in your browser.</p>
+            </div>);
+
+  }
+
+}
+
 export default class App extends React.Component {
   render() {
     return (
       <div id="content">
-        <h5>Time to <a href="https://facebook.github.io/react/">React</a>.</h5>
+        <ComplexEncoder maxBit={128}/>
       </div>
     );
   }
diff --git a/app/initialize.js b/app/initialize.js
index 84becf0..9b01e2a 100644
--- a/app/initialize.js
+++ b/app/initialize.js
@@ -3,5 +3,5 @@ import React from 'react';
 import App from 'components/App';
 
 document.addEventListener('DOMContentLoaded', () => {
-  ReactDOM.render(<App />, document.querySelector('#app'));
+  ReactDOM.render(<App />, document.querySelector('#react-root'));
 });
diff --git a/app/styles/application.css b/app/styles/application.css
index efa58d9..c35888a 100644
--- a/app/styles/application.css
+++ b/app/styles/application.css
@@ -1,20 +1,169 @@
-.brunch {
-  font-family: -apple-system, Sans-Serif;
+@font-face {
+  font-family: "Suprema";
+  src: url("SupremaRegular.woff") format('woff');
+}
+
+html {
+  min-height: 100%;
+  position: relative;
+  margin: 0px;
+  padding: 0px;
+  background-color: #f2f2f0;
+}
+
+body{
+  margin: 0px;
+  padding: 0px;
+  font-family: sans-serif;
+}
+
+header h1 {
+  margin: 0.8em auto 0.4em;
+}
+
+header {
+  font-family: "Suprema", sans-serif;
+  border-top: 1px solid #0e2f43;
+  margin: 0px;
+  background-color: #0e2f43;
+  border-bottom: 8px solid #2d8891;
+  color: #f2f2f0;
+}
+
+main {
+  margin: 0px auto 5em;
+  padding: 0em 1em;
+  text-align: justify;
+}
+
+@media only screen and (min-width: 820px) {
+  main {
+    width: 780px;
+  }
+  header h1 {
+    width: 780px;
+  }
+  footer p {
+    width: 780px;
+  }
+}
+
+code {
+  background-color: #d7bf7866;
+  font-family: monospace;
+  padding: 0.2em 0.7em;
+}
+
+.panel {
+  background-color: #cf5b55;
   text-align: center;
-  font-size: 24pt;
-  color: #3f894a;
+  padding: 1em;
+  margin: 1em 6em;
+  color: #fff;
+  font-weight: bold;
+  border-radius: 5px;
+}
+
+.started {
+  /*background-color: #55cf85;*/
+  background-color: #2d8891;
+}
+
+footer {
+  position: absolute;
+  bottom: 0px;
+  width: 100%;
+}
+
+footer p {
+  margin: 4em auto 0em;
+  padding: 1em;
+  border-top: 6px solid #2d8891;
+  text-align: right;
+  background-color: #0e2f43;
 }
 
 a {
-  color: #3f894a;
+  color: #2d8891;
   text-decoration: none;
-  border-bottom: 1px solid rgba(0, 0, 0, 0);
+}
+footer, footer a {
+  color: #ddd;
+}
+
+.terms h2 {
+  counter-reset: section;
 }
 
-a:hover {
-  color: #27552e;
+.terms h3:before {
+    content: counter(section) ".\0000a0\0000a0";
+    counter-increment: section;
+    counter-reset: list;
 }
 
-h5 > a {
-  text-decoration: underline;
+.terms ol ol {
+  list-style-type: lower-alpha;
 }
+
+#complexEncoder {
+  background-color: #fff;
+  padding: 1em;
+  margin: 1em;
+}
+
+.indicator-panel {
+  width: 515px;
+  margin: 1em auto;
+}
+
+.indicator-panel div {
+  width: 24px;
+  height: 24px;
+  padding: 2px;
+  line-height: 24px;
+  font-size: 10px;
+
+  margin: 1px;
+  text-align: center;
+  border-radius: 10px;
+  display: inline-block;
+
+}
+
+.indicator-panel div:nth-child(8n) {
+  margin-right: 16px;
+ }
+
+.indicator-panel .unknown {
+  background-color: #ceaa2c;
+}
+.indicator-panel .https {
+  background-color: #46ce2c;
+}
+.indicator-panel .http {
+  background-color: #812529;
+  color: #fff;
+}
+
+.content-indicator div {
+  display: inline-block;
+  padding: 0.6em;
+}
+
+.content-indicator {
+  margin: 0.4em 2em;
+}
+
+.content-indicator div:first-child {
+  font-family: monospace;
+  background-color: #ddd;
+}
+
+.content-indicator div.ready:first-child {
+  color: #2d8891;
+}
+
+.content-indicator div.ready {
+  color: #0e2f43;
+}
+
diff --git a/brunch-config.js b/brunch-config.js
index 0ff0ff1..e3079a7 100644
--- a/brunch-config.js
+++ b/brunch-config.js
@@ -3,10 +3,10 @@ exports.files = {
   javascripts: {
     joinTo: {
       'vendor.js': /^(?!app)/,
-      'app.js': /^app/
+      'script.js': /^app/
     }
   },
-  stylesheets: {joinTo: 'app.css'}
+  stylesheets: {joinTo: 'style.css'}
 };
 
 exports.plugins = {
-- 
GitLab