def test_source_is_deleted_while_logged_in(self, logger): """If a source is deleted by a journalist when they are logged in, a NoResultFound will occur. The source should be redirected to the index when this happens, and a warning logged.""" with self.client as client: codename = new_codename(client, session) resp = client.post('login', data=dict(codename=codename), follow_redirects=True) # Now the journalist deletes the source filesystem_id = crypto_util.hash_codename(codename) crypto_util.delete_reply_keypair(filesystem_id) source = Source.query.filter_by(filesystem_id=filesystem_id).one() db_session.delete(source) db_session.commit() # Source attempts to continue to navigate resp = client.post('/lookup', follow_redirects=True) self.assertEqual(resp.status_code, 200) self.assertIn('Submit documents for the first time', resp.data) self.assertNotIn('logged_in', session.keys()) self.assertNotIn('codename', session.keys()) logger.assert_called_once_with( "Found no Sources when one was expected: " "No row was found for one()")
def _can_decrypt_with_key(self, msg, key_fpr, passphrase=None): """ Test that the given GPG message can be decrypted with the given key (identified by its fingerprint). """ # GPG does not provide a way to specify which key to use to decrypt a # message. Since the default keyring that we use has both the # `config.JOURNALIST_KEY` and all of the reply keypairs, there's no way # to use it to test whether a message is decryptable with a specific # key. gpg_tmp_dir = tempfile.mkdtemp() gpg = gnupg.GPG(homedir=gpg_tmp_dir) # Export the key of interest from the application's keyring pubkey = self.gpg.export_keys(key_fpr) seckey = self.gpg.export_keys(key_fpr, secret=True) # Import it into our isolated temporary GPG directory for key in (pubkey, seckey): gpg.import_keys(key) # Attempt decryption with the given key if passphrase: passphrase = crypto_util.hash_codename( passphrase, salt=crypto_util.SCRYPT_GPG_PEPPER) decrypted_data = gpg.decrypt(msg, passphrase=passphrase) self.assertTrue( decrypted_data.ok, "Could not decrypt msg with key, gpg says: {}".format( decrypted_data.stderr)) # We have to clean up the temporary GPG dir shutil.rmtree(gpg_tmp_dir)
def wait_for_source_key(self, source_name): filesystem_id = crypto_util.hash_codename(source_name) def key_available(filesystem_id): assert crypto_util.getkey(filesystem_id) self.wait_for( lambda: key_available(filesystem_id), timeout=60)
def wait_for_source_key(self, source_name): filesystem_id = crypto_util.hash_codename(source_name) def key_available(filesystem_id): assert crypto_util.getkey(filesystem_id) self.wait_for(lambda: key_available(filesystem_id), timeout=60)
def generate_unique_codename(num_words): """Generate random codenames until we get an unused one""" while True: codename = crypto_util.genrandomid(num_words) sid = crypto_util.hash_codename(codename) # scrypt (slow) matching_sources = Source.query.filter(Source.filesystem_id == sid).all() if len(matching_sources) == 0: return codename
def create(): sid = crypto_util.hash_codename(session['codename']) if os.path.exists(store.path(sid)): # if this happens, we're not using very secure crypto app.logger.warning("Got a duplicate ID '%s'" % sid) else: os.mkdir(store.path(sid)) session['logged_in'] = True session['flagged'] = False return redirect(url_for('lookup'))
def test_genkeypair(self): codename = crypto_util.genrandomid() filesystem_id = crypto_util.hash_codename(codename) journalist_filename = crypto_util.display_id() source = db.Source(filesystem_id, journalist_filename) db.db_session.add(source) db.db_session.commit() crypto_util.genkeypair(source.filesystem_id, codename) self.assertIsNotNone(crypto_util.getkey(filesystem_id))
def valid_codename(codename): try: filesystem_id = crypto_util.hash_codename(codename) except crypto_util.CryptoException as e: app.logger.warning( "Could not compute filesystem ID for codename '{}': {}".format( codename, e)) abort(500) source = Source.query.filter_by(filesystem_id=filesystem_id).first() return source is not None
def create(): sid = crypto_util.hash_codename(session['codename']) if os.path.exists(store.path(sid)): # if this happens, we're not using very secure crypto log.warning("Got a duplicate ID '%s'" % sid) else: os.mkdir(store.path(sid)) session['logged_in'] = True session['flagged'] = False return redirect(url_for('lookup'))
def setup_g(): """Store commonly used values in Flask's special g object""" # ignore_static here because `crypto_util.hash_codename` is scrypt (very # time consuming), and we don't need to waste time running if we're just # serving a static resource that won't need to access these common values. if logged_in(): # We use session.get (which defaults to None if 'flagged' is not in the # session) to avoid a KeyError on the redirect from login/ to lookup/ g.flagged = session.get('flagged') g.codename = session['codename'] g.sid = crypto_util.hash_codename(g.codename) g.loc = store.path(g.sid)
def create(): sid = crypto_util.hash_codename(session['codename']) source = Source(sid, crypto_util.display_id()) db_session.add(source) try: db_session.commit() except IntegrityError as e: app.logger.error("Attempt to create a source with duplicate codename: %s" % (e,)) else: os.mkdir(store.path(sid)) session['logged_in'] = True return redirect(url_for('lookup'))
def create(): sid = crypto_util.hash_codename(session['codename']) source = Source(sid, crypto_util.display_id()) db_session.add(source) db_session.commit() if os.path.exists(store.path(sid)): # if this happens, we're not using very secure crypto log.warning("Got a duplicate ID '%s'" % sid) else: os.mkdir(store.path(sid)) session['logged_in'] = True return redirect(url_for('lookup'))
def setup_g(): """Store commonly used values in Flask's special g object""" # ignore_static here because `crypto_util.hash_codename` is scrypt (very # time consuming), and we don't need to waste time running if we're just # serving a static resource that won't need to access these common values. if logged_in(): g.codename = session['codename'] g.sid = crypto_util.hash_codename(g.codename) try: g.source = Source.query.filter(Source.filesystem_id == g.sid).one() except MultipleResultsFound as e: app.logger.error("Found multiple Sources when one was expected: %s" % (e,)) abort(500) except NoResultFound as e: app.logger.error("Found no Sources when one was expected: %s" % (e,)) abort(404) g.loc = store.path(g.sid)
def valid_codename(codename): # Ignore codenames that are too long to avoid DoS if len(codename) > Source.MAX_CODENAME_LEN: app.logger.info( "Ignored attempted login because the codename was too long.") return False try: filesystem_id = crypto_util.hash_codename(codename) except crypto_util.CryptoException as e: app.logger.info( "Could not compute filesystem ID for codename '{}': {}".format( codename, e)) abort(500) source = Source.query.filter_by(filesystem_id=filesystem_id).first() return source is not None
def setup_g(): """Store commonly used values in Flask's special g object""" # ignore_static here because `crypto_util.hash_codename` is scrypt (very # time consuming), and we don't need to waste time running if we're just # serving a static resource that won't need to access these common values. if logged_in(): g.codename = session['codename'] g.sid = crypto_util.hash_codename(g.codename) try: g.source = Source.query.filter(Source.filesystem_id == g.sid).one() except MultipleResultsFound as e: app.logger.error("Found multiple Sources when one was expected: %s" % (e,)) abort(500) except NoResultFound as e: app.logger.error("Found no Sources when one was expected: %s" % (e,)) del session['logged_in'] del session['codename'] return redirect(url_for('index')) g.loc = store.path(g.sid)
def init_source_without_keypair(): """Initialize a source: create their database record and the filesystem directory that stores their submissions & replies. Return a source object and their codename string. :returns: A 2-tuple. The first entry, the :class:`db.Source` initialized. The second, their codename string. """ # Create source identity and database record codename = crypto_util.genrandomid() filesystem_id = crypto_util.hash_codename(codename) journalist_filename = crypto_util.display_id() source = db.Source(filesystem_id, journalist_filename) db.db_session.add(source) db.db_session.commit() # Create the directory to store their submissions and replies os.mkdir(store.path(source.filesystem_id)) return source, codename
def create(): filesystem_id = crypto_util.hash_codename(session['codename']) source = Source(filesystem_id, crypto_util.display_id()) db_session.add(source) try: db_session.commit() except IntegrityError as e: db_session.rollback() current_app.logger.error( "Attempt to create a source with duplicate codename: %s" % (e, )) # Issue 2386: don't log in on duplicates del session['codename'] abort(500) else: os.mkdir(store.path(filesystem_id)) session['logged_in'] = True return redirect(url_for('.lookup'))
def setup_g(): """Store commonly used values in Flask's special g object""" g.locale = i18n.get_locale(config) g.text_direction = i18n.get_text_direction(g.locale) g.html_lang = i18n.locale_to_rfc_5646(g.locale) g.locales = i18n.get_locale2name() if 'expires' in session and datetime.utcnow() >= session['expires']: msg = render_template('session_timeout.html') # clear the session after we render the message so it's localized session.clear() flash(Markup(msg), "important") session['expires'] = datetime.utcnow() + \ timedelta(minutes=getattr(config, 'SESSION_EXPIRATION_MINUTES', 120)) # ignore_static here because `crypto_util.hash_codename` is scrypt # (very time consuming), and we don't need to waste time running if # we're just serving a static resource that won't need to access # these common values. if logged_in(): g.codename = session['codename'] g.filesystem_id = crypto_util.hash_codename(g.codename) try: g.source = Source.query \ .filter(Source.filesystem_id == g.filesystem_id) \ .one() except NoResultFound as e: app.logger.error( "Found no Sources when one was expected: %s" % (e,)) del session['logged_in'] del session['codename'] return redirect(url_for('main.index')) g.loc = store.path(g.filesystem_id)
def setup_g(): """Store commonly used values in Flask's special g object""" g.locale = i18n.get_locale(config) g.text_direction = i18n.get_text_direction(g.locale) g.html_lang = i18n.locale_to_rfc_5646(g.locale) g.locales = i18n.get_locale2name() if 'expires' in session and datetime.utcnow() >= session['expires']: msg = render_template('session_timeout.html') # clear the session after we render the message so it's localized session.clear() flash(Markup(msg), "important") session['expires'] = datetime.utcnow() + \ timedelta(minutes=getattr(config, 'SESSION_EXPIRATION_MINUTES', 120)) # ignore_static here because `crypto_util.hash_codename` is scrypt # (very time consuming), and we don't need to waste time running if # we're just serving a static resource that won't need to access # these common values. if logged_in(): g.codename = session['codename'] g.filesystem_id = crypto_util.hash_codename(g.codename) try: g.source = Source.query \ .filter(Source.filesystem_id == g.filesystem_id) \ .one() except NoResultFound as e: app.logger.error("Found no Sources when one was expected: %s" % (e, )) del session['logged_in'] del session['codename'] return redirect(url_for('main.index')) g.loc = store.path(g.filesystem_id)
def generate_unique_codename(): """Generate random codenames until we get an unused one""" while True: codename = crypto_util.genrandomid(Source.NUM_WORDS) # The maximum length of a word in the wordlist is 9 letters and the # codename length is 7 words, so it is currently impossible to # generate a codename that is longer than the maximum codename length # (currently 128 characters). This code is meant to be defense in depth # to guard against potential future changes, such as modifications to # the word list or the maximum codename length. if len(codename) > Source.MAX_CODENAME_LEN: app.logger.warning( "Generated a source codename that was too long, " "skipping it. This should not happen. " "(Codename='{}')".format(codename)) continue filesystem_id = crypto_util.hash_codename(codename) # scrypt (slow) matching_sources = Source.query.filter( Source.filesystem_id == filesystem_id).all() if len(matching_sources) == 0: return codename
def _source_delete_key(self): filesystem_id = crypto_util.hash_codename(self.source_name) crypto_util.delete_reply_keypair(filesystem_id)
def _block_on_reply_keypair_gen(codename): sid = crypto_util.hash_codename(codename) while not crypto_util.getkey(sid): sleep(0.1)
def valid_codename(codename): return os.path.exists(store.path(crypto_util.hash_codename(codename)))
def test_hash_codename(self): codename = crypto_util.genrandomid() hashed_codename = crypto_util.hash_codename(codename) self.assertRegexpMatches(hashed_codename, '^[2-7A-Z]{103}=$')