├── python
├── webworker.js
├── py-worker.js
└── identicon.py
├── worker.js
├── LICENSE
├── index.html
├── script.js
└── README.md
/python/webworker.js:
--------------------------------------------------------------------------------
1 | importScripts("https://cdn.jsdelivr.net/pyodide/v0.19.1/full/pyodide.js");
2 |
3 | async function loadPyodideAndPackages() {
4 | self.pyodide = await loadPyodide({
5 | indexURL: "https://cdn.jsdelivr.net/pyodide/v0.19.1/full/",
6 | });
7 | }
8 | let pyodideReadyPromise = loadPyodideAndPackages();
9 |
10 | self.onmessage = async (event) => {
11 | await pyodideReadyPromise;
12 |
13 | const { id, python, ...context } = event.data;
14 | console.log(context);
15 | for (const key of Object.keys(context)) {
16 | self[key] = context[key];
17 | }
18 |
19 | try {
20 | await self.pyodide.loadPackagesFromImports(python);
21 | let results = await self.pyodide.runPythonAsync(python);
22 | self.postMessage({ results: results.toJs(), id });
23 | } catch (error) {
24 | self.postMessage({ error: error.message, id });
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/worker.js:
--------------------------------------------------------------------------------
1 | self.importScripts(
2 | "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/core.min.js",
3 | );
4 |
5 | self.importScripts(
6 | "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/md5.min.js",
7 | );
8 |
9 | // adapted from https://stackoverflow.com/a/29433028
10 | function* nibble({ words }) {
11 | for (let i = 0; i < 8; ++i) {
12 | let byte = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
13 |
14 | let hi = byte & 0xf0;
15 | let lo = byte & 0x0f;
16 |
17 | yield hi >> 4;
18 | yield lo;
19 | }
20 | }
21 |
22 | function isParityMatch(nibble, lsb) {
23 | return !lsb || (nibble & 1) == Number(lsb);
24 | }
25 |
26 | self.addEventListener("message", function (e) {
27 | const pattern = e.data;
28 |
29 | for (let i = 0; i < 100_000_000; ++i) {
30 | let hash = CryptoJS.MD5(String(i));
31 | let nibbles = Array.from(nibble(hash));
32 | if (nibbles.every((n, j) => isParityMatch(n, pattern[j]))) {
33 | self.postMessage(i);
34 | }
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 kashav
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 |
5 |
6 | what's that identicon
7 |
52 |
53 |
54 |
55 |
56 | Go
57 |
58 | ( )
59 | Click on pixels to toggle them and then click Go.
60 |
61 | Source code on GitHub.
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/python/py-worker.js:
--------------------------------------------------------------------------------
1 | // https://pyodide.org/en/stable/usage/webworker.html
2 |
3 | const pyodideWorker = new Worker("./webworker.js");
4 |
5 | const callbacks = {};
6 |
7 | pyodideWorker.onmessage = (event) => {
8 | const { id, ...data } = event.data;
9 | const onSuccess = callbacks[id];
10 | delete callbacks[id];
11 | onSuccess(data);
12 | };
13 |
14 | const asyncRun = (() => {
15 | let id = 0;
16 | return (script, context) => {
17 | id = (id + 1) % Number.MAX_SAFE_INTEGER;
18 | return new Promise((onSuccess) => {
19 | callbacks[id] = onSuccess;
20 | pyodideWorker.postMessage({
21 | ...context,
22 | python: script,
23 | id,
24 | });
25 | });
26 | };
27 | })();
28 |
29 | export { asyncRun };
30 |
31 | // import { asyncRun } from "./py-worker.js";
32 |
33 | // async function computeIds() {
34 | // const output = document.querySelector("#output");
35 | // output.innerText = "";
36 |
37 | // const script = `
38 | // import hashlib
39 | // from js import pattern
40 |
41 | // def nibble(digest):
42 | // for byte in digest[:8]:
43 | // hi, lo = byte & 0xF0, byte & 0x0F
44 | // yield hi >> 4
45 | // yield lo
46 |
47 | // def is_parity_match(c, v):
48 | // return c == "x" or v & 1 == int(c)
49 |
50 | // def md5_test(n, pattern):
51 | // b = bytes(str(n), "utf8")
52 | // h = hashlib.md5(b)
53 | // return all(is_parity_match(c, v) for v, c in zip(nibble(h.digest()), pattern))
54 |
55 | // [i for i in range(100_000_000) if md5_test(i, pattern)]
56 | // `;
57 |
58 | // let { result, error } = await asyncRun(script, {
59 | // pattern: getBitRepr(),
60 | // });
61 |
62 | // if (error) {
63 | // console.error(error);
64 | // }
65 |
66 | // for (let result of results) {
67 | // output.innerText += result + "\n";
68 | // }
69 | // }
70 |
--------------------------------------------------------------------------------
/python/identicon.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import random
3 | import sys
4 |
5 |
6 | def is_parity_match(c, v):
7 | return c == "x" or v & 1 == int(c)
8 |
9 |
10 | def gen(c):
11 | v = random.getrandbits(4)
12 | while not is_parity_match(c, v):
13 | v = random.getrandbits(4)
14 |
15 | return v
16 |
17 |
18 | def gen_int(pair):
19 | a, b = pair
20 |
21 | n = gen(a)
22 | n <<= 4
23 | n ^= gen(b)
24 |
25 | return n
26 |
27 |
28 | def gen_matching_bytes():
29 | if len(sys.argv) != 2:
30 | sys.exit(1)
31 |
32 | s = sys.argv[1] # parsed pattern string (not a grid!)
33 |
34 | ints = map(gen_int, s.split(" "))
35 |
36 | for i, n in enumerate(ints):
37 | print(f"bytes[{i}] = {n};")
38 |
39 |
40 | def nibble(digest):
41 | # nibble the first 8 chunks in 4 bit increments, see nibbler.rs
42 | for byte in digest[:8]:
43 | hi, lo = byte & 0xF0, byte & 0x0F
44 | yield hi >> 4
45 | yield lo
46 |
47 |
48 | def md5_test(n, pattern):
49 | b = bytes(str(n), "utf8")
50 | h = hashlib.md5(b)
51 | if n == 12789:
52 | print(n, list(nibble(h.digest())), pattern)
53 | return all(is_parity_match(c, v) for v, c in zip(nibble(h.digest()), pattern))
54 |
55 |
56 | def parse_grid(grid):
57 | """
58 | converts a grid like
59 |
60 | K F A
61 | L G B
62 | M H C
63 | N I D
64 | O J E
65 |
66 | to
67 |
68 | ABCDEFGHIJKLMNO
69 | """
70 |
71 | rows = [row.strip().split(" ") for row in grid]
72 | return "".join(rows[row][col] for col in range(2, -1, -1) for row in range(0, 5))
73 |
74 |
75 | def do_all_tests(pattern):
76 | for i in range(100_000_000):
77 | if md5_test(i, pattern):
78 | print(i)
79 |
80 |
81 | def main():
82 | grid = """
83 | 0 0 1
84 | 1 0 0
85 | 0 0 1
86 | 1 0 1
87 | 1 0 1"""
88 |
89 | grid = grid.strip().split("\n")
90 |
91 | # grid = sys.stdin.readlines() # uncomment this to echo your own grid
92 | pattern = parse_grid(grid)
93 |
94 | do_all_tests(pattern)
95 |
96 |
97 | if __name__ == "__main__":
98 | main()
99 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | function getBoxElement(ix) {
2 | return document.querySelector(`.box[data-ix="${ix}"]`);
3 | }
4 |
5 | function handleBoxClick() {
6 | this.classList.toggle("on");
7 | this.classList.toggle("off");
8 |
9 | const mirror = getBoxElement(this.dataset.mirrorIx);
10 |
11 | if (mirror != this) {
12 | mirror.classList.toggle("on");
13 | mirror.classList.toggle("off");
14 | }
15 | }
16 |
17 | function makeBox(ix, mirrorIx, clazz) {
18 | const box = document.createElement("div");
19 | box.dataset.ix = ix;
20 | box.dataset.mirrorIx = mirrorIx;
21 | box.classList.add("box");
22 | box.classList.add(clazz);
23 | box.addEventListener("click", handleBoxClick.bind(box));
24 | return box;
25 | }
26 |
27 | function makeBoxes() {
28 | const boxes = [];
29 |
30 | for (let col = 2; col >= 0; --col) {
31 | for (let row = 0; row < 5; ++row) {
32 | const ix = row * 5 + col;
33 | const mirrorIx = row * 5 + (4 - col);
34 |
35 | const clazz = Math.random() > 0.5 ? "on" : "off";
36 |
37 | const box = makeBox(ix, mirrorIx, clazz);
38 | const mirror = ix == mirrorIx ? null : makeBox(mirrorIx, ix, clazz);
39 |
40 | boxes.push(box);
41 | if (mirror) boxes.push(mirror);
42 | }
43 | }
44 |
45 | boxes.sort((a, b) => a.dataset.ix - b.dataset.ix);
46 |
47 | const container = document.getElementById("container");
48 |
49 | for (let box of boxes) {
50 | container.appendChild(box);
51 | }
52 | }
53 |
54 | function getBitRepr() {
55 | let repr = "";
56 |
57 | for (let col = 2; col >= 0; --col) {
58 | for (let row = 0; row < 5; ++row) {
59 | const ix = row * 5 + col;
60 | const box = getBoxElement(ix);
61 | repr += box.classList.contains("on") ? "0" : "1";
62 | }
63 | }
64 |
65 | return repr;
66 | }
67 |
68 | function setAnchorWithId(id, href, innerText) {
69 | const anchor = document.getElementById(id);
70 | anchor.setAttribute("href", href);
71 | anchor.innerText = innerText;
72 | }
73 |
74 | async function handleAnchorClick() {
75 | try {
76 | const url = `https://api.github.com/user/${this.dataset.id}`;
77 | const resp = await fetch(url);
78 | const json = await resp.json();
79 |
80 | if (!json || !json.login) {
81 | throw new Error("failed to retrieve login from api");
82 | }
83 |
84 | const accountUrl = `github.com/${json.login}`;
85 | setAnchorWithId("account-url", "https://" + accountUrl, accountUrl);
86 |
87 | const pngHref = `https://github.com/identicons/${json.login}.png`;
88 | const pngInnerText = `identicon ${this.dataset.id}`;
89 | setAnchorWithId("png-url", pngHref, pngInnerText);
90 |
91 | const pngUrlWrapper = document.getElementById("png-url-wrapper");
92 | pngUrlWrapper.style.display = "inline";
93 | } catch (error) {
94 | console.error(error);
95 | alert(error);
96 | }
97 | }
98 |
99 | let worker;
100 | function startWorker() {
101 | const output = document.getElementById("output");
102 | output.innerHTML = "";
103 |
104 | const button = document.querySelector("button");
105 | button.onclick = stopWorker;
106 | button.innerText = "Stop";
107 |
108 | if (worker) worker.terminate();
109 |
110 | worker = new Worker("worker.js");
111 | worker.addEventListener("message", (event) => {
112 | const id = event.data;
113 | const anchor = document.createElement("a");
114 | anchor.dataset.id = id;
115 | anchor.setAttribute("href", "javascript:void(0);");
116 | anchor.addEventListener("click", handleAnchorClick.bind(anchor));
117 | anchor.innerText = id;
118 | output.appendChild(anchor);
119 | });
120 |
121 | worker.postMessage(getBitRepr());
122 | }
123 |
124 | function stopWorker() {
125 | if (worker) worker.terminate();
126 |
127 | const button = document.querySelector("button");
128 | button.onclick = startWorker;
129 | button.innerText = "Go";
130 | }
131 |
132 | window.onload = function () {
133 | makeBoxes();
134 | document.querySelector("button").onclick = startWorker;
135 | };
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # identicon
2 |
3 | Silly little tool to reverse-map Github identicons to user accounts.
4 |
5 | Built by inspecting [dgraham/identicon][dgraham] and [stewartlord/identicon.js][identicon.js], and just doing the reverse of what they're doing.
6 |
7 | ### How does it work?
8 |
9 | Here's how the original algorithm generates the graphic:
10 |
11 | 1. It hashes the user id. (The string version of it! Not the integer.)
12 |
13 | 2. Then [nibbles][bit-nibbler] the first 8 chunks of the hash digest in 4 bit increments. A chunk is 8 bits, so 8 chunks produce 16 nibbles.
14 |
15 | 3. It uses the parity bit of each nibble (aka is it even or odd?) to decide whether a particular pixel in the 5x5 avatar is filled in or not. It starts with the middle column and moves outwards, from top to bottom. The 1st and the 2nd column are reflected along the 3rd column, so it's actually only using 3×5 nibbles.
16 |
17 | Assuming a digest of the form `AB CD EF GH IJ KL MN OP`, it paints pixels in the following order:
18 |
19 | ```
20 | start
21 | ▼
22 | K F A F′ K′
23 | L G B G′ L′
24 | M H C H′ M′
25 | N I D I′ N′
26 | O J E J′ O′
27 | ▲
28 | end
29 | ```
30 |
31 | The `P` nibble is unused. `F′` to `O′` are just mirrors of their non-prime values.
32 |
33 | 4. It also does some rgb/hsl math with the lower 28 bits of the digest to choose a colour for the pixels, but I didn't bother learning how that works. Maybe you can figure it out and add support.
34 |
35 | And so this program just implements the reverse of that. It computes the md5 for every number and checks that against the user-supplied bit string. The bit string is represented as a html grid, and converted to a digest pattern at compute time.
36 |
37 | Read the Python code, it's easier to follow: [`identicon.py`](./python/identicon.py). (I was originally going to use Pyodide for this, but that didn't really work out. The code is still there just in case.)
38 |
39 | ##### Here's an example.
40 |
41 | This walks through searching for a user id that yields the middle avatar from [Jason's blog post][jason].
42 |
43 | Assuming 0s represent pixels that are filled in and 1s represent pixels that aren't, the initial grid is:
44 |
45 | ```
46 | 1 0 0 0 1
47 | 0 0 0 0 0
48 | 0 1 1 1 0
49 | 1 0 0 0 1
50 | 0 0 1 0 0
51 | ```
52 |
53 | or, since we only care about the first 3 columns:
54 |
55 | ```
56 | 1 0 0
57 | 0 0 0
58 | 0 1 1
59 | 1 0 0
60 | 0 0 1
61 | ```
62 |
63 | Loosely, this means we're seeking a digest of the form `00 10 10 01 00 10 01 0-`, following the `A` to `O` example from above.
64 |
65 | Specifically, however, it means that we're seeking integers with MD5
66 | digests whose 8 most significant chunks resemble the following, in binary:
67 |
68 | ```
69 | xxx0xxx0
70 | xxx1xxx0
71 | xxx1xxx0
72 | xxx0xxx1
73 | xxx0xxx0
74 | xxx1xxx0
75 | xxx0xxx1
76 | xxx0xxxx
77 | ```
78 |
79 | We only care about the parity bit for each nibble, so the `x`s can be anything.
80 |
81 | ###### And now taking the number 2013 as an example:
82 |
83 | ```python3
84 | >>> import hashlib
85 | >>> h = hashlib.md5(b"2013")
86 | >>> for byte in h.digest()[:8]:
87 | ... print(format(byte, "08b")) # pad to 8 bits
88 | 10000000
89 | 00111000
90 | 11011010
91 | 10001001
92 | 11100100
93 | 10011010
94 | 11000101
95 | 11101010
96 | ```
97 |
98 | The output matches the pattern from above! So theoretically, the account with
99 | an id of 2013 should have a robot as its default avatar. And it does: [jeffsmith.png][jeffsmith.png]! (the username mapping happens [here][api])
100 |
101 | ### Important links
102 |
103 | - https://github.blog/2013-08-14-identicons/
104 | - https://github.com/dgraham/identicon
105 | - https://github.com/stewartlord/identicon.js
106 | - https://api.github.com/user/8116382
107 | - https://api.github.com/users/kashav
108 | - https://github.com/identicons/kashav.png
109 |
110 |
111 | [jason]: https://github.blog/2013-08-14-identicons/
112 | [dgraham]: https://github.com/dgraham/identicon
113 | [identicon.js]: https://github.com/stewartlord/identicon.js
114 |
115 | [jeffsmith.png]: https://github.com/identicons/jeffsmith.png
116 | [api]: https://api.github.com/user/2013
117 |
118 | [bit-nibbler]: https://en.wikipedia.org/wiki/Bit_nibbler
119 |
--------------------------------------------------------------------------------