├── .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 |
--------------------------------------------------------------------------------