def cancel(cancelKeyb58): # convert url end to bytes, extract uuid (bytes 0-16) and cancel key (rest) cancelKeyBytes = En(cancelKeyb58)._by58() uuid, cancelKey = authso.AuthSo.ioUUID( cancelKeyBytes[0:16]), cancelKeyBytes[16:] # ensure uuid is valid - avoid SQL injections if not re.fullmatch( r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', uuid): return {'success': False} # SQL obj to fetch user dict gsql = authso.GSQLBridge() query = gsql.SQL.fetch(f"SELECT nextkeys FROM users WHERE uuid = '{uuid}'") if True if not query else not query[0][0]: return {'success': False} # extract from 'nextkeys' the hashed cancelKey; seq of nextkeys is nix ts (4 bytes), hashed cancel key (32 bytes), # public key (32 bytes), encrypted private key (rest, usually 144 bytes) _, cancelKeyHashed, _ = query[0][0][0:4], query[0][0][4:36], query[0][0][ 36:] # ensure key provided once hashed matches the hash if not cancelKeyHashed == En(cancelKey)._sha256(): return {'success': False} # remove the request gsql.SQL.wesc(f"UPDATE users SET nextkeys* WHERE uuid = '{uuid}'", v=[(None, )]) return {'success': True}
def parseCookie(self): # b64 → bytes, huuid (16 bytes) + AES 'secret' + time (UTC/*nix) cookieBytes = En(self.cookieIn)._by64() huuid, phKey, ts = cookieBytes[0:16], cookieBytes[16:48], cookieBytes[48:] # from bytes huuid get uuid len 36 which IDs user and get SQL dict (line) for this user self.userDict = self.sql.getOneDataDict('users', 'uuid', self.ioUUID(huuid)) # in case login from cookie if not self.email: if False if not self.userDict else self.userDict.get('email'): self.email = self.userDict.get('email') else: return False # check that cookie not expired if int(time.time()) - gv.COOKIE.MAX > af.ifb(cookieBytes[48:]): return False if not self.userDict: return False # concatenate AES 'secret' with time and SHA256 hash to get AES key to decrypt 'saes' session vault and get # Ed25519 private key self.priv = CryW(cryptedObj = self.userDict['saes'], bytesPwrd = En(phKey + ts)._sha256()).decrypt() # keyring if self.priv: self.keyring = Crypt25519(self.priv, self.userDict['pub']) return bool(self.priv)
def report(token, secret, since): # avoid injection, ensure token matches secret (hashed secret = token) if not re.fullmatch(r'^[0-9]{1,10}', since): return JSONResponse({'success': False}) if not re.fullmatch(r'^[0-9a-zA-Z=/$]{1,50}', token): return JSONResponse({'success': False}) if not re.fullmatch(r'^[0-9a-zA-Z=/$]{1,50}', secret): return JSONResponse({'success': False}) if not En(secret).by58()._sha256() == En(token)._by58(): return JSONResponse({'success': False}) # extract data for this token; if since is 0 then all data otherwise all data since 'since' in days eg GET URL .../7 # then since is 7 so all things in past 7 days will be retrieved (7 days in dhacess is 7 * 86400 s * 1000000) sql = WSQL(*CONF.sqlCredentials) if int(since): sqlQuery = sql.getDataDicts( 'hits', where= f"token = '{token}' AND dhaccess > {(int(time.time() - (since * 86400))) * 1000000}" ) else: sqlQuery = sql.getDataDicts('hits', where=f"token = '{token}'") # modify result with adding 'dh' key as date/time YYYY-MM-DDTHH:MM as more easy to read than dhaccess timestamps # also remove token as repetitive modQuery = [{ **{k: v for k, v in hitDict.items() if not k == 'token'}, 'dt': af.mytime(hitDict['dhaccess'] / 1000000, 0) } for hitDict in sqlQuery] # the report is sent as attachment - file name base for JSON and zip fileNameBase = f"{token}_{af.mytime(time.time(), 1)}_{af.mytime(time.time(), 4)}" # dump the extracted/modified SQL dict result as JSON then zip it as a zip stream -> bytes ie zipped is a bytes # obj of a zipped file b'PK...' zipped = af.makeZipBytes( {f"{fileNameBase}.json": json.dumps(modQuery).encode('utf8')}) # create the mailjet obj; API key in 'conf.yaml' format 'username:password' (no Basic, really username:password) # as sms.Mailjet takes care of make the auth header mail = sms.Mailjet(CONF.mailjetAPIkey, CONF.sender) # lookup all emails related to this token go 1 at the time for email in sql.getOneDataDict('su', 'token', token)['emails']: # as specified by sms.Mailjet which is based on mailjet API mail.send(message={ 'Subject': f'Visit counter summary for {token}', 'TextPart': f'Your visit counter summary for {token} is attached as JSON in ZIP file.' }, emailTo=email, attachments={f'{fileNameBase}.zip': zipped}) WSQL.closeAll() return {'success': True}
def requestPwrdReset(self, uuid, newAccount=False): self.sql.reconnect() userDict = self.sql.getOneDataDict('users', 'uuid', uuid) password, priv, pub = self.newKeypair() cancel = En()._rnd(32) message = { 'Subject': 'Signout password change request', 'TextPart': f"Dear {tf.bj(userDict['dat'])['name']},\r\nA request to {'create an account' if newAccount else 'change password'} " f"was made to signout.\r\n\r\nIf you did not make this request, you can cancel it with this link: " f"https://so.alexhal.me/cancel/{En(AuthSo.ioUUID(uuid) + cancel)._b58()}." f"\r\n\r\nOtherwise, your temporary password is: {password}.\r\n\r\nThe Signout team." } email = sms.Mailjet(af.iob('mailjetapi.txt').decode('utf8'), '*****@*****.**') # email.send(message, userDict['email']) print(password) userDict['nextkeys'] = af.itb(int(time.time())) + En(cancel)._sha256() + pub + priv if newAccount: userDict['pub'] = pub self.sql.replaceRows('users', userDict) return password, priv, pub
def renewCookie(self): # ensure the renewed cookie will be valid ie current cookie can actually decrypt the session vault if not CryW(cryptedObj=self.userDict['saes'], bytesPwrd=En(En(self.cookieIn)._by64()[16:])._sha256()).decrypt(): return False # create a new session vault with known, same private Ed25519 key but change the 'secret' and time hence AES key too cookieBytes = self.ioUUID(self.userDict['uuid']) + En()._rnd(32) + af.itb(int(time.time())) self.cookieOut = En(cookieBytes)._b64() self.sql.replaceRows('users', { **self.userDict, 'saes': CryW(bytesMessage = self.priv, bytesPwrd = En(cookieBytes[16:])._sha256()).encrypt(), }) return True
def create(): # create token/secret secret = En()._rnd(32) tokenb58 = En(secret).sha256()._b58() secretb58 = En(secret)._b58() emails = [] # save sql = WSQL(*CONF.sqlCredentials) sql.replaceRows('su', {'token': tokenb58, 'emails': emails}) # show what to write print(f"Link will be 'https://count.alexhal.me/count/{tokenb58}'") print( f"Crontab will be '* * * * * curl 127.0.0.1:{CONF.port}/report/{tokenb58}/{secretb58}'" ) print( f"or '* * * * * curl 127.0.0.1:{CONF.port}/report/{tokenb58}/{secretb58}/somedurationdays >> /some/file/log.log' for logging" )
def getChallenge(self): if not self.userDict: return False # none if never logged in + case where first login ever *or* reset email password privateKeys = { 'current': self.userDict['priv'], 'next': self.parseNextKeys(self.userDict['nextkeys'])[3] } self.response['challenge'] = {k: En(CryW.getChallenge(v))._b64() for k, v in privateKeys.items() if v} return self
def changePassword(self, newHash, pub, priv): testBytes = En()._rnd(32) assert( testBytes == Crypt25519(privateBytes=priv).decrypt(Crypt25519(publicBytes=pub).encrypt(testBytes)) ) self.userDict.update({ 'nextkeys': None, 'priv': CryW(bytesMessage = newHash + priv, bytesPwrd = newHash).encrypt(), 'pub': pub, 'pwrdchange': 0 }) self.sql.replaceRows('users', self.userDict) self.sql.reconnect() self.response['type'] = 'passwordchanged'
def createList(self): self.luid = tf.getUUID() self.aes = En()._rnd(32) self.shareList(1, uuids=[self.auth.userDict['uuid']], first=True) self.updateList(newDat={ 'name': f"(nameless list)", 'cols': [] }, newActive=1) # add empty col self.updateCol() # check if one list displayed else display this one if not self.auth.sql.fetch( f"SELECT luid FROM lists WHERE luid IN (SELECT luid FROM rights WHERE uuid = '{self.auth.userDict['uuid']}' AND disp = 1) AND active = 1" ): self.auth.sql.wesc('UPDATE rights SET disp* WHERE uuid* AND luid*', v=[(True, self.auth.userDict['uuid'], self.luid) ])
af.itb(random.randint(15, 126)) for x in range(random.randint(100, 1000)) ]) for y in range(100) ] asymKC = Crypt25519() t = time.time() for plain in plains: asym = asymKC.encrypt(plain) dasym = Crypt25519(asymKC.privateBytes, asymKC.publicBytes).decrypt(asym) assert (dasym == plain) print(time.time() - t) aes = En().rnd(16)._sha256() t = time.time() for plain in plains: sym = CryW(bytesMessage=plain, bytesPwrd=aes, slow=1).encrypt() dsym = CryW(cryptedObj=sym, bytesPwrd=aes, slow=1).decrypt() assert (dsym == plain) print(time.time() - t) # speed test def makeNotes(n=1, z=1): generator = getattr(lipsum, { 0: 'generate_words',
def jb(obj, aes = None): compressed = zlib.compress(En(json.dumps(obj))._by()) return CryW(bytesMessage = compressed, bytesPwrd = aes).encrypt() if aes else compressed
def newKeypair(self): password = En()._rnd58(8) keys25519 = Crypt25519() return password, CryW(bytesMessage=En(password)._sha512u() + keys25519.privateBytes, bytesPwrd=En(password)._sha512u()).encrypt(), keys25519.publicBytes
def ioUUID(cls, val): if len(val) == 36: return En(val.replace('-', ''))._by16() if len(val) == 16: return f"{En(val)._b16().lower()[0:8]}-{En(val)._b16().lower()[8:12]}-{En(val)._b16().lower()[12:16]}-{En(val)._b16().lower()[16:20]}-{En(val)._b16().lower()[20:]}" return False
def getCookie(self, login): self.response['cookie'] = '' self.getChallenge() # get encrypted vault for this user and try to decrypt decrypted = { k: CryW( cryptedObj = self.userDict['priv'] if k == 'current' else self.parseNextKeys(self.userDict['nextkeys'])[3], pbkd = En(v)._by64() ).decrypt() for k, v in login.pbkdf2b64.items() } decrypted = {k: v for k, v in decrypted.items() if v} # analyze content of dict to figure out what type is to be sent to front end -> standard, reset, change, first # (cookie and forgot are other 'types' but not relevant here) if self.userDict['nextkeys']: type = 'reset' if self.userDict['priv'] else 'first' if not decrypted: self.response['type'] = type return self if not login.newhash and (not decrypted.get('current') or type == 'first'): self.response['type'] = type return self else: type = 'change' if self.userDict['pwrdchange'] else 'standard' if not decrypted: self.response['type'] = type return self if not login.newhash and type == 'change': self.response['type'] = 'change' return self self.response['type'] = 'standard' # we can cancel the request as user knows password if type == 'reset': if decrypted.get('current'): self.sql.wesc(f"UPDATE users SET nextkeys* WHERE uuid*", v = [(None, self.userDict['uuid'])]) self.userDict['nextkeys'] = None else: if not login.newhash: self.response['type'] = 'reset' return self # case where 'nextkeys' needs to be moved to usual permanents vaults ie new user or pwrd reset (not change, reset) if set(decrypted.keys()) == {'next'}: _, _, pub, _ = self.parseNextKeys(self.userDict['nextkeys']) self.hashedpwrd, self.priv = af.En(login.newhash)._by64(), decrypted.get('next')[64:] self.changePassword(self.hashedpwrd, pub, self.priv) # enable lists where assigned if type == 'first': self.sql.wesc(f"UPDATE rights SET disp* WHERE uuid*", v = [(1, self.userDict['uuid'])]) else: # otherwise if reset then all keys are lost :( self.sql.wesc(f"UPDATE rights SET aes* WHERE uuid*", v = [(None, self.userDict['uuid'])]) else: # decrypt the user's vault which contains (1) the hashed pwrd and (2) the private Ed25519 key self.hashedpwrd, self.priv = decrypted.get('current')[0:64], decrypted.get('current')[64:] # case where we are just Δing password if type == 'change': self.hashedpwrd = af.En(login.newhash)._by64() self.changePassword(self.hashedpwrd, self.userDict['pub'], self.priv) # cookie content in bytes: 16 bytes for uuid, 32 bytes of session AES 'secret' (not key) and time UTC, make b64 cookie cookieBytes = self.ioUUID(self.userDict['uuid']) + En()._rnd(32) + af.itb(int(time.time())) self.cookieOut = En(cookieBytes)._b64() # then rotate user's vault (avoid replay attack if XSS attack getting client-generated PBKDF2) and generate session # AES vault which is a SHA256 hashed combination of AES 'secret' above with time UTC so even a corrupt browser # can't fake cookie time generation to server as the hash which decrypts the session vault would change self.sql.replaceRows('users', { **self.userDict, 'priv': CryW(bytesMessage = self.hashedpwrd + self.priv, bytesPwrd = self.hashedpwrd).encrypt(), 'saes': CryW(bytesMessage = self.priv, bytesPwrd = En(cookieBytes[16:])._sha256()).encrypt(), }) self.keyring = Crypt25519(self.priv, self.userDict['pub']) self.response['cookie'] = self.cookieOut return self
self.response['challenge'] = {k: En(CryW.getChallenge(v))._b64() for k, v in privateKeys.items() if v} return self def parseNextKeys(self, nextKeys): if not nextKeys: return b'', b'', b'', b'' return af.ifb(nextKeys[0:4]), nextKeys[4:36], nextKeys[36:68], nextKeys[68:] password = '******' msg= b'\x16C\xd8\xa6\xc1\xdb~\xcdY\xc3\xd77\xeb\xfe\xa1\x0e\x9c\xe4\xdc:m5\xe7\xf5|\xc1\xb2U\x15\xbb\xe9\xd6\x86\xc1\xd8\xdcM\xb2\xeez\xd3\xe5C\xb9o\x1eN\x040\x19_\xe6\xc8\x9b\xdee\x95\xe2t\xe9\x82\xdd\xf5\t\x80U\xeb/\x08\xab~\xe5\x86"\xdf\xf2\xf2\x04!P\xae\xc5\xe9tv\x1c\xb9|\xbd\x99\xd2\xf1V\x14\x04\xac' cipher = CryW(bytesMessage = msg, bytesPwrd = En(password)._sha512u()).encrypt() CryW(cryptedObj = cipher, bytesPwrd = En(password)._sha512u()).decrypt()