├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── arts ├── 9999.art └── target.art ├── crypto.py ├── main.py └── randomart.py /.gitignore: -------------------------------------------------------------------------------- 1 | keys/ 2 | arts/* 3 | !arts/9999.art 4 | !arts/target.art 5 | 6 | ### VisualStudioCode ### 7 | .vscode 8 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | cryptography = "*" 10 | tqdm = "*" 11 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "5cac99230b32cfa5359b85c2df2470062f0072e1a62acce80f021796ba82ec70" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "cffi": { 18 | "hashes": [ 19 | "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", 20 | "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", 21 | "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", 22 | "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", 23 | "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", 24 | "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", 25 | "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", 26 | "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", 27 | "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", 28 | "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", 29 | "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", 30 | "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", 31 | "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", 32 | "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", 33 | "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", 34 | "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", 35 | "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", 36 | "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", 37 | "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", 38 | "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", 39 | "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", 40 | "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", 41 | "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", 42 | "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", 43 | "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", 44 | "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", 45 | "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", 46 | "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", 47 | "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", 48 | "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", 49 | "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", 50 | "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", 51 | "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", 52 | "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", 53 | "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", 54 | "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", 55 | "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", 56 | "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", 57 | "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", 58 | "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", 59 | "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", 60 | "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", 61 | "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", 62 | "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", 63 | "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", 64 | "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", 65 | "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", 66 | "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", 67 | "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", 68 | "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", 69 | "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", 70 | "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", 71 | "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", 72 | "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", 73 | "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", 74 | "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", 75 | "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", 76 | "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", 77 | "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", 78 | "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", 79 | "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", 80 | "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", 81 | "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", 82 | "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", 83 | "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", 84 | "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", 85 | "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" 86 | ], 87 | "markers": "platform_python_implementation != 'PyPy'", 88 | "version": "==1.17.1" 89 | }, 90 | "cryptography": { 91 | "hashes": [ 92 | "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7", 93 | "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3", 94 | "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183", 95 | "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69", 96 | "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a", 97 | "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62", 98 | "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911", 99 | "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7", 100 | "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a", 101 | "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41", 102 | "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83", 103 | "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12", 104 | "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864", 105 | "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf", 106 | "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c", 107 | "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2", 108 | "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b", 109 | "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0", 110 | "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4", 111 | "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9", 112 | "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008", 113 | "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862", 114 | "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009", 115 | "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7", 116 | "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f", 117 | "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026", 118 | "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f", 119 | "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd", 120 | "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420", 121 | "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14", 122 | "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00" 123 | ], 124 | "index": "pypi", 125 | "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'", 126 | "version": "==44.0.1" 127 | }, 128 | "pycparser": { 129 | "hashes": [ 130 | "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", 131 | "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" 132 | ], 133 | "markers": "python_version >= '3.8'", 134 | "version": "==2.22" 135 | }, 136 | "tqdm": { 137 | "hashes": [ 138 | "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", 139 | "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2" 140 | ], 141 | "index": "pypi", 142 | "markers": "python_version >= '3.7'", 143 | "version": "==4.67.1" 144 | } 145 | }, 146 | "develop": {} 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSH Artist 2 | 3 | Want to find an SSH key with a specific hash visualization pattern? If so, then 4 | this is the tool for you. If not, don't make me SSH into your servers with their 5 | ugly keys! 6 | 7 | ## Visualizing Public Keys 8 | 9 | OpenSSH 5.1 introduced a visual hash representation of public keys. From its changelog: 10 | 11 | ``` 12 | * Introduce experimental SSH Fingerprint ASCII Visualization to ssh(1) 13 | and ssh-keygen(1). Visual fingerprint display is controlled by a new 14 | ssh_config(5) option "VisualHostKey". The intent is to render 15 | SSH host keys in a visual form that is amenable to easy recall and 16 | rejection of changed host keys. This technique inspired by the 17 | graphical hash visualization schemes known as "random art[*]", and 18 | by Dan Kaminsky's musings at 23C3 in Berlin. 19 | 20 | Fingerprint visualization in is currently disabled by default, as the 21 | algorithm used to generate the random art is still subject to change. 22 | 23 | [*] "Hash Visualization: a New Technique to improve Real-World 24 | Security", Perrig A. and Song D., 1999, International Workshop on 25 | Cryptographic Techniques and E-Commerce (CrypTEC '99) 26 | http://sparrow.ece.cmu.edu/~adrian/projects/validation/validation.pdf 27 | ``` 28 | 29 | You can visualize your existing keys with the following command: 30 | 31 | ``` 32 | ssh-keygen -lv -f ~/.ssh/id_ed25519.pub 33 | ``` 34 | 35 | It's probably gibberish, as expected from all the randomness involved. My 36 | original key looked like this: 37 | 38 | ``` 39 | +--[ED25519 256]--+ 40 | | | 41 | | .| 42 | | . . o.| 43 | | . .o..=.o| 44 | | o S o.ooo=.| 45 | | o o E+o*o*| 46 | | = +o.X=**| 47 | | = o.+*+B| 48 | | . o+== | 49 | +----[SHA256]-----+ 50 | ``` 51 | 52 | ## Good Looking Public Keys 53 | 54 | Once visualization is introduced, so is aesthetics. This feature presents a 55 | great opportunity to fight against truly random key generation in order to trade 56 | security for arbitrary human desires. 57 | 58 | For example, I wanted a key that looked like this: 59 | 60 | ``` 61 | +--[ED25519 256]--+ 62 | | | 63 | | .OOO OOO | 64 | | ..OOO OOO. | 65 | | ..OOO OOO. | 66 | | ..OOSOO. | 67 | | ..OEO. | 68 | | .... | 69 | | .. | 70 | | | 71 | +-----[SHA256]----+ 72 | ``` 73 | 74 | In order to get it, draw the desired end state on the `arts/target.art` and then 75 | start the artist creative process with 76 | 77 | ``` 78 | pipenv run python main.py 79 | ``` 80 | 81 | and kill the artist when patience is depleted. 82 | 83 | Here's what I got in (quite) a few hours: 84 | 85 | ``` 86 | +--[ED25519 256]--+ 87 | | .+.+ *oo. | 88 | | .*o+ =o+. | 89 | | E.oo.. +=. | 90 | | ..ooo@+oo | 91 | | ..+.S*B+ | 92 | | + ++.o | 93 | | o+ . | 94 | | .+ | 95 | | | 96 | +-----[SHA256]----+ 97 | ``` 98 | 99 | Still ugly, but at least it resembles something. 100 | -------------------------------------------------------------------------------- /arts/9999.art: -------------------------------------------------------------------------------- 1 | +--[ED25519 256]--+ 2 | | | 3 | | | 4 | | | 5 | | | 6 | | S | 7 | | | 8 | | | 9 | | | 10 | | | 11 | +-----[SHA256]----+ -------------------------------------------------------------------------------- /arts/target.art: -------------------------------------------------------------------------------- 1 | +--[ED25519 256]--+ 2 | | | 3 | | .OOO OOO | 4 | | ..OOO OOO. | 5 | | ..OOO OOO. | 6 | | ..OOSOO. | 7 | | ..OEO. | 8 | | .... | 9 | | .. | 10 | | | 11 | +-----[SHA256]----+ -------------------------------------------------------------------------------- /crypto.py: -------------------------------------------------------------------------------- 1 | from cryptography.hazmat.primitives.asymmetric import ed25519 2 | from cryptography.hazmat.primitives import serialization as crypto_serialization 3 | 4 | 5 | def generate_key(): 6 | # Generate an Ed25519 private key 7 | key = ed25519.Ed25519PrivateKey.generate() 8 | 9 | # Serialize the private key to OpenSSH format 10 | private_key = key.private_bytes( 11 | crypto_serialization.Encoding.PEM, 12 | crypto_serialization.PrivateFormat.OpenSSH, 13 | crypto_serialization.NoEncryption(), 14 | ) 15 | 16 | # Serialize the public key to OpenSSH format 17 | public_key = key.public_key().public_bytes( 18 | crypto_serialization.Encoding.OpenSSH, 19 | crypto_serialization.PublicFormat.OpenSSH, 20 | ) 21 | 22 | return private_key, public_key 23 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from operator import add 3 | from functools import reduce 4 | from multiprocessing import Pool 5 | from tqdm import tqdm 6 | from crypto import generate_key 7 | from randomart import generate_key_art 8 | 9 | HEAVY_SET = {"B", "O", "X", "@", "%", "&", "#", "/", "^", "S", "E"} 10 | LIGHT_SET = {".", "o", "+", "=", "*"} 11 | 12 | 13 | def diff(a_art: str, b_art: str) -> int: 14 | def diff_coin_pair(pair: tuple) -> int: 15 | a, b = pair 16 | if a == b: 17 | return 0 18 | if (a in LIGHT_SET and b in LIGHT_SET) or (a in HEAVY_SET and b in HEAVY_SET): 19 | return 1 20 | if (a in LIGHT_SET and b in HEAVY_SET) or (a in HEAVY_SET and b in LIGHT_SET): 21 | return 2 22 | if (a == " " and b in LIGHT_SET) or (a in LIGHT_SET and b == " "): 23 | return 50 24 | if (a == " " and b in HEAVY_SET) or (a in HEAVY_SET and b == " "): 25 | return 100 26 | 27 | raise ValueError(f"Unexpected character pair: '{a}'<>'{b}'") 28 | 29 | return reduce(add, map(diff_coin_pair, zip(a_art, b_art))) 30 | 31 | 32 | arts_dir = Path("./arts") 33 | keys_dir = Path("./keys") 34 | keys_dir.mkdir(exist_ok=True, parents=True) 35 | 36 | 37 | def search(_): 38 | target_art = (arts_dir / "target.art").read_text() 39 | art_files = sorted(arts_dir.glob("*.art")) 40 | best_diff = int(art_files[0].stem) 41 | 42 | private_key, public_key = generate_key() 43 | new_art = generate_key_art(public_key) 44 | new_diff = diff(target_art, new_art) 45 | 46 | if new_diff > 2000: # lame art threshold, don't bother saving 47 | return 48 | 49 | if new_diff < best_diff: 50 | print(f"New approximation found! Diff = {new_diff}") 51 | print(new_art) 52 | newest_art_file = f"{new_diff:04d}.art" 53 | Path(arts_dir / newest_art_file).write_text(new_art) 54 | 55 | private_key_file = keys_dir / "id_ed25519" 56 | private_key_file.write_text(private_key.decode()) 57 | public_key_file = keys_dir / "id_ed25519.pub" 58 | public_key_file.write_text(public_key.decode()) 59 | 60 | 61 | if __name__ == "__main__": 62 | attempts = 500_000_000 63 | with Pool(processes=20) as p: 64 | list(tqdm(p.imap(search, range(attempts)), total=attempts)) 65 | -------------------------------------------------------------------------------- /randomart.py: -------------------------------------------------------------------------------- 1 | # Derived from https://github.com/natmchugh/drunken-bishop 2 | # Licensed under MIT 3 | # Copyright (c) 2015 Nathaniel McHugh, 2018 Victor Villas 4 | 5 | import base64 6 | import hashlib 7 | import math 8 | 9 | 10 | def generate_key_art(public_key, digest="SHA256"): 11 | key_prefix, key_encoded = public_key.split(b" ", 2) 12 | key_decoded = base64.b64decode(key_encoded) 13 | hash_fn = { 14 | "SHA256": hashlib.sha256, 15 | "MD5": hashlib.md5, 16 | }[digest] 17 | key_type_name = { 18 | b"ssh-rsa": "RSA", 19 | b"ssh-dss": "DSA", 20 | b"ecdsa-sha2-nistp256": "ECDSA 256", 21 | b"ssh-ed25519": "ED25519 256", 22 | }[key_prefix] 23 | 24 | if key_prefix == b"ssh-rsa": 25 | key_type_name += " %s" % get_rsa_key_length(key_decoded) 26 | 27 | key_digest = hash_fn(key_decoded).hexdigest() 28 | hash_blocks = [int(key_digest[i : i + 8], 16) for i in range(0, len(key_digest), 8)] 29 | return str(Fingerprint(hash_blocks, key_type_name, digest)) 30 | 31 | 32 | def get_rsa_key_length(key): 33 | hex_str = key.encode("hex") 34 | start = 8 35 | typeLength = int(hex_str[0:start], 16) 36 | start += typeLength * 2 37 | lengthExponent = int(hex_str[start : start + 8], 16) 38 | start += 8 + 2 * lengthExponent 39 | lengthKey = int(hex_str[start : start + 8], 16) 40 | start += 8 41 | key = hex_str[start : start + 2 * lengthKey] 42 | n = int(key, 16) 43 | return int(math.log(n, 2)) + 1 44 | 45 | 46 | class Bishop: 47 | def __init__(self, pos): 48 | self.pos = pos 49 | 50 | def coords(self): 51 | x = self.pos % 17 52 | y = int(math.floor(self.pos / 17)) 53 | return [x, y] 54 | 55 | def type(self): 56 | [x, y] = self.coords() 57 | if y == 0: 58 | if x == 0: 59 | return "a" 60 | if x == 16: 61 | return "b" 62 | return "T" 63 | if x == 0: 64 | if y == 8: 65 | return "c" 66 | return "L" 67 | if x == 16: 68 | if y == 8: 69 | return "d" 70 | return "R" 71 | if y == 8: 72 | return "B" 73 | return "M" 74 | 75 | def move(self, step): 76 | w = 0 77 | squareType = self.type() 78 | # quite literally corner cases 79 | if "a" == squareType: 80 | w = {0: 18, 1: 17, 2: 1}.get(step, 0) 81 | if "b" == squareType: 82 | w = {0: 17, 1: 16, 3: 1}.get(step, 0) 83 | if "c" == squareType: 84 | w = {0: 1, 2: -16, 3: -17}.get(step, 0) 85 | if "d" == squareType: 86 | w = {1: -1, 2: -17, 3: -18}.get(step, 0) 87 | if "R" == squareType and step % 2 == 1: 88 | w = -1 89 | if "T" == squareType and step < 2: 90 | w = 17 91 | if "B" == squareType and step > 1: 92 | w = -17 93 | if "L" == squareType and step % 2 == 0: 94 | w = 1 95 | d = { 96 | 0: -18, 97 | 1: -16, 98 | 2: 16, 99 | 3: 18, 100 | }[step] 101 | self.pos += d + w 102 | 103 | def location(self): 104 | return self.pos 105 | 106 | 107 | class Atrium: 108 | def __init__(self, bishop, key_type, digest): 109 | self.bishop = bishop 110 | self.counts = [0] * 153 111 | self.counts[76] = 15 112 | self.key_type = key_type 113 | self.digest = digest 114 | 115 | def move(self, step): 116 | self.bishop.move(step) 117 | if self.counts[self.bishop.location()] < 15: 118 | self.counts[self.bishop.location()] += 1 119 | 120 | def stop(self, step): 121 | self.bishop.move(step) 122 | self.counts[self.bishop.location()] = 16 123 | 124 | def coin(self, count): 125 | return { 126 | 0: " ", 127 | 1: ".", 128 | 2: "o", 129 | 3: "+", 130 | 4: "=", 131 | 5: "*", 132 | 6: "B", 133 | 7: "O", 134 | 8: "X", 135 | 9: "@", 136 | 10: "%", 137 | 11: "&", 138 | 12: "#", 139 | 13: "/", 140 | 14: "^", 141 | 15: "S", 142 | 16: "E", 143 | }.get(count) 144 | 145 | def draw(self): 146 | type_str = "[" + self.key_type + "]-" 147 | output = "+" + type_str.center(17, "-") + "+\n" 148 | for idx, val in enumerate(self.counts): 149 | coin = self.coin(val) 150 | if idx % 17 == 0: 151 | output += "|" 152 | output += coin 153 | if (idx + 1) % 17 == 0: 154 | output += "|\n" 155 | hash_str = "[" + self.digest + "]" 156 | output += "+" + hash_str.center(17, "-") + "+" 157 | return output 158 | 159 | 160 | class Fingerprint: 161 | def __init__(self, hash, key_type, digest): 162 | bishop = Bishop(76) 163 | self.atrium = Atrium(bishop, key_type, digest) 164 | moves = self.hash_to_moves(hash) 165 | lastmove = moves.pop() 166 | for move in moves: 167 | self.atrium.move(move) 168 | self.atrium.stop(lastmove) 169 | 170 | def __str__(self): 171 | return self.atrium.draw() 172 | 173 | def hash_to_moves(self, hash): 174 | moves = [] 175 | for word in hash: 176 | for pair in (3, 2, 1, 0): 177 | shift = pair * 8 178 | byte = (word & (255 << shift)) >> shift 179 | for step in range(0, 8, 2): 180 | mask = 3 << step 181 | move = (byte & mask) >> step 182 | moves.append(move) 183 | return moves 184 | --------------------------------------------------------------------------------