def reply(journalist, source, num_replies): """Generates and submits *num_replies* replies to *source* from *journalist*. Returns reply objects as a list. :param db.Journalist journalist: The journalist to write the reply from. :param db.Source source: The source to send the reply to. :param int num_replies: Number of random-data replies to make. :returns: A list of the :class:`db.Reply`s submitted. """ assert num_replies >= 1 replies = [] for _ in range(num_replies): source.interaction_count += 1 fname = "{}-{}-reply.gpg".format(source.interaction_count, source.journalist_filename) crypto_util.encrypt( str(os.urandom(1)), [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY], store.path(source.filesystem_id, fname)) reply = db.Reply(journalist, source, fname) replies.append(reply) db.db_session.add(reply) db.db_session.commit() return replies
def reply(journalist, source, num_replies): """Generates and submits *num_replies* replies to *source* from *journalist*. Returns reply objects as a list. :param db.Journalist journalist: The journalist to write the reply from. :param db.Source source: The source to send the reply to. :param int num_replies: Number of random-data replies to make. :returns: A list of the :class:`db.Reply`s submitted. """ assert num_replies >= 1 replies = [] for _ in range(num_replies): source.interaction_count += 1 fname = "{}-{}-reply.gpg".format(source.interaction_count, source.journalist_filename) crypto_util.encrypt(str(os.urandom(1)), [ crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY ], store.path(source.filesystem_id, fname)) reply = db.Reply(journalist, source, fname) replies.append(reply) db.db_session.add(reply) db.db_session.commit() return replies
def save_file_submission(filesystem_id, count, journalist_filename, filename, stream): sanitized_filename = secure_filename(filename) # We store file submissions in a .gz file for two reasons: # # 1. Downloading large files over Tor is very slow. If we can # compress the file, we can speed up future downloads. # # 2. We want to record the original filename because it might be # useful, either for context about the content of the submission # or for figuring out which application should be used to open # it. However, we'd like to encrypt that info and have the # decrypted file automatically have the name of the original # file. Given various usability constraints in GPG and Tails, this # is the most user-friendly way we have found to do this. encrypted_file_name = "{0}-{1}-doc.gz.gpg".format( count, journalist_filename) encrypted_file_path = path(filesystem_id, encrypted_file_name) with SecureTemporaryFile("/tmp") as stf: with gzip.GzipFile(filename=sanitized_filename, mode='wb', fileobj=stf) as gzf: # Buffer the stream into the gzip file to avoid excessive # memory consumption while True: buf = stream.read(1024 * 8) if not buf: break gzf.write(buf) crypto_util.encrypt(stf, config.JOURNALIST_KEY, encrypted_file_path) return encrypted_file_name
def test_encrypt_failure(self): source, _ = utils.db_helper.init_source() with self.assertRaisesRegexp(crypto_util.CryptoException, 'no terminal at all requested'): crypto_util.encrypt( str(os.urandom(1)), [], store.path(source.filesystem_id, 'other.gpg'))
def reply(): sid, msg_candidate = request.form["sid"], request.form["msg"] try: msg = msg_candidate.decode() except (UnicodeDecodeError, UnicodeEncodeError): flash("You have entered text that we could not parse. Please try again.", "notification") return render_template("col.html", sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle())) crypto_util.encrypt(crypto_util.getkey(sid), msg, output=store.path(sid, "reply-%s.gpg" % uuid.uuid4())) return render_template("reply.html", sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle()))
def save_file_submission(sid, filename, stream): sanitized_filename = secure_filename(filename) s = StringIO() with zipfile.ZipFile(s, 'w') as zf: zf.writestr(sanitized_filename, stream.read()) s.reset() file_loc = path(sid, "%s_doc.zip.gpg" % uuid.uuid4()) crypto_util.encrypt(config.JOURNALIST_KEY, s, file_loc)
def reply(): g.source.interaction_count += 1 filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count, g.source.journalist_filename) crypto_util.encrypt( request.form["msg"], [crypto_util.getkey(g.sid), config.JOURNALIST_KEY], output=store.path(g.sid, filename) ) reply = Reply(g.user, g.source, filename) db_session.add(reply) db_session.commit() flash("Thanks! Your reply has been stored.", "notification") return redirect(url_for("col", sid=g.sid))
def reply(): g.source.interaction_count += 1 filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count, g.source.journalist_filename) crypto_util.encrypt(request.form['msg'], [crypto_util.getkey(g.sid), config.JOURNALIST_KEY], output=store.path(g.sid, filename)) reply = Reply(g.user, g.source, filename) db_session.add(reply) db_session.commit() flash("Thanks! Your reply has been stored.", "notification") return redirect(url_for('col', sid=g.sid))
def save_file_submission(sid, filename, stream, content_type, strip_metadata): sanitized_filename = secure_filename(filename) clean_file = sanitize_metadata(stream, content_type, strip_metadata) s = StringIO() with zipfile.ZipFile(s, 'w') as zf: zf.writestr(sanitized_filename, clean_file.read() if clean_file else stream.read()) s.reset() filename = "%s_doc.zip.gpg" % uuid.uuid4() file_loc = path(sid, filename) crypto_util.encrypt(config.JOURNALIST_KEY, s, file_loc) return filename
def reply(): """Attempt to send a Reply from a Journalist to a Source. Empty messages are rejected, and an informative error message is flashed on the client. In the case of unexpected errors involving database transactions (potentially caused by racing request threads that modify the same the database object) logging is done in such a way so as not to write potentially sensitive information to disk, and a generic error message is flashed on the client. Returns: flask.Response: The user is redirected to the same Source collection view, regardless if the Reply is created successfully. """ form = ReplyForm() if not form.validate_on_submit(): for error in form.message.errors: flash(error, "error") return redirect(url_for('col.col', filesystem_id=g.filesystem_id)) g.source.interaction_count += 1 filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count, g.source.journalist_filename) crypto_util.encrypt(form.message.data, [crypto_util.getkey(g.filesystem_id), config.JOURNALIST_KEY], output=store.path(g.filesystem_id, filename)) reply = Reply(g.user, g.source, filename) try: db_session.add(reply) db_session.commit() except Exception as exc: flash(gettext( "An unexpected error occurred! Please " "inform your administrator."), "error") # We take a cautious approach to logging here because we're dealing # with responses to sources. It's possible the exception message # could contain information we don't want to write to disk. current_app.logger.error( "Reply from '{}' (ID {}) failed: {}!".format(g.user.username, g.user.id, exc.__class__)) else: flash(gettext("Thanks. Your reply has been stored."), "notification") finally: return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
def reply(): """Attempt to send a Reply from a Journalist to a Source. Empty messages are rejected, and an informative error message is flashed on the client. In the case of unexpected errors involving database transactions (potentially caused by racing request threads that modify the same the database object) logging is done in such a way so as not to write potentially sensitive information to disk, and a generic error message is flashed on the client. Returns: flask.Response: The user is redirected to the same Source collection view, regardless if the Reply is created successfully. """ form = ReplyForm() if not form.validate_on_submit(): for error in form.message.errors: flash(error, "error") return redirect(url_for('col.col', filesystem_id=g.filesystem_id)) g.source.interaction_count += 1 filename = "{0}-{1}-reply.gpg".format(g.source.interaction_count, g.source.journalist_filename) crypto_util.encrypt( form.message.data, [crypto_util.getkey(g.filesystem_id), config.JOURNALIST_KEY], output=store.path(g.filesystem_id, filename)) reply = Reply(g.user, g.source, filename) try: db_session.add(reply) db_session.commit() except Exception as exc: flash( gettext("An unexpected error occurred! Please " "inform your administrator."), "error") # We take a cautious approach to logging here because we're dealing # with responses to sources. It's possible the exception message # could contain information we don't want to write to disk. current_app.logger.error( "Reply from '{}' (ID {}) failed: {}!".format( g.user.username, g.user.id, exc.__class__)) else: flash(gettext("Thanks. Your reply has been stored."), "notification") finally: return redirect(url_for('col.col', filesystem_id=g.filesystem_id))
def reply(): sid, msg_candidate = request.form['sid'], request.form['msg'] try: msg = msg_candidate.decode() except (UnicodeDecodeError, UnicodeEncodeError): flash( "You have entered text that we could not parse. Please try again.", "notification") return render_template('col.html', sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle())) crypto_util.encrypt(crypto_util.getkey(sid), msg, output=store.path(sid, 'reply-%s.gpg' % uuid.uuid4())) return render_template('reply.html', sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle()))
def test_encrypt_success(self): source, _ = utils.db_helper.init_source() message = str(os.urandom(1)) ciphertext = crypto_util.encrypt( message, [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY], store.path(source.filesystem_id, 'somefile.gpg')) self.assertIsInstance(ciphertext, str) self.assertNotEqual(ciphertext, message) self.assertGreater(len(ciphertext), 0)
def test_encrypt_fingerprints_not_a_list_or_tuple(self): """If passed a single fingerprint as a string, encrypt should correctly place that string in a list, and encryption/ decryption should work as intended.""" source, codename = utils.db_helper.init_source() message = str(os.urandom(1)) ciphertext = crypto_util.encrypt( message, crypto_util.getkey(source.filesystem_id), store.path(source.filesystem_id, 'somefile.gpg')) plaintext = crypto_util.decrypt(codename, ciphertext) self.assertEqual(message, plaintext)
def submit(): msg = request.form["msg"] fh = request.files["fh"] if msg: msg_loc = store.path(g.sid, "%s_msg.gpg" % uuid.uuid4()) crypto_util.encrypt(config.JOURNALIST_KEY, msg, msg_loc) flash("Thanks! We received your message.", "notification") if fh: file_loc = store.path(g.sid, "%s_doc.zip.gpg" % uuid.uuid4()) s = StringIO() zip_file = zipfile.ZipFile(s, "w") zip_file.writestr(fh.filename, fh.read()) zip_file.close() s.reset() crypto_util.encrypt(config.JOURNALIST_KEY, s, file_loc) flash("Thanks! We received your document '%s'." % fh.filename or "[unnamed]", "notification") return redirect(url_for("lookup"))
def test_encrypt_without_output(self): """We simply do not specify the option output keyword argument to crypto_util.encrypt() here in order to confirm encryption works when it defaults to `None`. """ source, codename = utils.db_helper.init_source() message = str(os.urandom(1)) ciphertext = crypto_util.encrypt( message, [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY]) plaintext = crypto_util.decrypt(codename, ciphertext) self.assertEqual(message, plaintext)
def test_basic_encrypt_then_decrypt_multiple_recipients(self): source, codename = utils.db_helper.init_source() message = str(os.urandom(1)) ciphertext = crypto_util.encrypt( message, [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY], store.path(source.filesystem_id, 'somefile.gpg')) plaintext = crypto_util.decrypt(codename, ciphertext) self.assertEqual(message, plaintext) # Since there's no way to specify which key to use for # decryption to python-gnupg, we delete the `source`'s key and # ensure we can decrypt with the `config.JOURNALIST_KEY`. crypto_util.delete_reply_keypair(source.filesystem_id) plaintext_ = crypto_util.gpg.decrypt(ciphertext).data self.assertEqual(message, plaintext_)
def test_encrypt_binary_stream(self): """Generally, we pass unicode strings (the type form data is returned as) as plaintext to crypto_util.encrypt(). These have to be converted to "binary stream" types (such as `file`) before we can actually call gnupg.GPG.encrypt() on them. This is done in crypto_util.encrypt() with an `if` branch that uses `gnupg._util._is_stream(plaintext)` as the predicate, and calls `gnupg._util._make_binary_stream(plaintext)` if necessary. This test ensures our encrypt function works even if we provide inputs such that this `if` branch is skipped (i.e., the object passed for `plaintext` is one such that `gnupg._util._is_stream(plaintext)` returns `True`). """ source, codename = utils.db_helper.init_source() with open(os.path.realpath(__file__)) as fh: ciphertext = crypto_util.encrypt( fh, [crypto_util.getkey(source.filesystem_id), config.JOURNALIST_KEY], store.path(source.filesystem_id, 'somefile.gpg')) plaintext = crypto_util.decrypt(codename, ciphertext) with open(os.path.realpath(__file__)) as fh: self.assertEqual(fh.read(), plaintext)
def save_message_submission(filesystem_id, count, journalist_filename, message): filename = "{0}-{1}-msg.gpg".format(count, journalist_filename) msg_loc = path(filesystem_id, filename) crypto_util.encrypt(message, config.JOURNALIST_KEY, msg_loc) return filename
def save_message_submission(sid, message): filename = "%s_msg.gpg" % uuid.uuid4() msg_loc = path(sid, filename) crypto_util.encrypt(config.JOURNALIST_KEY, message, msg_loc) return filename
def reply(): msg = request.form['msg'] crypto_util.encrypt(crypto_util.getkey(g.sid), msg, output= store.path(g.sid, 'reply-%s.gpg' % uuid.uuid4())) return render_template('reply.html', sid=g.sid, codename=g.source.journalist_designation)
def reply(): sid, msg = request.form['sid'], request.form['msg'] crypto_util.encrypt(crypto_util.getkey(sid), request.form['msg'], output= store.path(sid, 'reply-%s.gpg' % uuid.uuid4())) return render_template('reply.html', sid=sid, codename=db.display_id(sid, db.sqlalchemy_handle()))
def save_message_submission(sid, message): msg_loc = path(sid, '%s_msg.gpg' % uuid.uuid4()) crypto_util.encrypt(config.JOURNALIST_KEY, message, msg_loc)