def test_authenticate_source_user_wrong_passphrase(self, source_app, app_storage): # Given a source in the DB create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default().generate_passphrase(), source_app_storage=app_storage, ) # When a user tries to authenticate using a wrong passphrase, it fails wrong_passphrase = "rehydrate flaring study raven fence extenuate linguist" with pytest.raises(InvalidPassphraseError): authenticate_source_user(db_session=db.session, supplied_passphrase=wrong_passphrase)
def test_create_source_user_passphrase_collision(self, source_app, app_storage): # Given a source in the DB passphrase = PassphraseGenerator.get_default().generate_passphrase() create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=app_storage, ) # When trying to create another with the same passphrase, it fails with pytest.raises(SourcePassphraseCollisionError): create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=app_storage, )
def create() -> werkzeug.Response: if SessionManager.is_user_logged_in(db_session=db.session): flash_msg( "notification", None, gettext( "You are already logged in. Please verify your codename as it " "may differ from the one displayed on the previous page." ), ) else: # Ensure the codenames have not expired date_codenames_expire = session.get("codenames_expire") if not date_codenames_expire or datetime.now(timezone.utc) >= date_codenames_expire: return clear_session_and_redirect_to_logged_out_page(flask_session=session) tab_id = request.form["tab_id"] codename = session["codenames"][tab_id] del session["codenames"] try: current_app.logger.info("Creating new source user...") create_source_user( db_session=db.session, source_passphrase=codename, source_app_storage=Storage.get_default(), ) except (SourcePassphraseCollisionError, SourceDesignationCollisionError) as e: current_app.logger.error("Could not create a source: {}".format(e)) flash_msg( "error", None, gettext( "There was a temporary problem creating your account. Please try again." ), ) return redirect(url_for(".index")) # All done - source user was successfully created current_app.logger.info("New source user created") session["new_user_codename"] = codename SessionManager.log_user_in( db_session=db.session, supplied_passphrase=DicewarePassphrase(codename) ) return redirect(url_for(".lookup"))
def test_encrypt_and_decrypt_journalist_reply(self, source_app, test_source, tmp_path, app_storage): # Given a source user with a key pair in the default encryption manager source_user1 = test_source["source_user"] encryption_mgr = EncryptionManager.get_default() # And another source with a key pair in the default encryption manager with source_app.app_context(): source_user2 = create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default(). generate_passphrase(), source_app_storage=app_storage, ) encryption_mgr.generate_source_key_pair(source_user2) # When the journalist tries to encrypt a reply to source1 # It succeeds journalist_reply = "s3cr3t message" encrypted_reply_path = tmp_path / "reply.gpg" encryption_mgr.encrypt_journalist_reply( for_source_with_filesystem_id=source_user1.filesystem_id, reply_in=journalist_reply, encrypted_reply_path_out=encrypted_reply_path, ) # And the output file contains the encrypted data encrypted_reply = encrypted_reply_path.read_bytes() assert encrypted_reply # And source1 is able to decrypt the reply decrypted_reply = encryption_mgr.decrypt_journalist_reply( for_source_user=source_user1, ciphertext_in=encrypted_reply) assert decrypted_reply assert decrypted_reply == journalist_reply # And source2 is NOT able to decrypt the reply with pytest.raises(GpgDecryptError): encryption_mgr.decrypt_journalist_reply( for_source_user=source_user2, ciphertext_in=encrypted_reply) # Amd the reply can't be decrypted without providing the source1's gpg secret result = encryption_mgr._gpg.decrypt( # For GPG 2.1+, a non-null passphrase _must_ be passed to decrypt() encrypted_reply, passphrase="test 123", ) assert not result.ok # And the journalist is able to decrypt their reply with import_journalist_private_key(encryption_mgr): decrypted_reply_for_journalist = encryption_mgr._gpg.decrypt( # For GPG 2.1+, a non-null passphrase _must_ be passed to decrypt() encrypted_reply, passphrase="test 123", ).data assert decrypted_reply_for_journalist.decode() == journalist_reply
def _create_source_and_submission(config_in_use: SecureDropConfig) -> Path: """Directly create a source and a submission within the app. Some tests for the journalist app require a submission to already be present, and this function is used to create the source user and submission when the journalist app starts. This implementation is much faster than using Selenium to navigate the source app in order to create a submission: it takes 0.2s to run, while the Selenium implementation takes 7s. """ # This function will be called in a separate Process that runs the app # Hence the late imports from encryption import EncryptionManager from models import Submission from passphrases import PassphraseGenerator from source_user import create_source_user from store import Storage, add_checksum_for_file from tests.functional.db_session import get_database_session # Create a source passphrase = PassphraseGenerator.get_default().generate_passphrase() with get_database_session( database_uri=config_in_use.DATABASE_URI) as db_session: source_user = create_source_user( db_session=db_session, source_passphrase=passphrase, source_app_storage=Storage.get_default(), ) source_db_record = source_user.get_db_record() EncryptionManager.get_default().generate_source_key_pair(source_user) # Create a file submission from this source source_db_record.interaction_count += 1 app_storage = Storage.get_default() encrypted_file_name = app_storage.save_file_submission( filesystem_id=source_user.filesystem_id, count=source_db_record.interaction_count, journalist_filename=source_db_record.journalist_filename, filename="filename.txt", stream=BytesIO(b"File with S3cr3t content"), ) submission = Submission(source_db_record, encrypted_file_name, app_storage) db_session.add(submission) source_db_record.pending = False source_db_record.last_updated = datetime.now(timezone.utc) db_session.commit() submission_file_path = app_storage.path(source_user.filesystem_id, submission.filename) add_checksum_for_file( session=db_session, db_obj=submission, file_path=submission_file_path, ) return Path(submission_file_path)
def test_log_user_out(self, source_app, app_storage): # Given a source user passphrase = PassphraseGenerator.get_default().generate_passphrase() create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=app_storage, ) with source_app.test_request_context(): # Who previously logged in SessionManager.log_user_in(db_session=db.session, supplied_passphrase=passphrase) # When they log out, it succeeds SessionManager.log_user_out() # And the SessionManager no longer returns a current user assert not SessionManager.is_user_logged_in(db_session=db.session) with pytest.raises(UserNotLoggedIn): SessionManager.get_logged_in_user(db_session=db.session)
def test_create_source_user(self, source_app, app_storage): # Given a passphrase passphrase = PassphraseGenerator.get_default().generate_passphrase() # When trying to create a new source user with this passphrase, it succeeds source_user = create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=app_storage, ) assert source_user assert source_user.get_db_record()
def test_create_source_user_designation_collision(self, source_app, app_storage): # Given a source in the DB existing_source = create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default().generate_passphrase(), source_app_storage=app_storage, ) existing_designation = existing_source.get_db_record().journalist_designation # And the next generated journalist designation will be identical to this source's with mock.patch.object( source_user._DesignationGenerator, "generate_journalist_designation", return_value=existing_designation, ): # When trying to create another source, it fails, because the designation is the same with pytest.raises(SourceDesignationCollisionError): create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default().generate_passphrase(), source_app_storage=app_storage, )
def test_get_logged_in_user_but_session_expired(self, source_app, app_storage): # Given a source user passphrase = PassphraseGenerator.get_default().generate_passphrase() create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=app_storage, ) with source_app.test_request_context(): # Who previously logged in SessionManager.log_user_in(db_session=db.session, supplied_passphrase=passphrase) # But we're now 6 hours later hence their session expired with mock.patch("source_app.session_manager.datetime") as mock_datetime: six_hours_later = datetime.now(timezone.utc) + timedelta(hours=6) mock_datetime.now.return_value = six_hours_later # When querying the current user from the SessionManager # it fails with the right error with pytest.raises(UserSessionExpired): SessionManager.get_logged_in_user(db_session=db.session)
def init_source(storage): """Initialize a source: create their database record, the filesystem directory that stores their submissions & replies, and their GPG key encrypted with their codename. Return a source object and their codename string. :returns: A 2-tuple. The first entry, the :class:`Source` initialized. The second, their codename string. """ passphrase = PassphraseGenerator.get_default().generate_passphrase() source_user = create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=storage, ) EncryptionManager.get_default().generate_source_key_pair(source_user) return source_user.get_db_record(), passphrase
def test_authenticate_source_user(self, source_app, app_storage): # Given a source in the DB passphrase = PassphraseGenerator.get_default().generate_passphrase() source_user = create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=app_storage, ) # When they try to authenticate using their passphrase authenticated_user = authenticate_source_user( db_session=db.session, supplied_passphrase=passphrase ) # It succeeds and the user is mapped to the right source in the DB assert authenticated_user assert authenticated_user.db_record_id == source_user.db_record_id
def test_log_user_in(self, source_app, app_storage): # Given a source user passphrase = PassphraseGenerator.get_default().generate_passphrase() source_user = create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=app_storage, ) with source_app.test_request_context(): # When they log in, it succeeds SessionManager.log_user_in(db_session=db.session, supplied_passphrase=passphrase) # And the SessionManager returns them as the current user assert SessionManager.is_user_logged_in(db_session=db.session) logged_in_user = SessionManager.get_logged_in_user(db_session=db.session) assert logged_in_user.db_record_id == source_user.db_record_id
def test_add_checksum_for_file(config, app_storage, db_model): """ Check that when we execute the `add_checksum_for_file` function, the database object is correctly updated with the actual hash of the file. We have to create our own app in order to have more control over the SQLAlchemy sessions. The fixture pushes a single app context that forces us to work within a single transaction. """ app = create_app(config) test_storage = app_storage with app.app_context(): db.create_all() source_user = create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default(). generate_passphrase(), source_app_storage=test_storage, ) source = source_user.get_db_record() target_file_path = test_storage.path(source.filesystem_id, "1-foo-msg.gpg") test_message = b"hash me!" expected_hash = "f1df4a6d8659471333f7f6470d593e0911b4d487856d88c83d2d187afa195927" with open(target_file_path, "wb") as f: f.write(test_message) if db_model == Submission: db_obj = Submission(source, target_file_path, app_storage) else: journalist, _ = utils.db_helper.init_journalist() db_obj = Reply(journalist, source, target_file_path, app_storage) db.session.add(db_obj) db.session.commit() db_obj_id = db_obj.id queued_add_checksum_for_file(db_model, db_obj_id, target_file_path, app.config["SQLALCHEMY_DATABASE_URI"]) with app.app_context(): # requery to get a new object db_obj = db_model.query.filter_by(id=db_obj_id).one() assert db_obj.checksum == "sha256:" + expected_hash
def add_source() -> Tuple[Source, str]: """ Adds a single source. """ codename = PassphraseGenerator.get_default().generate_passphrase() source_user = create_source_user( db_session=db.session, source_passphrase=codename, source_app_storage=Storage.get_default(), ) source = source_user.get_db_record() source.pending = False db.session.commit() # Generate source key EncryptionManager.get_default().generate_source_key_pair(source_user) return source, codename
def test_generate_source_key_pair(self, setup_journalist_key_and_gpg_folder, source_app, app_storage): # Given a source user with source_app.app_context(): source_user = create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default(). generate_passphrase(), source_app_storage=app_storage, ) # And an encryption manager journalist_key_fingerprint, gpg_key_dir = setup_journalist_key_and_gpg_folder encryption_mgr = EncryptionManager( gpg_key_dir=gpg_key_dir, journalist_key_fingerprint=journalist_key_fingerprint) # When using the encryption manager to generate a key pair for this source user # It succeeds encryption_mgr.generate_source_key_pair(source_user) # And the newly-created key's fingerprint was added to Redis fingerprint_in_redis = encryption_mgr._redis.hget( encryption_mgr.REDIS_FINGERPRINT_HASH, source_user.filesystem_id) assert fingerprint_in_redis source_key_fingerprint = encryption_mgr.get_source_key_fingerprint( source_user.filesystem_id) assert fingerprint_in_redis == source_key_fingerprint # And the user's newly-generated public key can be retrieved assert encryption_mgr.get_source_public_key(source_user.filesystem_id) # And the key has a hardcoded creation date to avoid leaking information about when sources # first created their account source_key_details = encryption_mgr._get_source_key_details( source_user.filesystem_id) assert source_key_details creation_date = _parse_gpg_date_string(source_key_details["date"]) assert creation_date.date( ) == EncryptionManager.DEFAULT_KEY_CREATION_DATE # And the user's key does not expire assert source_key_details["expires"] == ""
def test_source(journalist_app: Flask, app_storage: Storage) -> Dict[str, Any]: with journalist_app.app_context(): passphrase = PassphraseGenerator.get_default().generate_passphrase() source_user = create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=app_storage, ) EncryptionManager.get_default().generate_source_key_pair(source_user) source = source_user.get_db_record() return { "source_user": source_user, # TODO(AD): Eventually the next keys could be removed as they are in source_user "source": source, "codename": passphrase, "filesystem_id": source_user.filesystem_id, "uuid": source.uuid, "id": source.id, }
def test_get_logged_in_user_but_user_deleted(self, source_app, app_storage): # Given a source user passphrase = PassphraseGenerator.get_default().generate_passphrase() source_user = create_source_user( db_session=db.session, source_passphrase=passphrase, source_app_storage=app_storage, ) with source_app.test_request_context(): # Who previously logged in SessionManager.log_user_in(db_session=db.session, supplied_passphrase=passphrase) # But since then their account was deleted source_in_db = source_user.get_db_record() source_in_db.deleted_at = datetime.utcnow() db.session.commit() # When querying the current user from the SessionManager, it fails with the right error with pytest.raises(UserHasBeenDeleted): SessionManager.get_logged_in_user(db_session=db.session)
def test_were_there_submissions_today(source_app, config, app_storage): with source_app.app_context() as context: # We need to override the config to point at the per-test DB data_root = config.SECUREDROP_DATA_ROOT args = argparse.Namespace(data_root=data_root, verbose=logging.DEBUG) count_file = os.path.join(data_root, "submissions_today.txt") source_user = create_source_user( db_session=db.session, source_passphrase=PassphraseGenerator.get_default(). generate_passphrase(), source_app_storage=app_storage, ) source = source_user.get_db_record() source.last_updated = datetime.datetime.utcnow() - datetime.timedelta( hours=24 * 2) db.session.commit() submissions.were_there_submissions_today(args, context) assert io.open(count_file).read() == "0" source.last_updated = datetime.datetime.utcnow() db.session.commit() submissions.were_there_submissions_today(args, context) assert io.open(count_file).read() == "1"