A tool for whistleblowers to identify digital fingerprinting in text documents.


In an attempt to identify information leaks, employers are inserting zero-width (hidden) characters into documents. Each employee receives what appears to be the same document, but each contains a unique digital fingerprint. If the document is leaked, the employer can trace it back to the person who was given that unique version.

How To

Download the files to a storage device, and plug it into the target computer. Open openme.html in a web browser (e.g. right-click, then choose "open with ... "). Open a document, copy its contents, and paste it into the tool. Hidden characters will appear in red.

Visit to see zero-width text in action.

<!doctype html> <html> <head> <meta charset="utf-8"> <meta name=viewport content="width=device-width, initial-scale=1"> <title>Google</title> <style> ::placeholder { color: #555555; } body { background: #222222; } #logo { position: absolute; display: inline-block; height: 5vw; width: 27vw; left: 3vw; top: 4vw; background: url(logo.svg) no-repeat center center; background-size: contain; } #input { position: absolute; height: 14vw; right: 4vw; left: 8.2vw; top: 12vw; font-family: Helvetica, Verdana, sans-serif; font-size: 16px; color: #555555; background: #222222; border: none; resize: none; } #input:focus { outline: none; } #output { padding-bottom: 4vw; position: absolute; right: 4vw; left: 8.2vw; top: 30vw; } span { margin: 1px 1px 1px 1px; display: inline-block; height: 36px; width: 27px; text-align: center; font-family: Helvetica, sans-serif; font-weight: 100; line-height: 36px; font-size: 22px; color: #999999; background: #292929; } span.danger { background: #61000C; font-weight: 700; color: #D0021B; } span.danger:after { content: "!"; } </style> </head> <body> <div id="logo"></div> <form><textarea id="input" placeholder="Paste here ..." autofocus></textarea></form> <div id="output"></div> <script type="text/javascript"> function isUnicoder(char) { let reg = new RegExp("[-qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM!@#$%^&*()1234567890<>?:'\u0020\".;+=_~`,/]"); return reg.test(char); }; // creates local UnicodeDetector var and uses the init property var UnicodeDetector = function() { this.init(); }; // sets our vars up UnicodeDetector.prototype.init = function() { this.msgArea = document.getElementById('input'); this.charContainer = document.getElementById('output'); // Creates a array of characters common in coding languages not to be shown but not indicative of hidden finger printing this.mapGSMExtended = ["\n",'{','}','\\','[',']','|',' ']; }; UnicodeDetector.prototype.renderCharContainer= function() { // sets the var str = to whatever is inside the msg area set in the above function var str = this.msgArea.value; // sets our div that displays the text to nothing because each time it runs over the whole string this.charContainer.innerHTML = ''; // for loop the length of our str val for(var i = 0; i < str.length; i++) { // get the char at current position in the string var ch = str.charAt(i); console.log(ch); // checks it through our isUnicoder function var isUnicode = isUnicoder(ch); // sets attr value to the html output of the char var attr = { html:ch }; // if the char is a space or a new line or a tab, print a space (ch === ' ' || ch === "\n" || ch === " ") ? attr.html = '&nbsp;' : ""; // if isUnicode is false, append class danger to make it red if(!isUnicode) { attr.class = 'danger'; } // Check if there is any possibly harmless hidden character if(this.mapGSMExtended.indexOf(ch) !== -1) { attr.class = 'warning'; } // sets our created attr to a span element var brick = document.createElement("SPAN"); brick.innerHTML = attr.html; brick.className = attr.class; // appends our brick to the char container we set in init this.charContainer.appendChild(brick); } }; // creates a new unicode object var u = new UnicodeDetector(); // on the change in the input area run the render function u.msgArea.addEventListener('input', function() { u.renderCharContainer(); }); </script> </body> </html>