From 8082570b7fc78470d601b0d57171a7b8b723805e Mon Sep 17 00:00:00 2001 From: Kevin Froman Date: Wed, 19 Jun 2019 01:57:13 -0500 Subject: [PATCH] * bumped nacl and unpaddedbase32 verison * added/improved support for unpaddedbase32 keys * greatly improved home UI and mail * deniable blocks shouldnt use forward secrecy anymore * dont add yourself as a contact --- onionr/communicatorutils/deniableinserts.py | 2 +- onionr/core.py | 9 +++- onionr/httpapi/friendsapi/__init__.py | 3 +- onionr/onionrcommands/pubkeymanager.py | 2 + onionr/onionrcrypto.py | 6 ++- onionr/onionrusers/contactmanager.py | 2 + onionr/onionrusers/onionrusers.py | 5 +- onionr/onionrutils.py | 6 ++- onionr/static-data/www/mail/mail.css | 4 ++ onionr/static-data/www/mail/mail.js | 6 +-- onionr/static-data/www/mail/sendmail.js | 7 ++- onionr/static-data/www/private/index.html | 10 ++-- onionr/static-data/www/private/main.css | 10 ++++ onionr/static-data/www/shared/main/stats.js | 25 +++++++++ onionr/static-data/www/shared/main/style.css | 8 +++ onionr/tests/test_stringvalidations.py | 8 +-- requirements.in | 3 +- requirements.txt | 56 ++++++++------------ 18 files changed, 117 insertions(+), 55 deletions(-) diff --git a/onionr/communicatorutils/deniableinserts.py b/onionr/communicatorutils/deniableinserts.py index f993e6d0..571a6093 100755 --- a/onionr/communicatorutils/deniableinserts.py +++ b/onionr/communicatorutils/deniableinserts.py @@ -27,5 +27,5 @@ def insert_deniable_block(comm_inst): # This assumes on the libsodium primitives to have key-privacy fakePeer = onionrvalues.DENIABLE_PEER_ADDRESS data = secrets.token_hex(secrets.randbelow(1024) + 1) - comm_inst._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, meta={'subject': 'foo'}) + comm_inst._core.insertBlock(data, header='pm', encryptType='asym', asymPeer=fakePeer, disableForward=True, meta={'subject': 'foo'}) comm_inst.decrementThreadCount('insert_deniable_block') \ No newline at end of file diff --git a/onionr/core.py b/onionr/core.py index 33b310af..be309309 100755 --- a/onionr/core.py +++ b/onionr/core.py @@ -128,7 +128,8 @@ class Core: ''' Adds a public key to the key database (misleading function name) ''' - assert peerID not in self.listPeers() + if peerID in self.listPeers() or peerID == self._crypto.pubKey: + raise ValueError("specified id is already known") # This function simply adds a peer to the DB if not self._utils.validatePubKey(peerID): @@ -776,7 +777,11 @@ class Core: data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True).decode() signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True).decode() signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True).decode() - onionrusers.OnionrUser(self, asymPeer, saveUser=True) + try: + onionrusers.OnionrUser(self, asymPeer, saveUser=True) + except ValueError: + # if peer is already known + pass else: raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key') diff --git a/onionr/httpapi/friendsapi/__init__.py b/onionr/httpapi/friendsapi/__init__.py index 8d6b4122..5b4a883b 100755 --- a/onionr/httpapi/friendsapi/__init__.py +++ b/onionr/httpapi/friendsapi/__init__.py @@ -26,7 +26,8 @@ friends = Blueprint('friends', __name__) @friends.route('/friends/list') def list_friends(): pubkey_list = {} - friend_list = contactmanager.ContactManager.list_friends(core.Core()) + c = core.Core() + friend_list = contactmanager.ContactManager.list_friends(c) for friend in friend_list: pubkey_list[friend.publicKey] = {'name': friend.get_info('name')} return json.dumps(pubkey_list) diff --git a/onionr/onionrcommands/pubkeymanager.py b/onionr/onionrcommands/pubkeymanager.py index e12fc670..2a6370f9 100755 --- a/onionr/onionrcommands/pubkeymanager.py +++ b/onionr/onionrcommands/pubkeymanager.py @@ -21,6 +21,7 @@ import sys, getpass import logger, onionrexceptions from onionrusers import onionrusers, contactmanager +import unpaddedbase32 def add_ID(o_inst): try: sys.argv[2] @@ -50,6 +51,7 @@ def add_ID(o_inst): def change_ID(o_inst): try: key = sys.argv[2] + key = unpaddedbase32.repad(key.encode()).decode() except IndexError: logger.warn('Specify pubkey to use') else: diff --git a/onionr/onionrcrypto.py b/onionr/onionrcrypto.py index 75fe2138..3c77b3ce 100755 --- a/onionr/onionrcrypto.py +++ b/onionr/onionrcrypto.py @@ -19,6 +19,7 @@ ''' import os, binascii, base64, hashlib, time, sys, hmac, secrets import nacl.signing, nacl.encoding, nacl.public, nacl.hash, nacl.pwhash, nacl.utils, nacl.secret +import unpaddedbase32 import logger, onionrproofs import onionrexceptions, keymanager, core import config @@ -93,6 +94,7 @@ class OnionrCrypto: def pubKeyEncrypt(self, data, pubkey, encodedData=False): '''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)''' + pubkey = unpaddedbase32.repad(self._core._utils.strToBytes(pubkey)) retVal = '' box = None data = self._core._utils.strToBytes(data) @@ -129,7 +131,7 @@ class OnionrCrypto: return decrypted def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True): - '''Encrypt data to a 32-byte key (Salsa20-Poly1305 MAC)''' + '''Encrypt data with a 32-byte key (Salsa20-Poly1305 MAC)''' if encodedKey: encoding = nacl.encoding.Base64Encoder else: @@ -199,7 +201,7 @@ class OnionrCrypto: if pubkey == '': pubkey = self.pubKey prev = '' - pubkey = pubkey.encode() + pubkey = self._core._utils.strToBytes(pubkey) for i in range(self.HASH_ID_ROUNDS): try: prev = prev.encode() diff --git a/onionr/onionrusers/contactmanager.py b/onionr/onionrusers/contactmanager.py index 0524ed64..098cba65 100755 --- a/onionr/onionrusers/contactmanager.py +++ b/onionr/onionrusers/contactmanager.py @@ -18,10 +18,12 @@ along with this program. If not, see . ''' import os, json, onionrexceptions +import unpaddedbase32 from onionrusers import onionrusers class ContactManager(onionrusers.OnionrUser): def __init__(self, coreInst, publicKey, saveUser=False, recordExpireSeconds=5): + publicKey = unpaddedbase32.repad(coreInst._utils.strToBytes(publicKey)).decode() super(ContactManager, self).__init__(coreInst, publicKey, saveUser=saveUser) self.dataDir = coreInst.dataDir + '/contacts/' self.dataFile = '%s/contacts/%s.json' % (coreInst.dataDir, publicKey) diff --git a/onionr/onionrusers/onionrusers.py b/onionr/onionrusers/onionrusers.py index 9e26d047..51977b39 100755 --- a/onionr/onionrusers/onionrusers.py +++ b/onionr/onionrusers/onionrusers.py @@ -18,6 +18,7 @@ along with this program. If not, see . ''' import onionrblockapi, logger, onionrexceptions, json, sqlite3, time +import unpaddedbase32 import nacl.exceptions def deleteExpiredKeys(coreInst): @@ -55,8 +56,7 @@ class OnionrUser: Takes an instance of onionr core, a base32 encoded ed25519 public key, and a bool saveUser saveUser determines if we should add a user to our peer database or not. ''' - if ' ' in coreInst._utils.bytesToStr(publicKey).strip(): - publicKey = coreInst._utils.convertHumanReadableID(publicKey) + publicKey = unpaddedbase32.repad(coreInst._utils.strToBytes(publicKey)).decode() self.trust = 0 self._core = coreInst @@ -190,6 +190,7 @@ class OnionrUser: return list(keyList) def addForwardKey(self, newKey, expire=DEFAULT_KEY_EXPIRE): + newKey = self._core._utils.bytesToStr(unpaddedbase32.repad(self._core._utils.strToBytes(newKey))) if not self._core._utils.validatePubKey(newKey): # Do not add if something went wrong with the key raise onionrexceptions.InvalidPubkey(newKey) diff --git a/onionr/onionrutils.py b/onionr/onionrutils.py index e2a5964d..887c421a 100755 --- a/onionr/onionrutils.py +++ b/onionr/onionrutils.py @@ -21,6 +21,7 @@ import sys, os, sqlite3, binascii, time, base64, json, glob, shutil, math, re, urllib.parse, string import requests import nacl.signing, nacl.encoding +import unpaddedbase32 from onionrblockapi import Block import onionrexceptions, config, logger from onionr import API_VERSION @@ -319,9 +320,12 @@ class OnionrUtils: ''' Validate if a string is a valid base32 encoded Ed25519 key ''' - retVal = False if type(key) is type(None): return False + # Accept keys that have no = padding + key = unpaddedbase32.repad(self.strToBytes(key)) + + retVal = False try: nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder) except nacl.exceptions.ValueError: diff --git a/onionr/static-data/www/mail/mail.css b/onionr/static-data/www/mail/mail.css index 32999440..f2c31086 100755 --- a/onionr/static-data/www/mail/mail.css +++ b/onionr/static-data/www/mail/mail.css @@ -113,4 +113,8 @@ input{ color: black; font-size: 1.5em; width: 10%; +} + +.content{ + min-height: 1000px; } \ No newline at end of file diff --git a/onionr/static-data/www/mail/mail.js b/onionr/static-data/www/mail/mail.js index cc4895a1..27180173 100755 --- a/onionr/static-data/www/mail/mail.js +++ b/onionr/static-data/www/mail/mail.js @@ -58,9 +58,9 @@ function openReply(bHash, quote, subject){ // Add quoted reply var splitQuotes = quote.split('\n') for (var x = 0; x < splitQuotes.length; x++){ - splitQuotes[x] = '>' + splitQuotes[x] + splitQuotes[x] = '> ' + splitQuotes[x] } - quote = '\n' + splitQuotes.join('\n') + quote = '\n' + key.substring(0, 12) + ' wrote:' + '\n' + splitQuotes.join('\n') document.getElementById('draftText').value = quote setActiveTab('send message') } @@ -77,7 +77,7 @@ function openThread(bHash, sender, date, sigBool, pubkey, subjectLine){ var sigMsg = 'signature' // show add unknown contact button if peer is unknown but still has pubkey - if (sender == pubkey){ + if (sender === pubkey && sender !== myPub){ addUnknownContact.style.display = 'inline' } diff --git a/onionr/static-data/www/mail/sendmail.js b/onionr/static-data/www/mail/sendmail.js index f8ff49bc..e2d34c1f 100755 --- a/onionr/static-data/www/mail/sendmail.js +++ b/onionr/static-data/www/mail/sendmail.js @@ -54,6 +54,11 @@ sendForm.onsubmit = function(){ return false } } - sendMail(to.value, messageContent.value, subject.value) + if (to.value.length !== 56 && to.value.length !== 52){ + alert('Public key is not valid') + } + else{ + sendMail(to.value, messageContent.value, subject.value) + } return false } diff --git a/onionr/static-data/www/private/index.html b/onionr/static-data/www/private/index.html index 151c3704..a2d3d3c9 100755 --- a/onionr/static-data/www/private/index.html +++ b/onionr/static-data/www/private/index.html @@ -21,26 +21,26 @@

🕵️‍♂️ Current Used Identity:

- +

-

Onionr Services


-

Mail - Friend Manager - Boards - +

Mail - Friend Manager - Circle - Clandestine


Edit Configuration
-

Warning: Some values can be dangerous to change. Use caution.

+

Warning: Some values can be dangerous to change.

Configuration contains sensitive information.



-

Statistics

+

🔒 Security Level:

🕰️ Uptime:

Connections

+

🖇️ Last Received Request: None since start

⬇️ Total Requests Received: None since start

🔗 Outgoing Connections:

diff --git a/onionr/static-data/www/private/main.css b/onionr/static-data/www/private/main.css index 91a7b588..896cd2f5 100755 --- a/onionr/static-data/www/private/main.css +++ b/onionr/static-data/www/private/main.css @@ -5,4 +5,14 @@ .saveConfig{ margin-top: 1em; +} + + +.idLink{ + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome and Opera */ } \ No newline at end of file diff --git a/onionr/static-data/www/shared/main/stats.js b/onionr/static-data/www/shared/main/stats.js index 86b7f451..9b50185e 100755 --- a/onionr/static-data/www/shared/main/stats.js +++ b/onionr/static-data/www/shared/main/stats.js @@ -18,10 +18,34 @@ */ uptimeDisplay = document.getElementById('uptime') connectedDisplay = document.getElementById('connectedNodes') +connectedDisplay.style.maxHeight = '300px' +connectedDisplay.style.overflowY = 'scroll' storedBlockDisplay = document.getElementById('storedBlocks') queuedBlockDisplay = document.getElementById('blockQueue') lastIncoming = document.getElementById('lastIncoming') totalRec = document.getElementById('totalRec') +securityLevel = document.getElementById('securityLevel') +sec_description_str = 'unknown' + +function showSecStatNotice(){ + var secWarnEls = document.getElementsByClassName('secRequestNotice') + for (el = 0; el < secWarnEls.length; el++){ + secWarnEls[el].style.display = 'block' + } +} + +switch (httpGet('/config/get/general.security_level')){ + case "0": + sec_description_str = 'normal' + break; + case "1": + sec_description_str = 'high' + break; +} + +if (sec_description_str !== 'normal'){ + showSecStatNotice() +} function getStats(){ stats = JSON.parse(httpGet('getstats', webpass)) @@ -29,6 +53,7 @@ function getStats(){ connectedDisplay.innerText = stats['connectedNodes'] storedBlockDisplay.innerText = stats['blockCount'] queuedBlockDisplay.innerText = stats['blockQueueCount'] + securityLevel.innerText = sec_description_str totalRec.innerText = httpGet('/hitcount') var lastConnect = httpGet('/lastconnect') if (lastConnect > 0){ diff --git a/onionr/static-data/www/shared/main/style.css b/onionr/static-data/www/shared/main/style.css index 18388845..8181df8d 100755 --- a/onionr/static-data/www/shared/main/style.css +++ b/onionr/static-data/www/shared/main/style.css @@ -178,8 +178,16 @@ body{ background-color:#396BAC; } +.btn:hover{ + opacity: 0.6; +} + .openSiteBtn{ padding: 5px; border: 1px solid black; border-radius: 5px; } + +.hidden{ + display: none; +} diff --git a/onionr/tests/test_stringvalidations.py b/onionr/tests/test_stringvalidations.py index 0cc45eae..caac34df 100755 --- a/onionr/tests/test_stringvalidations.py +++ b/onionr/tests/test_stringvalidations.py @@ -29,11 +29,13 @@ class OnionrValidations(unittest.TestCase): def test_pubkey_validator(self): # Test ed25519 public key validity - valid = 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ====' + valids = ['JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ====', 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIRRQ'] invalid = [None, '', ' ', 'dfsg', '\n', 'JZ5VE72GUS3C7BOHDRIYZX4B5U5EJMCMLKHLYCVBQQF3UKHYIR$Q===='] c = core.Core() - print('testing', valid) - self.assertTrue(c._utils.validatePubKey(valid)) + + for valid in valids: + print('testing', valid) + self.assertTrue(c._utils.validatePubKey(valid)) for x in invalid: #print('testing', x) diff --git a/requirements.in b/requirements.in index bbb9cdc3..c285834a 100644 --- a/requirements.in +++ b/requirements.in @@ -1,9 +1,10 @@ urllib3==1.24.2 requests==2.21.0 -PyNaCl==1.2.1 +PyNaCl==1.3.0 gevent==1.3.6 Flask==1.0.2 PySocks==1.6.8 stem==1.7.1 deadsimplekv==0.1.1 +unpaddedbase32==0.1.0 jinja2==2.10.1 diff --git a/requirements.txt b/requirements.txt index 06b3303d..261a9996 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --generate-hashes --output-file requirements.txt requirements.in +# pip-compile --generate-hashes --output-file=requirements.txt requirements.in # certifi==2018.11.29 \ --hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \ @@ -140,38 +140,26 @@ markupsafe==1.1.1 \ pycparser==2.19 \ --hash=sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3 \ # via cffi -pynacl==1.2.1 \ - --hash=sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca \ - --hash=sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512 \ - --hash=sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6 \ - --hash=sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776 \ - --hash=sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac \ - --hash=sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b \ - --hash=sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb \ - --hash=sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98 \ - --hash=sha256:63cfccdc6217edcaa48369191ae4dca0c390af3c74f23c619e954973035948cd \ - --hash=sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2 \ - --hash=sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef \ - --hash=sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f \ - --hash=sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988 \ - --hash=sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b \ - --hash=sha256:9c8a06556918ee8e3ab48c65574f318f5a0a4d31437fc135da7ee9d4f9080415 \ - --hash=sha256:a1e25fc5650cf64f01c9e435033e53a4aca9de30eb9929d099f3bb078e18f8f2 \ - --hash=sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101 \ - --hash=sha256:c5b1a7a680218dee9da0f1b5e24072c46b3c275d35712bc1d505b85bb03441c0 \ - --hash=sha256:cb785db1a9468841a1265c9215c60fe5d7af2fb1b209e3316a152704607fc582 \ - --hash=sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a \ - --hash=sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975 \ - --hash=sha256:d3a934e2b9f20abac009d5b6951067cfb5486889cb913192b4d8288b216842f1 \ - --hash=sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb \ - --hash=sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45 \ - --hash=sha256:de2aaca8386cf4d70f1796352f2346f48ddb0bed61dc43a3ce773ba12e064031 \ - --hash=sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9 \ - --hash=sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752 \ - --hash=sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0 \ - --hash=sha256:f5836463a3c0cca300295b229b6c7003c415a9d11f8f9288ddbd728e2746524c \ - --hash=sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053 \ - --hash=sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4 +pynacl==1.3.0 \ + --hash=sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255 \ + --hash=sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c \ + --hash=sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e \ + --hash=sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae \ + --hash=sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621 \ + --hash=sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56 \ + --hash=sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39 \ + --hash=sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310 \ + --hash=sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1 \ + --hash=sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a \ + --hash=sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786 \ + --hash=sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b \ + --hash=sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b \ + --hash=sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f \ + --hash=sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20 \ + --hash=sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415 \ + --hash=sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715 \ + --hash=sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1 \ + --hash=sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0 pysocks==1.6.8 \ --hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672 requests==2.21.0 \ @@ -183,6 +171,8 @@ six==1.12.0 \ # via pynacl stem==1.7.1 \ --hash=sha256:c9eaf3116cb60c15995cbd3dec3a5cbc50e9bb6e062c4d6d42201e566f498ca2 +unpaddedbase32==0.1.0 \ + --hash=sha256:5e4143fcaf77c9c6b4f60d18301c7570f0dac561dcf9b9aed8b5ba6ead7f218c urllib3==1.24.2 \ --hash=sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0 \ --hash=sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3