Source code for zpywallet.utils.aes

# -*- coding: utf-8 -*-
#
# This is an implementation of the AES algorithm, specifically GCM-SIV mode,
# as defined in RFC 5297, with 256 bits key length and PKCS7 padding.
# Note that this is *not* AES-GCM-SIV!
#
#
# Copyright (c) Ali Sherief 2023-2024 <ali@notatether.com>
# Copyright Andrey Izman (c) 2018-2019 <izmanw@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


import hashlib
import base64
from Cryptodome import Random
from Cryptodome.Cipher import AES

BLOCK_SIZE = 16
KEY_LEN = 32
NONCE_LEN = 12


[docs]def hash_password_pbkdf2( password: bytes, salt: bytes, iterations=600000, key_length=KEY_LEN + NONCE_LEN ): # Hash the password using PBKDF2 with OWASP-recommended rounds return hashlib.pbkdf2_hmac("sha256", password + salt, salt, iterations, key_length)
[docs]def encrypt(raw: bytes, passphrase: bytes) -> bytes: """ Encrypt binary data with the passphrase Args: raw (bytes): data to encrypt passphrase (bytes): Encryption password. It is recommended to use a strong password. Returns: bytes: The encrypted text """ salt = Random.new().read(8) key, nonce = __derive_key_and_nonce(passphrase, salt) cipher = AES.new(key, AES.MODE_SIV, nonce) text, mac = cipher.encrypt_and_digest(__pkcs7_padding(raw)) return base64.b64encode(b"Salted__" + salt + mac + text)
[docs]def encrypt_str(raw: str, passphrase: str) -> str: """A wrapper around encyrpt() for str objects""" return encrypt(raw.encode("utf-8"), passphrase.encode("utf-8"))
[docs]def decrypt(enc: bytes, passphrase: bytes) -> bytes: """ Decrypt encrypted binary data with the passphrase Args: enc (bytes): data to decrypt passphrase (bytes): Decryption password Returns: bytes: The original text """ ct = base64.b64decode(enc) salted = ct[:8] if salted != b"Salted__": raise ValueError("Decryption failed") salt = ct[8:16] key, nonce = __derive_key_and_nonce(passphrase, salt) cipher = AES.new(key, AES.MODE_SIV, nonce) mac = ct[16:32] text = ct[32:] # Will throw ValueError if the authentication tag was tampered with d = __pkcs7_trimming(cipher.decrypt_and_verify(text, mac)) if len(d) == 0: raise ValueError("Decryption failed") return d
[docs]def decrypt_str(enc: bytes, passphrase: str) -> str: """A wrapper around decrypt() for str objects""" return decrypt(enc, passphrase.encode("utf-8")).decode("utf-8")
def __pkcs7_padding(s): # Padding to blocksize according to PKCS #7. # # Calculates the number of missing characters to BLOCK_SIZE and pads with # ord(number of missing characters). # # See: http://www.di-mgt.com.au/cryptopad.html s_len = len(s) s = s + (BLOCK_SIZE - s_len % BLOCK_SIZE) * bytes( chr(BLOCK_SIZE - s_len % BLOCK_SIZE), "utf-8" ) return s def __pkcs7_trimming(s): # Trims padding (unpads) according to PKCS #7. return s[0 : -s[-1]] def __derive_key_and_nonce(password, salt): d = d_i = b"" enc_pass = password while len(d) < KEY_LEN + NONCE_LEN: d_i = hash_password_pbkdf2(d_i + enc_pass, salt) d += d_i return d[:KEY_LEN], d[KEY_LEN : KEY_LEN + NONCE_LEN]