def __init__(self, gpg_key_dir: Path, journalist_key_fingerprint: str) -> None:
        self._gpg_key_dir = gpg_key_dir
        self._journalist_key_fingerprint = journalist_key_fingerprint
        self._redis = Redis(decode_responses=True)

        # Instantiate the "main" GPG binary
        gpg = gnupg.GPG(
            binary="gpg2", homedir=str(self._gpg_key_dir), options=["--trust-model direct"]
        )
        if StrictVersion(gpg.binary_version) >= StrictVersion("2.1"):
            # --pinentry-mode, required for SecureDrop on GPG 2.1.x+, was added in GPG 2.1.
            self._gpg = gnupg.GPG(
                binary="gpg2",
                homedir=str(gpg_key_dir),
                options=["--pinentry-mode loopback", "--trust-model direct"],
            )
        else:
            self._gpg = gpg

        # Instantiate the GPG binary to be used for key deletion: always delete keys without
        # invoking pinentry-mode=loopback
        # see: https://lists.gnupg.org/pipermail/gnupg-users/2016-May/055965.html
        self._gpg_for_key_deletion = gnupg.GPG(
            binary="gpg2", homedir=str(self._gpg_key_dir), options=["--yes", "--trust-model direct"]
        )

        # Ensure that the journalist public key has been previously imported in GPG
        try:
            self.get_journalist_public_key()
        except GpgKeyNotFoundError:
            raise EnvironmentError(
                f"The journalist public key with fingerprint {journalist_key_fingerprint}"
                f" has not been imported into GPG."
            )
Beispiel #2
0
def config(tmpdir):
    '''Clone the module so we can modify it per test.'''

    cnf = SDConfig()

    data = tmpdir.mkdir('data')
    keys = data.mkdir('keys')
    os.chmod(str(keys), 0o700)
    store = data.mkdir('store')
    tmp = data.mkdir('tmp')
    sqlite = data.join('db.sqlite')

    # gpg 2.1+ requires gpg-agent, see #4013
    gpg_agent_config = str(keys.join('gpg-agent.conf'))
    with open(gpg_agent_config, 'w+') as f:
        f.write('allow-loopback-pinentry')

    gpg = gnupg.GPG('gpg2', homedir=str(keys))
    for ext in ['sec', 'pub']:
        with io.open(
                path.join(path.dirname(__file__), 'files',
                          'test_journalist_key.{}'.format(ext))) as f:
            gpg.import_keys(f.read())

    cnf.SECUREDROP_DATA_ROOT = str(data)
    cnf.GPG_KEY_DIR = str(keys)
    cnf.STORE_DIR = str(store)
    cnf.TEMP_DIR = str(tmp)
    cnf.DATABASE_FILE = str(sqlite)

    # create the db file
    subprocess.check_call(['sqlite3', cnf.DATABASE_FILE, '.databases'])

    return cnf
Beispiel #3
0
def setup_journalist_key_and_gpg_folder(
) -> Generator[Tuple[str, Path], None, None]:
    """Set up the journalist test key and the key folder, and reduce source key length for speed.

    This fixture takes about 2s to complete hence we use the "session" scope to only run it once.
    """
    # This path matches the GPG_KEY_DIR defined in the config.py used for the tests
    # If they don't match, it can make the tests flaky and very hard to debug
    tmp_gpg_dir = Path("/tmp") / "securedrop" / "keys"
    tmp_gpg_dir.mkdir(parents=True, exist_ok=True, mode=0o0700)

    try:
        # GPG 2.1+ requires gpg-agent, see #4013
        gpg_agent_config = tmp_gpg_dir / "gpg-agent.conf"
        gpg_agent_config.write_text(
            "allow-loopback-pinentry\ndefault-cache-ttl 0")

        # Import the journalist public key in GPG
        # WARNING: don't import the journalist secret key; it will make the decryption tests
        # unreliable
        gpg = gnupg.GPG("gpg2", homedir=str(tmp_gpg_dir))
        journalist_public_key_path = Path(
            __file__).parent / "files" / "test_journalist_key.pub"
        journalist_public_key = journalist_public_key_path.read_text()
        journalist_key_fingerprint = gpg.import_keys(
            journalist_public_key).fingerprints[0]

        # Reduce source GPG key length to speed up tests at the expense of security
        with mock.patch.object(EncryptionManager, "GPG_KEY_LENGTH",
                               PropertyMock(return_value=1024)):

            yield journalist_key_fingerprint, tmp_gpg_dir

    finally:
        shutil.rmtree(tmp_gpg_dir, ignore_errors=True)
Beispiel #4
0
    def __init__(self,
                 scrypt_params: Dict[str, int],
                 scrypt_id_pepper: str,
                 scrypt_gpg_pepper: str,
                 securedrop_root: str,
                 word_list: str,
                 nouns_file: str,
                 adjectives_file: str,
                 gpg_key_dir: str) -> None:
        self.__securedrop_root = securedrop_root
        self.__word_list = word_list

        if os.environ.get('SECUREDROP_ENV') in ('dev', 'test'):
            # Optimize crypto to speed up tests (at the expense of security
            # DO NOT use these settings in production)
            self.__gpg_key_length = 1024
            self.scrypt_params = dict(N=2**1, r=1, p=1)
        else:  # pragma: no cover
            self.__gpg_key_length = 4096
            self.scrypt_params = scrypt_params

        self.scrypt_id_pepper = scrypt_id_pepper
        self.scrypt_gpg_pepper = scrypt_gpg_pepper

        self.do_runtime_tests()

        # --pinentry-mode, required for SecureDrop on GPG 2.1.x+, was
        # added in GPG 2.1.
        self.gpg_key_dir = gpg_key_dir
        gpg_binary = gnupg.GPG(binary='gpg2', homedir=self.gpg_key_dir)
        if StrictVersion(gpg_binary.binary_version) >= StrictVersion('2.1'):
            self.gpg = gnupg.GPG(binary='gpg2',
                                 homedir=gpg_key_dir,
                                 options=['--pinentry-mode loopback'])
        else:
            self.gpg = gpg_binary

        # map code for a given language to a localized wordlist
        self.__language2words = {}  # type: Dict[str, List[str]]

        with io.open(nouns_file) as f:
            self.nouns = f.read().splitlines()

        with io.open(adjectives_file) as f:
            self.adjectives = f.read().splitlines()

        self.redis = Redis(decode_responses=True)
    def delete_reply_keypair(self, source_filesystem_id):
        key = self.getkey(source_filesystem_id)
        # If this source was never flagged for review, they won't have a reply
        # keypair
        if not key:
            return

        # Always delete keys without invoking pinentry-mode = loopback
        # see: https://lists.gnupg.org/pipermail/gnupg-users/2016-May/055965.html
        temp_gpg = gnupg.GPG(binary='gpg2', homedir=self.gpg_key_dir)
        # The subkeys keyword argument deletes both secret and public keys.
        temp_gpg.delete_keys(key, secret=True, subkeys=True)
        self.keycache.delete(source_filesystem_id)
Beispiel #6
0
def init_gpg():
    """Initialize the GPG keyring and import the journalist key for
    testing.
    """

    # gpg 2.1+ requires gpg-agent, see #4013
    gpg_agent_config = os.path.join(config.GPG_KEY_DIR, 'gpg-agent.conf')
    with open(gpg_agent_config, 'w+') as f:
        f.write('allow-loopback-pinentry')

    gpg_binary = gnupg.GPG(binary='gpg2', homedir=config.GPG_KEY_DIR)
    if StrictVersion(gpg_binary.binary_version) >= StrictVersion('2.1'):
        gpg = gnupg.GPG(binary='gpg2',
                        homedir=config.GPG_KEY_DIR,
                        options=['--pinentry-mode loopback'])
    else:
        gpg = gpg_binary

    # Faster to import a pre-generated key than to gen a new one every time.
    for keyfile in (join(FILES_DIR, "test_journalist_key.pub"),
                    join(FILES_DIR, "test_journalist_key.sec")):
        gpg.import_keys(io.open(keyfile).read())
    return gpg
Beispiel #7
0
def gpg_key_dir() -> Generator[Path, None, None]:
    """Set up the journalist test key in GPG and the parent folder.

    This fixture takes about 2s to complete hence we use the "session" scope to only run it once.
    """
    with TemporaryDirectory() as tmp_gpg_dir_name:
        tmp_gpg_dir = Path(tmp_gpg_dir_name)

        # GPG 2.1+ requires gpg-agent, see #4013
        gpg_agent_config = tmp_gpg_dir / "gpg-agent.conf"
        gpg_agent_config.write_text("allow-loopback-pinentry")

        # Import the test key in GPG
        gpg = gnupg.GPG("gpg2", homedir=str(tmp_gpg_dir))
        test_keys_dir = Path(__file__).parent / "files"
        for ext in ["sec", "pub"]:
            key_file = test_keys_dir / "test_journalist_key.{}".format(ext)
            gpg.import_keys(key_file.read_text())

        yield tmp_gpg_dir
Beispiel #8
0
    def delete_reply_keypair(self, source_filesystem_id: str) -> None:
        fingerprint = self.get_fingerprint(source_filesystem_id)

        # If this source was never flagged for review, they won't have a reply
        # keypair
        if not fingerprint:
            return

        # verify that the key with the given fingerprint belongs to a source
        key = self.find_source_key(fingerprint)
        if not key:
            raise ValueError("source key not found")

        # Always delete keys without invoking pinentry-mode = loopback
        # see: https://lists.gnupg.org/pipermail/gnupg-users/2016-May/055965.html
        temp_gpg = gnupg.GPG(binary='gpg2', homedir=self.gpg_key_dir)

        # The subkeys keyword argument deletes both secret and public keys.
        temp_gpg.delete_keys(fingerprint, secret=True, subkeys=True)
        self.redis.hdel(self.REDIS_KEY_HASH, self.get_fingerprint(source_filesystem_id))
        self.redis.hdel(self.REDIS_FINGERPRINT_HASH, source_filesystem_id)