Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions src/bitpay/bitpay_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import requests

from utils.key_utils import *
from utils.secret_file import write_secret_file
from exceptions.bitpay_exception import BitPayException

# Will be set to Test otherwise
Expand Down Expand Up @@ -77,8 +78,7 @@ def store_key(private_key: str) -> None:
)

if input_value.lower() == "f":
with open(str(private_key_path), "wb") as f:
f.write(private_key.encode())
write_secret_file(str(private_key_path), private_key)
plain_private_key = None
print("Private key saved at path:", private_key_path)
select_tokens(private_key)
Expand Down Expand Up @@ -171,12 +171,9 @@ def update_config_file() -> None:
}
}

with open(os.path.abspath("bitpay.config.json"), "w") as outfile:
json.dump(config, outfile, indent=2)
print(
"Generated configuration file at path: ",
os.path.abspath("bitpay.config.json"),
)
config_path = os.path.abspath("bitpay.config.json")
write_secret_file(config_path, json.dumps(config, indent=2))
print("Generated configuration file at path: ", config_path)

print("Configuration generated successfully! \n")
print(
Expand Down
24 changes: 24 additions & 0 deletions src/bitpay/utils/secret_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import os

SECRET_FILE_MODE = 0o600


def write_secret_file(path: str, content: str) -> None:
"""
Writes a file that holds secrets (private key, API tokens) so only the owner
can read and write it.

The mode given to os.open only applies when the file is created. If the file
already exists, for example from an older setup run, its permissions are
tightened before and after writing. On Windows, chmod only controls the
read-only flag, so this has no effect there.
"""
if os.path.exists(path):
os.chmod(path, SECRET_FILE_MODE)

flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_BINARY", 0)
fd = os.open(path, flags, SECRET_FILE_MODE)
with os.fdopen(fd, "wb") as file:
file.write(content.encode("utf-8"))

os.chmod(path, SECRET_FILE_MODE)
Empty file added tests/unit/utils/__init__.py
Empty file.
48 changes: 48 additions & 0 deletions tests/unit/utils/test_secret_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
import sys

import pytest

from bitpay.utils.secret_file import write_secret_file

# POSIX permissions do not apply on Windows.
posix_only = pytest.mark.skipif(
sys.platform == "win32", reason="POSIX permissions do not apply on Windows"
)


@pytest.mark.unit
@posix_only
def test_creates_file_readable_only_by_owner(tmp_path): # type: ignore
file = tmp_path / "private_key.pem"

write_secret_file(str(file), "abc")

assert os.stat(file).st_mode & 0o777 == 0o600
assert file.read_text() == "abc"


@pytest.mark.unit
@posix_only
def test_tightens_existing_file_with_wider_permissions(tmp_path): # type: ignore
file = tmp_path / "bitpay.config.json"
file.write_text("old content that is longer")
os.chmod(file, 0o644)

write_secret_file(str(file), "new")

assert os.stat(file).st_mode & 0o777 == 0o600
assert file.read_text() == "new"


@pytest.mark.unit
@posix_only
def test_ignores_permissive_umask(tmp_path): # type: ignore
file = tmp_path / "private_key.pem"
old_umask = os.umask(0)
try:
write_secret_file(str(file), "abc")
finally:
os.umask(old_umask)

assert os.stat(file).st_mode & 0o777 == 0o600
Loading