Source code for narrenschiff.chest

# Copyright 2021 The Narrenschiff Authors

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at


# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import base64

import yaml
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

[docs]class AES256Cipher: """Encode and/or decode strings.""" def __init__(self, keychain): """ Construct :class:`narrenschiff.chest.AES256Cypher`. :param keychain: Object containing key and spice :type keychain: :class:`narrenschiff.chest.Keychain` :return: Void :rtype: ``None`` """ self.password = keychain.key.encode('utf-8') self.salt = keychain.spice.encode('utf-8') self.backend = default_backend() self.key = self.pbkdf2()
[docs] def encrypt(self, plaintext): """ Encrypt plaintext. :param plaintext: Plaintext :type plaintext: ``str`` :return: Ciphertext :rtype: ``str`` """ iv = os.urandom(16) cipher = Cipher( algorithms.AES(self.key), modes.CBC(iv), backend=self.backend ) encryptor = cipher.encryptor() padder = padding.PKCS7(256).padder() plaintext = plaintext.rstrip().encode('utf-8') padded = padder.update(plaintext) + padder.finalize() ciphertext = encryptor.update(padded) + encryptor.finalize() return base64.b64encode(iv + ciphertext)
[docs] def decrypt(self, ciphertext): """ Decrypt ciphertext. :param ciphertext: Ciphertext :type ciphertext: ``str`` :return: Plaintext :rtype: ``str`` """ ciphertext = base64.b64decode(ciphertext.rstrip()) iv, ciphertext = ciphertext[:16], ciphertext[16:] cipher = Cipher( algorithms.AES(self.key), modes.CBC(iv), backend=self.backend ) decryptor = cipher.decryptor() unpadder = padding.PKCS7(256).unpadder() plaintext = decryptor.update(ciphertext) + decryptor.finalize() unpadded = unpadder.update(plaintext) + unpadder.finalize() return unpadded.decode('utf-8')
[docs] def pbkdf2(self): """ Derive a 32 bytes (256 bits) key. :return: Password hash :rtype: ``byte string`` """ kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=self.salt, iterations=100000, backend=self.backend ) return kdf.derive(self.password)
[docs]class Chest: """Manipulate chest file.""" def __init__(self, keychain, path): """ Unlock treasure chest. :param keychain: Object containing key and spice :type keychain: :class:`narrenschiff.chest.Keychain` :param path: Path to the chest file :type path: ``str`` :return: Void :rtype: ``None`` """ self.keychain = keychain self.path = path self.chest = self.load_chest_file()
[docs] def load_chest_file(self): """ Load chest file with encrypted values. :return: Serialized YAML object :rtype: ``dict`` """ with open(self.path, 'r') as f: chest = yaml.load(f, Loader=yaml.FullLoader) return chest if chest else {}
[docs] def update(self, variable, value): """ Add or update chest file. :param variable: Variable name to update :type variable: ``str`` :param value: Value of the variable :type value: ``str`` :return: Void :rtype: ``None`` """ cipher = AES256Cipher(self.keychain) self.chest[variable] = cipher.encrypt(value).decode('utf-8') with open(self.path, 'w') as f: f.write(yaml.dump(self.chest))
[docs] def show(self, variable): """ Show decrypted value of the variable. :param variable: Variable name :type variable: ``str`` :param value: Value of the variable :type value: ``str`` :return: Decrypted value :rtype: ``str`` """ cipher = AES256Cipher(self.keychain) return cipher.decrypt(self.chest[variable])