├── .gitignore ├── setup.py ├── UNLICENSE ├── README.md └── humanhash.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | *.pyo 4 | .DS_Store 5 | MANIFEST 6 | build/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from distutils.core import setup 5 | 6 | setup( 7 | name='humanhash', 8 | version='0.0.1', 9 | description='Human-readable representations of digests.', 10 | author='Zachary Voase', 11 | author_email='z@zacharyvoase.com', 12 | url='http://github.com/zacharyvoase/humanhash', 13 | py_modules=['humanhash'], 14 | ) 15 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # humanhash 2 | 3 | humanhash provides human-readable representations of digests. 4 | 5 | 6 | ## Example 7 | 8 | >>> import humanhash 9 | 10 | >>> digest = '7528880a986c40e78c38115e640da2a1' 11 | >>> humanhash.humanize(digest) 12 | 'three-georgia-xray-jig' 13 | >>> humanhash.humanize(digest, words=6) 14 | 'high-mango-white-oregon-purple-charlie' 15 | 16 | >>> humanhash.uuid() 17 | ('potato-oranges-william-friend', '9d2278759ae24698b1345525bd53358b') 18 | 19 | 20 | ## Caveats 21 | 22 | Don't store the humanhash output, as its statistical uniqueness is only around 23 | 1 in 4.3 billion. Its intended use is as a human-readable (and, most 24 | importantly, **memorable**) representation of a longer digest, unique enough 25 | for display in a user interface, where a user may need to remember or verbally 26 | communicate the identity of a hash, without having to remember a 40-character 27 | hexadecimal sequence. Nevertheless, you should keep original digests around, 28 | then pass them through `humanize()` only as you're displaying them. 29 | 30 | 31 | ## How It Works 32 | 33 | The procedure for generating a humanhash involves compressing the input to a 34 | fixed length (default: 4 bytes), then mapping each of these bytes to a word in 35 | a pre-defined wordlist (a default wordlist is supplied with the library). This 36 | algorithm is consistent, so the same input, given the same wordlist, will 37 | always give the same output. You can also use your own wordlist, and specify a 38 | different number of words for output. 39 | 40 | 41 | ## Inspiration 42 | 43 | * [Chroma-Hash][]: a human-viewable representation of a hash (albeit not one 44 | that can be output on a terminal, or shouted down a hallway). 45 | * [The NATO Phonetic Alphabet][nato]: A great example of the trade-off between 46 | clarity of human communication and byte-wise efficiency of representation. 47 | 48 | [Chroma-Hash]: http://mattt.github.com/Chroma-Hash/ 49 | [nato]: http://en.wikipedia.org/wiki/NATO_phonetic_alphabet 50 | 51 | 52 | ## (Un)license 53 | 54 | This is free and unencumbered software released into the public domain. 55 | 56 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 57 | software, either in source code form or as a compiled binary, for any purpose, 58 | commercial or non-commercial, and by any means. 59 | 60 | In jurisdictions that recognize copyright laws, the author or authors of this 61 | software dedicate any and all copyright interest in the software to the public 62 | domain. We make this dedication for the benefit of the public at large and to 63 | the detriment of our heirs and successors. We intend this dedication to be an 64 | overt act of relinquishment in perpetuity of all present and future rights to 65 | this software under copyright law. 66 | 67 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 68 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 69 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE 70 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 71 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 72 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 73 | 74 | For more information, please refer to 75 | -------------------------------------------------------------------------------- /humanhash.py: -------------------------------------------------------------------------------- 1 | """ 2 | humanhash: Human-readable representations of digests. 3 | 4 | The simplest ways to use this module are the :func:`humanize` and :func:`uuid` 5 | functions. For tighter control over the output, see :class:`HumanHasher`. 6 | """ 7 | 8 | import operator 9 | import uuid as uuidlib 10 | 11 | 12 | DEFAULT_WORDLIST = ( 13 | 'ack', 'alabama', 'alanine', 'alaska', 'alpha', 'angel', 'apart', 'april', 14 | 'arizona', 'arkansas', 'artist', 'asparagus', 'aspen', 'august', 'autumn', 15 | 'avocado', 'bacon', 'bakerloo', 'batman', 'beer', 'berlin', 'beryllium', 16 | 'black', 'blossom', 'blue', 'bluebird', 'bravo', 'bulldog', 'burger', 17 | 'butter', 'california', 'carbon', 'cardinal', 'carolina', 'carpet', 'cat', 18 | 'ceiling', 'charlie', 'chicken', 'coffee', 'cola', 'cold', 'colorado', 19 | 'comet', 'connecticut', 'crazy', 'cup', 'dakota', 'december', 'delaware', 20 | 'delta', 'diet', 'don', 'double', 'early', 'earth', 'east', 'echo', 21 | 'edward', 'eight', 'eighteen', 'eleven', 'emma', 'enemy', 'equal', 22 | 'failed', 'fanta', 'fifteen', 'fillet', 'finch', 'fish', 'five', 'fix', 23 | 'floor', 'florida', 'football', 'four', 'fourteen', 'foxtrot', 'freddie', 24 | 'friend', 'fruit', 'gee', 'georgia', 'glucose', 'golf', 'green', 'grey', 25 | 'hamper', 'happy', 'harry', 'hawaii', 'helium', 'high', 'hot', 'hotel', 26 | 'hydrogen', 'idaho', 'illinois', 'india', 'indigo', 'ink', 'iowa', 27 | 'island', 'item', 'jersey', 'jig', 'johnny', 'juliet', 'july', 'jupiter', 28 | 'kansas', 'kentucky', 'kilo', 'king', 'kitten', 'lactose', 'lake', 'lamp', 29 | 'lemon', 'leopard', 'lima', 'lion', 'lithium', 'london', 'louisiana', 30 | 'low', 'magazine', 'magnesium', 'maine', 'mango', 'march', 'mars', 31 | 'maryland', 'massachusetts', 'may', 'mexico', 'michigan', 'mike', 32 | 'minnesota', 'mirror', 'mississippi', 'missouri', 'mobile', 'mockingbird', 33 | 'monkey', 'montana', 'moon', 'mountain', 'muppet', 'music', 'nebraska', 34 | 'neptune', 'network', 'nevada', 'nine', 'nineteen', 'nitrogen', 'north', 35 | 'november', 'nuts', 'october', 'ohio', 'oklahoma', 'one', 'orange', 36 | 'oranges', 'oregon', 'oscar', 'oven', 'oxygen', 'papa', 'paris', 'pasta', 37 | 'pennsylvania', 'pip', 'pizza', 'pluto', 'potato', 'princess', 'purple', 38 | 'quebec', 'queen', 'quiet', 'red', 'river', 'robert', 'robin', 'romeo', 39 | 'rugby', 'sad', 'salami', 'saturn', 'september', 'seven', 'seventeen', 40 | 'shade', 'sierra', 'single', 'sink', 'six', 'sixteen', 'skylark', 'snake', 41 | 'social', 'sodium', 'solar', 'south', 'spaghetti', 'speaker', 'spring', 42 | 'stairway', 'steak', 'stream', 'summer', 'sweet', 'table', 'tango', 'ten', 43 | 'tennessee', 'tennis', 'texas', 'thirteen', 'three', 'timing', 'triple', 44 | 'twelve', 'twenty', 'two', 'uncle', 'undress', 'uniform', 'uranus', 'utah', 45 | 'vegan', 'venus', 'vermont', 'victor', 'video', 'violet', 'virginia', 46 | 'washington', 'west', 'whiskey', 'white', 'william', 'winner', 'winter', 47 | 'wisconsin', 'wolfram', 'wyoming', 'xray', 'yankee', 'yellow', 'zebra', 48 | 'zulu') 49 | 50 | 51 | class HumanHasher(object): 52 | 53 | """ 54 | Transforms hex digests to human-readable strings. 55 | 56 | The format of these strings will look something like: 57 | `victor-bacon-zulu-lima`. The output is obtained by compressing the input 58 | digest to a fixed number of bytes, then mapping those bytes to one of 256 59 | words. A default wordlist is provided, but you can override this if you 60 | prefer. 61 | 62 | As long as you use the same wordlist, the output will be consistent (i.e. 63 | the same digest will always render the same representation). 64 | """ 65 | 66 | def __init__(self, wordlist=DEFAULT_WORDLIST): 67 | if len(wordlist) != 256: 68 | raise ArgumentError("Wordlist must have exactly 256 items") 69 | self.wordlist = wordlist 70 | 71 | def humanize(self, hexdigest, words=4, separator='-'): 72 | 73 | """ 74 | Humanize a given hexadecimal digest. 75 | 76 | Change the number of words output by specifying `words`. Change the 77 | word separator with `separator`. 78 | 79 | >>> digest = '60ad8d0d871b6095808297' 80 | >>> HumanHasher().humanize(digest) 81 | 'sodium-magnesium-nineteen-hydrogen' 82 | """ 83 | 84 | # Gets a list of byte values between 0-255. 85 | bytes = map(lambda x: int(x, 16), 86 | map(''.join, zip(hexdigest[::2], hexdigest[1::2]))) 87 | # Compress an arbitrary number of bytes to `words`. 88 | compressed = self.compress(bytes, words) 89 | # Map the compressed byte values through the word list. 90 | return separator.join(self.wordlist[byte] for byte in compressed) 91 | 92 | @staticmethod 93 | def compress(bytes, target): 94 | 95 | """ 96 | Compress a list of byte values to a fixed target length. 97 | 98 | >>> bytes = [96, 173, 141, 13, 135, 27, 96, 149, 128, 130, 151] 99 | >>> HumanHasher.compress(bytes, 4) 100 | [205, 128, 156, 96] 101 | 102 | Attempting to compress a smaller number of bytes to a larger number is 103 | an error: 104 | 105 | >>> HumanHasher.compress(bytes, 15) # doctest: +ELLIPSIS 106 | Traceback (most recent call last): 107 | ... 108 | ValueError: Fewer input bytes than requested output 109 | """ 110 | 111 | length = len(bytes) 112 | if target > length: 113 | raise ValueError("Fewer input bytes than requested output") 114 | 115 | # Split `bytes` into `target` segments. 116 | seg_size = length // target 117 | segments = [bytes[i * seg_size:(i + 1) * seg_size] 118 | for i in xrange(target)] 119 | # Catch any left-over bytes in the last segment. 120 | segments[-1].extend(bytes[target * seg_size:]) 121 | 122 | # Use a simple XOR checksum-like function for compression. 123 | checksum = lambda bytes: reduce(operator.xor, bytes, 0) 124 | checksums = map(checksum, segments) 125 | return checksums 126 | 127 | def uuid(self, **params): 128 | 129 | """ 130 | Generate a UUID with a human-readable representation. 131 | 132 | Returns `(human_repr, full_digest)`. Accepts the same keyword arguments 133 | as :meth:`humanize` (they'll be passed straight through). 134 | """ 135 | 136 | digest = str(uuidlib.uuid4()).replace('-', '') 137 | return self.humanize(digest, **params), digest 138 | 139 | 140 | DEFAULT_HASHER = HumanHasher() 141 | uuid = DEFAULT_HASHER.uuid 142 | humanize = DEFAULT_HASHER.humanize 143 | --------------------------------------------------------------------------------