├── favicon.ico
├── .gitignore
├── LICENSE
├── index.html
├── hidden
├── decrypt-bookmarklet.js
├── hidden.js
├── README.md
└── index.html
├── bruteforce
├── bruteforce.js
└── index.html
├── api.js
├── favicon.svg
├── decrypt
├── decrypt.js
└── index.html
├── index.js
├── style.css
├── b64.js
├── create
├── create.js
└── index.html
├── corner-ribbon-minified.svg
├── README.md
└── corner-ribbon.svg
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jstrieb/link-lock/HEAD/favicon.ico
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Vim temporary swap files
2 | *.swp
3 | *.swo
4 |
5 | # Backed up copies of old files
6 | *.bak
7 |
8 | # Generated SSL key file for SSL python http.server
9 | *.pem
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Jacob Strieb
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/hidden/decrypt-bookmarklet.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | var b64 = (() => {
3 | function generateIndexDict(a) {
4 | let result = {};
5 | for (let i = 0; i < a.length; i++) {
6 | result[a[i]] = i;
7 | }
8 | return result;
9 | }
10 |
11 | const _a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
12 | const _aRev = generateIndexDict(_a);
13 | _aRev["-"] = _aRev["+"];
14 | _aRev["_"] = _aRev["/"];
15 |
16 | const _enc = new TextEncoder("utf-8");
17 | const _dec = new TextDecoder("utf-8");
18 |
19 | return {
20 |
21 | decode: function(s) {
22 | return this.binaryToAscii(this.base64ToBinary(s));
23 | },
24 |
25 | encode: function(s) {
26 | return this.binaryToBase64(this.asciiToBinary(s));
27 | },
28 |
29 | asciiToBinary: function(text) {
30 | return _enc.encode(text);
31 | },
32 |
33 |
34 | binaryToAscii: function(binary) {
35 | return _dec.decode(binary);
36 | },
37 |
38 |
39 | binaryToBase64: function(originalBytes) {
40 | let length = originalBytes.length;
41 | let added = (length % 3 == 0) ? 0 : (3 - length % 3);
42 | let bytes = new Uint8Array(length + added);
43 | bytes.set(originalBytes);
44 |
45 | let output = "";
46 | for (let i = 0; i < bytes.length; i += 3) {
47 | output += _a[ bytes[i] >>> 2 ];
48 | output += _a[ ((bytes[i] & 0x3) << 4) | (bytes[i + 1] >>> 4) ];
49 | output += _a[ ((bytes[i + 1] & 0xF) << 2) | (bytes[i + 2] >>> 6) ];
50 | output += _a[ bytes[i + 2] & 0x3F ];
51 | }
52 |
53 | if (added > 0) {
54 | output = output.slice(0, -added) + ("=".repeat(added));
55 | }
56 |
57 | return output;
58 | },
59 |
60 | base64ToBinary: function(s) {
61 | let bytes = [];
62 |
63 | if (s.length % 4 == 1) {
64 | throw "Invalid base64 input";
65 | } else if (s.length % 4 != 0) {
66 | s += "=".repeat(4 - (s.length % 4));
67 | }
68 |
69 | for (let i = 0; i <= (s.length - 4); i += 4) {
70 | for (let j = 0; j < 4; j++) {
71 | if (s[i + j] != "=" && !(s[i + j] in _aRev)) {
72 | throw "Invalid base64 input";
73 | } else if (s[i + j] == "=" && Math.abs(s.length - (i + j)) > 2) {
74 | throw "Invalid base64 input";
75 | }
76 | }
77 |
78 | bytes.push((_aRev[s[i]] << 2) | (_aRev[s[i + 1]] >>> 4));
79 | if (s[i + 2] != "=") {
80 | bytes.push(((_aRev[s[i + 1]] & 0xF) << 4) | (_aRev[s[i + 2]] >>> 2));
81 | }
82 | if (s[i + 3] != "=") {
83 | bytes.push(((_aRev[s[i + 2]] & 0x3) << 6) | _aRev[s[i + 3]]);
84 | }
85 | }
86 |
87 | return new Uint8Array(bytes);
88 | }
89 |
90 | }
91 | })();
92 |
93 | const hash = window.location.hash.slice(1);
94 | try {
95 | const decoded = b64.decode(hash);
96 | const params = JSON.parse(decoded);
97 | if (params.unencrypted) {
98 | window.location.href = params.url;
99 | } else {
100 | window.location.href = "https://jstrieb.github.io/link-lock/" + window.location.hash;
101 | }
102 | } catch {
103 | window.location.replace("https://gmail.com");
104 | }
105 | })();
106 |
--------------------------------------------------------------------------------
/bruteforce/bruteforce.js:
--------------------------------------------------------------------------------
1 | function error(text) {
2 | const alert = document.querySelector(".alert");
3 | alert.innerText = text;
4 | alert.style.opacity = 1;
5 | }
6 |
7 | function onBruteForce() {
8 | if (!("importKey" in window.crypto.subtle)) {
9 | error("window.crypto not loaded. Please reload over https");
10 | return;
11 | }
12 | if (!("b64" in window && "apiVersions" in window)) {
13 | error("Important libraries not loaded!");
14 | return;
15 | }
16 |
17 | const urlText = document.querySelector("#encrypted-url").value;
18 | let url;
19 | try {
20 | url = new URL(urlText);
21 | } catch {
22 | error("Entered text is not a valid URL. Make sure it includes \"https://\" too!");
23 | return;
24 | }
25 |
26 | let params;
27 | try {
28 | params = JSON.parse(b64.decode(url.hash.slice(1)));
29 | } catch {
30 | error("The link appears corrupted.");
31 | return;
32 | }
33 |
34 | if (!("v" in params && "e" in params)) {
35 | error("The link appears corrupted. The encoded URL is missing necessary parameters.");
36 | return;
37 | }
38 |
39 | if (!(params["v"] in apiVersions)) {
40 | error("Unsupported API version. The link may be corrupted.");
41 | return;
42 | }
43 |
44 | const api = apiVersions[params["v"]];
45 |
46 | const encrypted = b64.base64ToBinary(params["e"]);
47 | const salt = "s" in params ? b64.base64ToBinary(params["s"]) : null;
48 | const iv = "i" in params ? b64.base64ToBinary(params["i"]) : null;
49 |
50 | const cset = document.querySelector("#charset").value.split("");
51 | if (charset == "") {
52 | error("Charset cannot be empty.");
53 | return;
54 | }
55 |
56 | var progress = {
57 | tried: 0,
58 | total: 0,
59 | len: 0,
60 | overallTotal: 0,
61 | done: false,
62 | startTime: performance.now()
63 | };
64 |
65 | async function tryAllLen(prefix, len, curLen) {
66 | if (progress.done) return;
67 | if (len == curLen) {
68 | progress.tried++;
69 | try {
70 | await api.decrypt(encrypted, prefix, salt, iv);
71 | document.querySelector("#output").value = prefix;
72 | progress.done = true;
73 | error("Completed!");
74 | } catch {}
75 | return;
76 | }
77 | for (let i=0; i < cset.length; i++) {
78 | let c = cset[i];
79 | await tryAllLen(prefix + c, len, curLen + 1);
80 | }
81 | }
82 |
83 | function progressUpdate() {
84 | if (progress.done) {
85 | clearInterval();
86 | return;
87 | }
88 | let delta = performance.now() - progress.startTime;
89 | error(`Trying ${progress.total} passwords of length ${progress.len} – ${Math.round(100000 * progress.tried / progress.total)/1000}% complete. Testing ${Math.round(1000000 * (progress.overallTotal + progress.tried) / delta)/1000} passwords per second.`);
90 | }
91 |
92 | (async () => {
93 | for (let len=0; !progress.done; len++) {
94 | progress.overallTotal += progress.tried;
95 | progress.tried = 0;
96 | progress.total = Math.pow(cset.length, len);
97 | progress.len = len;
98 | progressUpdate();
99 | await tryAllLen("", len, 0);
100 | }
101 | })();
102 |
103 | setInterval(progressUpdate, 4000);
104 | }
105 |
--------------------------------------------------------------------------------
/api.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by Jacob Strieb
3 | * May 2020
4 | */
5 |
6 |
7 |
8 | /*******************************************************************************
9 | * Global Variables
10 | ******************************************************************************/
11 |
12 | var LATEST_API_VERSION = "0.0.1";
13 |
14 | var apiVersions = {};
15 |
16 |
17 |
18 | /*******************************************************************************
19 | * API Version 0.0.1 (Latest)
20 | ******************************************************************************/
21 |
22 | apiVersions["0.0.1"] = {
23 |
24 | // Static salt and initialization vector for shorter, less secure links
25 | salt: Uint8Array.from([236, 231, 167, 249, 207, 95, 201, 235, 164, 98, 246,
26 | 26, 176, 174, 72, 249]),
27 |
28 | iv: Uint8Array.from([255, 237, 148, 105, 6, 255, 123, 202, 115, 130, 16,
29 | 116]),
30 |
31 |
32 | // Generate random salt and initialization vectors
33 | randomSalt: async function() {
34 | return await window.crypto.getRandomValues(new Uint8Array(16));
35 | },
36 |
37 | randomIv: async function() {
38 | return await window.crypto.getRandomValues(new Uint8Array(12));
39 | },
40 |
41 |
42 | // Import the raw, plain-text password and derive a key using a SHA-256 hash
43 | // and PBKDF2. Use the static salt for this version if one has not been given
44 | deriveKey: async function(password, salt=null) {
45 | let rawKey = await window.crypto.subtle.importKey(
46 | "raw",
47 | b64.asciiToBinary(password),
48 | { name: "PBKDF2" },
49 | false,
50 | [ "deriveBits", "deriveKey" ]
51 | );
52 | return await window.crypto.subtle.deriveKey(
53 | {
54 | name: "PBKDF2",
55 | salt: salt == null ? this.salt : salt,
56 | iterations: 100000,
57 | hash: "SHA-256"
58 | },
59 | rawKey,
60 | {
61 | name: "AES-GCM",
62 | length: 256
63 | },
64 | true,
65 | [ "encrypt", "decrypt" ]
66 | );
67 | },
68 |
69 |
70 | // Encrypt the text using AES-GCM with a key derived from the password. Takes
71 | // in strings for text and password, as well as optional salt and iv. Uses the
72 | // static iv for this version if one is not given.
73 | encrypt: async function(text, password, salt=null, iv=null) {
74 | let key = await this.deriveKey(password, salt=salt);
75 | let encryptedBinary = await window.crypto.subtle.encrypt(
76 | {
77 | name: "AES-GCM",
78 | iv: iv == null ? this.iv : iv
79 | },
80 | key,
81 | b64.asciiToBinary(text)
82 | );
83 | return encryptedBinary;
84 | },
85 |
86 |
87 | // Decrypt the text using AES-GCM with a key derived from the password. Takes
88 | // in text as an ArrayBuffer and a string password, as well as optional salt
89 | // and iv. Uses the static iv for this version if one is not given.
90 | decrypt: async function(text, password, salt=null, iv=null) {
91 | let key = await this.deriveKey(password, salt=salt);
92 | let decryptedBinary = await window.crypto.subtle.decrypt(
93 | {
94 | name: "AES-GCM",
95 | iv: iv == null ? this.iv : iv
96 | },
97 | key,
98 | new Uint8Array(text)
99 | );
100 | return b64.binaryToAscii(decryptedBinary);
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
90 |
--------------------------------------------------------------------------------
/bruteforce/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Brute Force Link Lock
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
38 |
39 |
40 |
Brute Force Link Lock URLs
41 |
"Brute forcing" a password is the act of guessing it by brute force – literally trying all passwords until one works. This page is a simple, proof-of-concept brute force application. It is designed to decrypt Link Lock URLs by trying every single possible password. It is not optimized in any way, and does minimal error-checking.
42 |
For more information about brute forcing Link Lock URLs, read the open GitHub issue about it.
43 |
For a command-line brute force application that performs parallel processing using all cores of the CPU, check out this project.