def decrypt_secret(b64str, pincode): # split the secret into components try: (scheme, salt, ciphertext) = b64str.split('$') salt = base64.b64decode(salt) ciphertext = base64.b64decode(ciphertext) except (ValueError, TypeError): raise totpcgi.UserSecretError('Failed to parse encrypted secret') key = pbkdf2_hmac('sha256', pincode, salt, KDF_ITER, KEY_SIZE * 2) aes_key = key[:KEY_SIZE] hmac_key = key[KEY_SIZE:] sig_size = hashlib.sha256().digest_size sig = ciphertext[-sig_size:] data = ciphertext[:-sig_size] # verify hmac sig first if hmac.new(hmac_key, data, hashlib.sha256).digest() != sig: raise totpcgi.UserSecretError('Failed to verify hmac!') iv_bytes = data[:AES_BLOCK_SIZE] data = data[AES_BLOCK_SIZE:] cypher = AES.new(aes_key, AES.MODE_CBC, iv_bytes) data = cypher.decrypt(data) padlen = bytearray((data[-1], ))[0] secret = data[:-padlen].decode('utf-8') logger.debug('Decryption successful') return secret
def get_user_secret(self, user, pincode=None): totp_file = os.path.join(self.secrets_dir, user) + '.totp' logger.debug('Examining user secret file: %s' % totp_file) if not os.access(totp_file, os.R_OK): raise totpcgi.UserNotFound('%s.totp does not exist or is not readable' % user) fh = open(totp_file, 'r') lockf(fh, LOCK_SH) # secret is always the first entry secret = fh.readline() secret = secret.strip() using_encrypted_secret = False if secret.find('aes256+hmac256') == 0: using_encrypted_secret = True if pincode is not None: secret = totpcgi.utils.decrypt_secret(secret, pincode) else: raise totpcgi.UserSecretError('Secret is encrypted, but no pincode provided') gaus = totpcgi.GAUserSecret(secret) while True: line = fh.readline() if line == '': break line = line.strip() if len(line) and line[0] == '"': if line[2:12] == 'RATE_LIMIT': (tries, seconds) = line[13:].split(' ') gaus.rate_limit = (int(tries), int(seconds)) logger.debug('rate_limit=%s' % str(gaus.rate_limit)) elif line[2:13] == 'WINDOW_SIZE': window_size = int(line[14:]) if 0 < window_size < 3: window_size = 3 gaus.window_size = window_size logger.debug('window_size=%s' % window_size) elif line[2:14] == 'HOTP_COUNTER': # This will most likely be overriden by user state, but load it up anyway, # as this will trigger HOTP mode. try: gaus.set_hotp(int(line[15:])) except ValueError: gaus.set_hotp(0) logger.debug('hotp_counter=%s' % gaus.counter) # Scratch code tokens are 8-digit # We ignore scratch tokens if we're using encrypted secret elif len(line) == 8 and not using_encrypted_secret: try: gaus.scratch_tokens.append(int(line)) logger.debug('Found a scratch-code token, adding it') except ValueError: logger.debug('Non-numeric scratch token found') # don't fail, just pretend we didn't see it continue lockf(fh, LOCK_UN) fh.close() # Make sure that we have a window_size defined # The topt configuration many not have had one, if not we need # to make sure we set it to the default of 3 if not hasattr(gaus, 'window_size'): gaus.window_size = 3 return gaus