Merge pull request #12983 from Chia-Network/checkpoint/long_lived_atari_from_main_0142129112e85360f5008b84a0cb84afd0215ba5

checkpoint: into long_lived/atari from main  @ 0142129112
This commit is contained in:
Kyle Altendorf
2022-08-15 13:05:17 -04:00
committed by GitHub
4 changed files with 98 additions and 96 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ import yaml
from chia.cmds.passphrase_funcs import prompt_for_passphrase, read_passphrase_from_file
from chia.util.default_root import DEFAULT_KEYS_ROOT_PATH
from chia.util.file_keyring import FileKeyring
from chia.util.file_keyring import FileKeyring, lockfile_path_for_file_path
from chia.util.keyring_wrapper import DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE, KeyringWrapper
from cryptography.exceptions import InvalidTag
from io import TextIOWrapper
@@ -52,7 +52,7 @@ def dump(keyring_file, full_payload: bool, passphrase_file: Optional[TextIOWrapp
passphrase = read_passphrase_from_file(passphrase_file)
keyring_path = Path(keyring_file)
keyring = FileKeyring(keyring_path, FileKeyring.lockfile_path_for_file_path(keyring_path))
keyring = FileKeyring(keyring_path, lockfile_path_for_file_path(keyring_path))
if full_payload:
keyring.load_outer_payload()
+85 -83
View File
@@ -29,6 +29,75 @@ CHECKBYTES_VALUE = b"5f365b8292ee505b" # Randomly generated
MAX_SUPPORTED_VERSION = 1 # Max supported file format version
def generate_nonce() -> bytes:
"""
Creates a nonce to be used by ChaCha20Poly1305. This should be called each time
the payload is encrypted.
"""
return token_bytes(NONCE_BYTES)
def generate_salt() -> bytes:
"""
Creates a salt to be used in combination with the master passphrase to derive
a symmetric key using PBKDF2
"""
return token_bytes(SALT_BYTES)
def have_valid_checkbytes(decrypted_data: bytes) -> bool:
return CHECKBYTES_VALUE == decrypted_data[: len(CHECKBYTES_VALUE)]
def symmetric_key_from_passphrase(passphrase: str, salt: bytes) -> bytes:
return pbkdf2_hmac("sha256", passphrase.encode(), salt, HASH_ITERS)
def get_symmetric_key(salt: bytes) -> bytes:
from chia.util.keychain import obtain_current_passphrase
try:
passphrase = obtain_current_passphrase(use_passphrase_cache=True)
except Exception as e:
print(f"Unable to unlock the keyring: {e}")
sys.exit(1)
return symmetric_key_from_passphrase(passphrase, salt)
def encrypt_data(input_data: bytes, key: bytes, nonce: bytes) -> bytes:
encryptor = ChaCha20Poly1305(key)
data = encryptor.encrypt(nonce, input_data, None)
return data
def decrypt_data(input_data: bytes, key: bytes, nonce: bytes) -> bytes:
decryptor = ChaCha20Poly1305(key)
output = decryptor.decrypt(nonce, input_data, None)
return output
def default_outer_payload() -> Dict[str, Any]:
return {"version": 1}
def keyring_path_from_root(keys_root_path: Path) -> Path:
"""
Returns the path to keyring.yaml
"""
path_filename = keys_root_path / "keyring.yaml"
return path_filename
def lockfile_path_for_file_path(file_path: Path) -> Path:
"""
Returns a path suitable for creating a lockfile derived from the input path.
Currently used to provide a lockfile path to be used by
fasteners.InterProcessReaderWriterLock when guarding access to keyring.yaml
"""
return file_path.with_name(f".{file_path.name}.lock")
class FileKeyringLockTimeout(Exception):
pass
@@ -135,35 +204,18 @@ class FileKeyring(FileSystemEventHandler): # type: ignore[misc] # Class cannot
# Key/value pairs to set on the outer payload on the next write
outer_payload_properties_for_next_write: Dict[str, Any] = field(default_factory=dict)
@staticmethod
def keyring_path_from_root(keys_root_path: Path) -> Path:
"""
Returns the path to keyring.yaml
"""
path_filename = keys_root_path / "keyring.yaml"
return path_filename
@staticmethod
def lockfile_path_for_file_path(file_path: Path) -> Path:
"""
Returns a path suitable for creating a lockfile derived from the input path.
Currently used to provide a lockfile path to be used by
fasteners.InterProcessReaderWriterLock when guarding access to keyring.yaml
"""
return file_path.with_name(f".{file_path.name}.lock")
@classmethod
def create(cls, keys_root_path: Path = DEFAULT_KEYS_ROOT_PATH) -> FileKeyring:
"""
Creates a fresh keyring.yaml file if necessary. Otherwise, loads and caches the
outer (plaintext) payload
"""
keyring_path = FileKeyring.keyring_path_from_root(keys_root_path)
obj = cls(keyring_path=keyring_path, keyring_lock_path=FileKeyring.lockfile_path_for_file_path(keyring_path))
keyring_path = keyring_path_from_root(keys_root_path)
obj = cls(keyring_path=keyring_path, keyring_lock_path=lockfile_path_for_file_path(keyring_path))
if not keyring_path.exists():
# Super simple payload if starting from scratch
outer_payload = FileKeyring.default_outer_payload()
outer_payload = default_outer_payload()
obj.write_data_to_keyring(outer_payload)
obj.outer_payload_cache = outer_payload
else:
@@ -202,26 +254,6 @@ class FileKeyring(FileSystemEventHandler): # type: ignore[misc] # Class cannot
# Shouldn't happen, but if the file doesn't exist there's nothing to do...
pass
@staticmethod
def default_outer_payload() -> Dict[str, Any]:
return {"version": 1}
@staticmethod
def generate_nonce() -> bytes:
"""
Creates a nonce to be used by ChaCha20Poly1305. This should be called each time
the payload is encrypted.
"""
return token_bytes(NONCE_BYTES)
@staticmethod
def generate_salt() -> bytes:
"""
Creates a salt to be used in combination with the master passphrase to derive
a symmetric key using PBKDF2
"""
return token_bytes(SALT_BYTES)
def has_content(self) -> bool:
"""
Quick test to determine if keyring is populated. The "data" value is expected
@@ -313,44 +345,14 @@ class FileKeyring(FileSystemEventHandler): # type: ignore[misc] # Class cannot
if not nonce:
return False
key = FileKeyring.symmetric_key_from_passphrase(passphrase, self.salt)
key = symmetric_key_from_passphrase(passphrase, self.salt)
encrypted_data = base64.b64decode(yaml.safe_load(self.outer_payload_cache.get("data") or ""))
try:
decrypted_data = self.decrypt_data(encrypted_data, key, nonce)
decrypted_data = decrypt_data(encrypted_data, key, nonce)
except Exception:
return False
return self.have_valid_checkbytes(decrypted_data)
def have_valid_checkbytes(self, decrypted_data: bytes) -> bool:
checkbytes = decrypted_data[: len(CHECKBYTES_VALUE)]
return checkbytes == CHECKBYTES_VALUE
@staticmethod
def symmetric_key_from_passphrase(passphrase: str, salt: bytes) -> bytes:
return pbkdf2_hmac("sha256", passphrase.encode(), salt, HASH_ITERS)
@staticmethod
def get_symmetric_key(salt: bytes) -> bytes:
from chia.util.keychain import obtain_current_passphrase
try:
passphrase = obtain_current_passphrase(use_passphrase_cache=True)
except Exception as e:
print(f"Unable to unlock the keyring: {e}")
sys.exit(1)
return FileKeyring.symmetric_key_from_passphrase(passphrase, salt)
def encrypt_data(self, input_data: bytes, key: bytes, nonce: bytes) -> bytes:
encryptor = ChaCha20Poly1305(key)
data = encryptor.encrypt(nonce, input_data, None)
return data
def decrypt_data(self, input_data: bytes, key: bytes, nonce: bytes) -> bytes:
decryptor = ChaCha20Poly1305(key)
output = decryptor.decrypt(nonce, input_data, None)
return output
return have_valid_checkbytes(decrypted_data)
def load_outer_payload(self) -> None:
if not self.keyring_path.is_file():
@@ -388,32 +390,32 @@ class FileKeyring(FileSystemEventHandler): # type: ignore[misc] # Class cannot
key = None
if passphrase:
key = FileKeyring.symmetric_key_from_passphrase(passphrase, salt)
key = symmetric_key_from_passphrase(passphrase, salt)
else:
key = FileKeyring.get_symmetric_key(salt)
key = get_symmetric_key(salt)
encrypted_payload = base64.b64decode(yaml.safe_load(self.outer_payload_cache.get("data") or ""))
decrypted_data = self.decrypt_data(encrypted_payload, key, nonce)
if not self.have_valid_checkbytes(decrypted_data):
decrypted_data = decrypt_data(encrypted_payload, key, nonce)
if not have_valid_checkbytes(decrypted_data):
raise ValueError("decryption failure (checkbytes)")
inner_payload = decrypted_data[len(CHECKBYTES_VALUE) :]
self.payload_cache = dict(yaml.safe_load(inner_payload))
def is_first_write(self) -> bool:
return self.outer_payload_cache == FileKeyring.default_outer_payload()
return self.outer_payload_cache == default_outer_payload()
def write_keyring(self, fresh_salt: bool = False) -> None:
from chia.util.keyring_wrapper import KeyringWrapper
inner_payload = self.payload_cache
inner_payload_yaml = yaml.safe_dump(inner_payload)
nonce = FileKeyring.generate_nonce()
nonce = generate_nonce()
key = None
# Update the salt when changing the master passphrase or when the keyring is new (empty)
if fresh_salt or not self.salt:
self.salt = FileKeyring.generate_salt()
self.salt = generate_salt()
salt = self.salt
@@ -421,14 +423,14 @@ class FileKeyring(FileSystemEventHandler): # type: ignore[misc] # Class cannot
# validated (because it can't be validated yet...)
# TODO Fix hinting in `KeyringWrapper` to get rid of the ignores below
if self.is_first_write() and KeyringWrapper.get_shared_instance().has_cached_master_passphrase(): # type: ignore[no-untyped-call] # noqa: E501
key = FileKeyring.symmetric_key_from_passphrase(
key = symmetric_key_from_passphrase(
KeyringWrapper.get_shared_instance().get_cached_master_passphrase()[0], self.salt # type: ignore[no-untyped-call] # noqa: E501
)
else:
# Prompt for the passphrase interactively and derive the key
key = FileKeyring.get_symmetric_key(salt)
key = get_symmetric_key(salt)
encrypted_inner_payload = self.encrypt_data(CHECKBYTES_VALUE + inner_payload_yaml.encode(), key, nonce)
encrypted_inner_payload = encrypt_data(CHECKBYTES_VALUE + inner_payload_yaml.encode(), key, nonce)
outer_payload = {
"version": 1,
@@ -3,7 +3,7 @@ import logging
import os
import pytest
from chia.util.file_keyring import acquire_writer_lock, FileKeyring, FileKeyringLockTimeout
from chia.util.file_keyring import acquire_writer_lock, lockfile_path_for_file_path, FileKeyringLockTimeout
from chia.util.keyring_wrapper import KeyringWrapper
from multiprocessing import Pool, TimeoutError
from pathlib import Path
@@ -215,7 +215,7 @@ class TestFileKeyringSynchronization:
If a writer lock is already held, another process should not be able to acquire
the same lock, failing after n attempts
"""
lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock_path = lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock = fasteners.InterProcessReaderWriterLock(str(lock_path))
# When: a writer lock is already acquired
@@ -262,7 +262,7 @@ class TestFileKeyringSynchronization:
If a write lock is already held, another process will be able to acquire the
same lock once the lock is released by the current holder
"""
lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock_path = lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock = fasteners.InterProcessReaderWriterLock(str(lock_path))
# When: a writer lock is already acquired
@@ -312,7 +312,7 @@ class TestFileKeyringSynchronization:
After the child process acquires the writer lock (and sleeps), the previous
holder should not be able to quickly reacquire the lock
"""
lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock_path = lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock = fasteners.InterProcessReaderWriterLock(str(lock_path))
# When: a writer lock is already acquired
@@ -361,7 +361,7 @@ class TestFileKeyringSynchronization:
After the child process releases the writer lock, we should be able to
acquire the lock
"""
lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock_path = lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock = fasteners.InterProcessReaderWriterLock(str(lock_path))
# When: a writer lock is already acquired
@@ -408,7 +408,7 @@ class TestFileKeyringSynchronization:
When a child process is holding the lock and aborts/crashes, we should be
able to acquire the lock
"""
lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock_path = lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock = fasteners.InterProcessReaderWriterLock(str(lock_path))
# When: a writer lock is already acquired
@@ -439,7 +439,7 @@ class TestFileKeyringSynchronization:
When a reader lock is already held, another thread/process should not be able
to acquire the lock for writing
"""
lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock_path = lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock = fasteners.InterProcessReaderWriterLock(str(lock_path))
# When: a reader lock is already held
@@ -485,7 +485,7 @@ class TestFileKeyringSynchronization:
When a reader lock is already held, another thread/process should not be able
to acquire the lock for writing until the reader releases its lock
"""
lock_path = FileKeyring.lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock_path = lockfile_path_for_file_path(KeyringWrapper.get_shared_instance().keyring.keyring_path)
lock = fasteners.InterProcessReaderWriterLock(str(lock_path))
# When: a reader lock is already acquired
+3 -3
View File
@@ -2,7 +2,7 @@ import os
import shutil
import tempfile
from chia.util.file_keyring import FileKeyring
from chia.util.file_keyring import FileKeyring, keyring_path_from_root
from chia.util.keychain import Keychain, default_keychain_service, default_keychain_user, get_private_key_user
from chia.util.keyring_wrapper import KeyringWrapper
from functools import wraps
@@ -36,11 +36,11 @@ def add_dummy_key_to_cryptfilekeyring(crypt_file_keyring: CryptFileKeyring):
def setup_mock_file_keyring(mock_configure_backend, temp_file_keyring_dir, populate=False):
if populate:
# Populate the file keyring with an empty (but encrypted) data set
file_keyring_path = FileKeyring.keyring_path_from_root(Path(temp_file_keyring_dir))
file_keyring_path = keyring_path_from_root(Path(temp_file_keyring_dir))
os.makedirs(os.path.dirname(file_keyring_path), 0o700, True)
with open(
os.open(
FileKeyring.keyring_path_from_root(Path(temp_file_keyring_dir)),
keyring_path_from_root(Path(temp_file_keyring_dir)),
os.O_CREAT | os.O_WRONLY | os.O_TRUNC,
0o600,
),