def watch_canary(sigid_base64): canary = get_canary(sigid_base64) if canary is None: return page_not_found('canary') if request.method == 'GET': return dict(canary=canary) if request.method == 'POST': try: email = request.form['email'] except KeyError: flash(err_messages['incomplete_form'], 'error') return dict(canary=canary) except Exception as e: flash(err_messages['generic'], 'error') app.logger.error(e) return dict(canary=canary) if not re.search('@', email) or len(email) > 254: flash(err_messages['invalid_email'], 'error') return dict(canary=canary) alerts = request.form.getlist('alerts') on_publish = 'onPublish' in alerts on_overdue = 'onOverdue' in alerts # on_delete = list['onDelete'] delay_days = 0 if on_overdue: delay = int(request.form['delay']) delay_type = request.form['delayType'] if not delay or not delay_type: on_overdue = False allowed_delays = 'day', 'week', 'month' in_range = 1 <= delay <= 100 if delay_type not in allowed_delays or not in_range: flash(err_messages['incomplete_form'], 'error') return dict(canary=canary) # Get the delay in days delay_days = days(delay, delay_type) # TODO: notify watchers when an canary is deleted # on_delete = request.form.get('onDelete') or False if not (on_publish or on_overdue): flash(err_messages['incomplete_form'], 'error') return dict(canary=canary) secret = os.urandom(16).encode('hex') alert = Alert(email, canary, False, on_overdue, on_publish, delay_days, secret) db_session.add(alert) db_session.commit() # Send verification email send_verification_email.delay(alert, canary) return redirect(url_for('canary', sigid_base64=sigid_base64))
def setUp(self): init_db() self.user = User('C2DFB998', '2fa69032cba961ff8387ef06d2969d08c2dfb998', 'Foo Bar <*****@*****.**>') self.sekrit = os.urandom(16).encode('hex') self.chal = Challenge(sha256(self.sekrit).hexdigest()) db_session.add(self.user) db_session.add(self.chal) db_session.commit()
def verify_watch_canary(code): try: a = Alert.query.filter(Alert.hash == code).one() canary = Canary.query.filter(Canary.id == a.canary_id).one() except: return page_not_found('canary') if request.method == 'GET': a.active = True db_session.commit() flash(messages['alert_verified'], 'message') return redirect(url_for('index'))
def check(cls, obj, solution): """Check if the SHA-255 hash of ``solution`` matches the challenge. """ if not is_hex_challenge(solution): return False chal = Challenge.query.filter(Challenge.id == obj.chal_id).one() # Set obj.chal_id to None so chal is never used again obj.chal_id = None db_session.commit() return chal.hash == hashlib.sha256(solution).hexdigest()
def delete(self, path): """Delete the canary in the filesystem and database, and delete the canary's publisher if this is the user's only canary. """ os.unlink(path) if len(self.user.canaries) == 1: db_session.delete(self.user) if "uid" in session: session.pop("uid") if "fp" in session: session.pop("fp") db_session.delete(self) db_session.commit()
def test_verify_watch_canary(self): """Test that visiting a verification page changes an Alert from unverified to verified. """ c = self._create_canary() a = Alert(c.user.uid, c, True, True, True, 3, 'sekrit') db_session.add(a) db_session.commit() with app.app_context(): r = self.app.get('verify/{}'.format(a.hash), follow_redirects=True) self.assertTrue(a.active) self.assertIn(messages['alert_verified'], r.data)
def generate(cls, obj, fingerprint): """Generate a challenge and return the hash of the challenge and the challenge encrypted with GPG. """ secret = os.urandom(16).encode("hex") hash = hashlib.sha256(secret).hexdigest() ciphertext = gpg.encrypt(secret, fingerprint) chal = Challenge(hash) db_session.add(chal) db_session.commit() obj.chal_id = chal.id db_session.commit() return ciphertext
def edit_canary(sigid_base64): if not logged_in(): return redirect(url_for('login')) canary = get_canary(sigid_base64) if canary is None: return page_not_found('canary') if logged_in and canary.user.fingerprint == session['fp']: freq_num = request.form['frequencyNum'] freq = request.form['frequency'] canary.freq_type = freq canary.frequency = days(freq_num, freq) canary.active = True db_session.commit() flash(messages['canary_updated'], 'message') return redirect(url_for('canary', sigid_base64=sigid_base64)) else: abort(403)
def send_overdue_alert(alert, canary, days_overdue): """Send a reminder to canary watchers the canary is overdue.""" if days_overdue >= alert.delay: canary_url = config.URL + '/canary/{}'.format( canary.sigid_base64) msg = format_message('overdue_alert', [alert.email], canary.user.uid, canary.user.uid, days_overdue, canary_url) """Set active to False so we don't send another alert unless the canary owner republishes or deletes the canary.""" alert.active = False db_session.commit() with app.app_context(): try: mail.send(msg) except SMTPException as e: app.logger.error(e)
def remind(canary, user): """Send an email to remind ``user`` to republish ``canary``. The email includes a link to a page with a challenge to decrypt. """ if not app.testing: canary = db_session.merge(canary) user = db_session.merge(user) canary_url = config.URL + '/canary/{}'.format(canary.sigid_base64) msg = format_message('reminder', [user.uid], canary_url, canary.frequency, canary.freq_type) """Set active to False so we don't send another reminder email unless the user republishes the canary.""" canary.active = False db_session.commit() with app.app_context(): try: mail.send(msg) except SMTPException as e: app.logger.error(e)
def new_canary(): if request.method == 'GET': return None if request.method == 'POST': try: signed = request.form['signedMessage'] frequency_num = int(request.form['frequencyNum']) frequency_type = request.form['frequency'] except KeyError: flash(err_messages['incomplete_form'], 'error') return None allowed_freqs = 'day', 'week', 'month' in_range = 1 <= frequency_num <= 100 if frequency_type not in allowed_freqs or not in_range: flash(err_messages['invalid_freq'], 'error') return None # Get the frequency in days frequency = days(frequency_num, frequency_type) verified, err = gpg.verify(signed) # Start over if the message wasn't verified. if err and not verified: flash(err, 'error') return None fp = verified.fingerprint sigid_base64 = base64.urlsafe_b64encode(verified.signature_id) try: canary = Canary(sigid_base64, frequency, frequency_type) db_session.add(canary) db_session.commit() except IntegrityError: # Throw an error if a canary with that sigid already exists db_session.rollback() db_session.flush() flash(err_messages['dupe_canary'], 'error') return redirect(url_for('new_canary')) except Exception as e: db_session.rollback() db_session.flush() app.logger.error(e) """An unexpected database error should not reveal any error details to the user.""" flash(err_messages['generic'], 'error') return None ciphertext = Challenge.generate(canary, fp) # TODO: This is sloppy. session['canary'] = dict(fp=verified.fingerprint.lower(), text=signed, uid=verified.username, keyid=verified.key_id, sigid_base64=sigid_base64, frequency=frequency, freq_type=frequency_type, ciphertext=str(ciphertext)) flash(messages['verified'], 'message') return dict(canary=session['canary'])
def canary(sigid_base64): if request.method == 'GET': canary = get_canary(sigid_base64) if canary is None: return page_not_found('canary') pathstr = str(sigid_base64) path = os.path.join(app.config.get('CANARIES_DIR'), pathstr) f = open(path, 'r') text = f.read() f.close() return dict(canary=canary, text=text) if request.method == 'POST': if not is_sigid(sigid_base64): return redirect(url_for('index')) try: canary = Canary.query.filter( Canary.sigid_base64 == sigid_base64).one() decrypted = request.form['decrypted'].strip() if not Challenge.check(canary, decrypted): raise IncorrectChallengeException except KeyError: flash(err_messages['incomplete_form'], 'error') return None except IncorrectChallengeException: db_session.delete(canary) db_session.commit() flash(err_messages['decrypt_fail'], 'error') return redirect(url_for('new_canary')) except Exception as e: flash(err_messages['generic'], 'error') app.logger.error(e) return redirect(url_for('new_canary')) sess = session['canary'] fp = sess['fp'] try: user = User.query.filter(User.fingerprint == fp).one() """Update the existing user's key info, in case the username or email address has been edited since we last saw it.""" user.update(canary['uid']) except NoResultFound: # Create a new user user = User(sess['keyid'], fp, sess['uid']) db_session.add(user) db_session.commit() canary.user_id = user.id canary.active = True db_session.commit() with app.app_context(): text = sess['text'] if app.testing: notify(canary, user, text) else: notify.delay(canary, user, text) pathstr = str(sigid_base64) path = os.path.join(app.config.get('CANARIES_DIR'), pathstr) with open(path, 'w') as f: f.write(text) f.close() flash(messages['published'], 'message') return redirect(url_for('canary', sigid_base64=sigid_base64))
def _create_user(self): """Helper for creating a user.""" u = User('C2DFB998', self.fp, 'Foo Bar <*****@*****.**>') db_session.add(u) db_session.commit() return u
def republish(self): """Republish the canary.""" self.date_last_updated = datetime.datetime.now() self.active = True db_session.commit()
def update(self, uid): """Updates the user's uid if it has changed.""" if uid != self.uid: self.uid = uid db_session.commit()