diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..095cb45 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,5 @@ +[bumpversion] +files = setup.py +commit = True +tag = True +current_version = 0.1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2950c9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.tox +.coverage +.project +*.rdb +junit*xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..51b7061 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python +python: 2.7 +install: + - pip install tox +script: + - tox +services: + - redis-server diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..0a79475 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,6 @@ +Credits +======= + +"snappass" is originally written and by Owen Coutts and Ryan Park. + +It is currently maintained by Dave Dash and Pinterest. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..0d05ba5 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,126 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/pinterest/snappass/issues. + +If you are reporting a bug, please include: + +* Your operating system name and version (if relevant). +* Any details about your local setup that might be helpful in troubleshooting. +* If you can, provide detailed steps to reproduce the bug. +* If you don't have steps to reproduce the bug, just note your observations in + as much detail as you can. Questions to start a discussion about the issue + are welcome. + +Python 3.3 Support +~~~~~~~~~~~~~~~~~~ + +We'd love for ``tox -e py33`` to work and would welcome anybody who can help +make that a reality. + +Fix Bugs +~~~~~~~~ + +Look through the GitHub issues for bugs. Anything tagged with "bug" +is open to whoever wants to implement it. + +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything tagged with "enhancement" +is open to whoever wants to implement it. + + +Write Documentation +~~~~~~~~~~~~~~~~~~~ + +Snappass could always use better documentation, whether as part of the +official docs, in docstrings, or even on the web in blog posts, articles, and +such. + +Submit Feedback +~~~~~~~~~~~~~~~ + +The best way to send feedback is to file an issue at +https://github.com/pinterest/snappass/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Note that this project has an intentionally narrow scope. + Our target users are small organizations that really need a + quick and dirty way to exchange secrets. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + + +Setting Up the Code for Local Development +----------------------------------------- + +Here's how to set up `snappass` for local development. + +1. Fork the `snappass` repo on GitHub. +2. Clone your fork locally:: + + $ git clone git@github.com:your_name_here/snappass.git + +3. Install your local copy into a ``virtualenv``. Assuming you have + ``virtualenvwrapper`` installed, this is how you set up your fork for local + development:: + + $ mkvirtualenv snappass + $ cd snappass/ + $ python setup.py develop + +4. Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +5. When you're done making changes, check that your changes pass the tests and + flake8:: + + $ flake8 snappass tests + $ tox + +6. Commit your changes and push your branch to GitHub:: + + $ git add . + $ git commit -m "Your detailed description of your changes." + $ git push origin name-of-your-bugfix-or-feature + +7. Check that the test coverage hasn't dropped:: + + coverage run --source snappass setup.py tests + coverage report -m + coverage html + +8. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +2. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring, and add the + feature to the list in README.rst. +3. The pull request should work for Python 2.7 and ideally 3.3. Check + `Travis`_ and make sure that + the tests pass for all supported Python versions. + +.. _Travis: https://travis-ci.org/pinterest/snappass/pull_requests diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fa6a9b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012-2013 Pinterest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ec67e98 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include *.rst LICENSE +recursive-include snappass/static * +recursive-include snappass/templates * diff --git a/README b/README deleted file mode 100644 index 636cc31..0000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -Redis frontend to securley share passwords diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..500e8e1 --- /dev/null +++ b/README.rst @@ -0,0 +1,61 @@ +======== +SnapPass +======== + +.. image:: https://travis-ci.org/pinterest/snappass.png + + +It's like SnapChat... for Passwords. + +This is a webapp that lets you share passwords securely. + +Let's say you have a password. You want to give it to your coworker, Jane. +You could email it to her, but then it's in her email, which might be backed up, +and probably is in some storage device controlled by the NSA. + +You could send it to her over chat, but chances are Jane logs all her messages +because she uses Google Talk, and Google Talk logs everything. + +You could write it down, but you can't find a pen, and there's way too many +characters because your Security Person, Paul, is paranoid. + +So we build SnapPass. It's not that complicated, it does one thing. If +Jane gets a link to the password and never looks at it, the password goes away. +If the NSA gets a hold of the link, and they look at the password... well they +have the password. Also, Jane can't get the password, but now Jane knows that +not only is someone looking in her email, they are clicking on links. + +Anyway, this took us very little time to write, but we figure we'd save you the +trouble of writing it yourself, because maybe you are busy and have other things +to do. Enjoy. + +Requirements +------------ + +* Redis. +* Python 2.6, 2.7 or 3.3. + +Installation +------------ + +:: + + $ pip install snappass + $ snappass + * Running on http://0.0.0.0:5000/ + * Restarting with reloader + +Configuration +------------- + +You can configure the following via environment variables. + +`SECRET_KEY` this should be a unique key that's used to sign key. This should +be kept secret. See the `Flask Documentation`_ for more information. + +.. _Flask Documentation: http://flask.pocoo.org/docs/quickstart/#sessions + +`STATIC_URL` this should be the location of your static assets. You might not +need to change this. + +`NO_SSL` if you are not using SSL. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..222c1be --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Flask==0.10.1 +Jinja2==2.7.1 +MarkupSafe==0.18 +Werkzeug==0.9.4 +itsdangerous==0.23 +redis==2.8.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..edce534 --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +from setuptools import setup + +setup( + name='snappass', + version='0.1.0', + description="It's like SnapChat... for Passwords.", + long_description=(open('README.rst').read() + '\n\n' + + open('AUTHORS.rst').read()), + url='http://github.com/Pinterest/snappass/', + install_requires=['Flask', 'redis'], + license='MIT', + author='Dave Dash', + author_email='dd+github@davedash.com', + packages=['snappass'], + entry_points={ + 'console_scripts': [ + 'snappass = snappass.main:main', + ], + }, + include_package_data=True, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], +) + diff --git a/snappass/__init__.py b/snappass/__init__.py new file mode 100644 index 0000000..845eb13 --- /dev/null +++ b/snappass/__init__.py @@ -0,0 +1 @@ +__author__ = 'davedash' diff --git a/app.py b/snappass/main.py similarity index 59% rename from app.py rename to snappass/main.py index 4aafad1..88f53c9 100644 --- a/app.py +++ b/snappass/main.py @@ -6,13 +6,15 @@ import redis from flask import abort, Flask, render_template, request -application = Flask(__name__) -application.secret_key = os.environ.get('SECRET_KEY', 'Secret Key') -application.config.update(dict(STATIC_URL=os.environ.get('STATIC_URL', 'static'))) +NO_SSL = os.environ.get('NO_SSL', False) +app = Flask(__name__) +app.secret_key = os.environ.get('SECRET_KEY', 'Secret Key') +app.config.update( + dict(STATIC_URL=os.environ.get('STATIC_URL', 'static'))) -id = lambda: uuid.uuid4().get_hex() +id_ = lambda: uuid.uuid4().hex redis_host = os.environ.get('REDIS_HOST', 'localhost') -r = redis.StrictRedis(host=redis_host, port=6379, db=0) +redis_client = redis.StrictRedis(host=redis_host, port=6379, db=0) time_conversion = { 'week': 604800, @@ -20,17 +22,20 @@ time_conversion = { 'hour': 3600 } + def set_password(password, ttl): - key = id() - r.set(key, password) - r.expire(key, ttl) + key = id_() + redis_client.set(key, password) + redis_client.expire(key, ttl) return key + def get_password(key): - password = r.get(key) - r.delete(key) + password = redis_client.get(key) + redis_client.delete(key) return password + def clean_input(): """ Make sure we're not getting bad data from the front end, @@ -47,19 +52,27 @@ def clean_input(): abort(400) return time_conversion[time_period], request.form['password'] - -@application.route('/', methods=['GET']) + + +@app.route('/', methods=['GET']) def index(): return render_template('set_password.html') -@application.route('/', methods=['POST']) + +@app.route('/', methods=['POST']) def handle_password(): ttl, password = clean_input() key = set_password(password, ttl) - link = request.url_root.replace("http://", "https://") + key + + if NO_SSL: + base_url = request.url_root + else: + base_url = request.url_root.replace("http://", "https://") + link = base_url + key return render_template('confirm.html', password_link=link) -@application.route('/', methods=['GET']) + +@app.route('/', methods=['GET']) def show_password(password_key): password = get_password(password_key) if not password: @@ -67,5 +80,10 @@ def show_password(password_key): return render_template('password.html', password=password) + +def main(): + app.run(host='0.0.0.0', debug=True) + + if __name__ == '__main__': - application.run(host='0.0.0.0', debug=True) + main() diff --git a/static/bootstrap/css/bootstrap-responsive.css b/snappass/static/bootstrap/css/bootstrap-responsive.css similarity index 100% rename from static/bootstrap/css/bootstrap-responsive.css rename to snappass/static/bootstrap/css/bootstrap-responsive.css diff --git a/static/bootstrap/css/bootstrap-responsive.min.css b/snappass/static/bootstrap/css/bootstrap-responsive.min.css similarity index 100% rename from static/bootstrap/css/bootstrap-responsive.min.css rename to snappass/static/bootstrap/css/bootstrap-responsive.min.css diff --git a/static/bootstrap/css/bootstrap.css b/snappass/static/bootstrap/css/bootstrap.css similarity index 100% rename from static/bootstrap/css/bootstrap.css rename to snappass/static/bootstrap/css/bootstrap.css diff --git a/static/bootstrap/css/bootstrap.min.css b/snappass/static/bootstrap/css/bootstrap.min.css similarity index 100% rename from static/bootstrap/css/bootstrap.min.css rename to snappass/static/bootstrap/css/bootstrap.min.css diff --git a/static/bootstrap/img/glyphicons-halflings-white.png b/snappass/static/bootstrap/img/glyphicons-halflings-white.png similarity index 100% rename from static/bootstrap/img/glyphicons-halflings-white.png rename to snappass/static/bootstrap/img/glyphicons-halflings-white.png diff --git a/static/bootstrap/img/glyphicons-halflings.png b/snappass/static/bootstrap/img/glyphicons-halflings.png similarity index 100% rename from static/bootstrap/img/glyphicons-halflings.png rename to snappass/static/bootstrap/img/glyphicons-halflings.png diff --git a/static/bootstrap/js/bootstrap.js b/snappass/static/bootstrap/js/bootstrap.js similarity index 100% rename from static/bootstrap/js/bootstrap.js rename to snappass/static/bootstrap/js/bootstrap.js diff --git a/static/bootstrap/js/bootstrap.min.js b/snappass/static/bootstrap/js/bootstrap.min.js similarity index 100% rename from static/bootstrap/js/bootstrap.min.js rename to snappass/static/bootstrap/js/bootstrap.min.js diff --git a/templates/base.html b/snappass/templates/base.html similarity index 100% rename from templates/base.html rename to snappass/templates/base.html diff --git a/templates/confirm.html b/snappass/templates/confirm.html similarity index 100% rename from templates/confirm.html rename to snappass/templates/confirm.html diff --git a/templates/password.html b/snappass/templates/password.html similarity index 100% rename from templates/password.html rename to snappass/templates/password.html diff --git a/templates/set_password.html b/snappass/templates/set_password.html similarity index 100% rename from templates/set_password.html rename to snappass/templates/set_password.html diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..b6fc65d --- /dev/null +++ b/tests.py @@ -0,0 +1,62 @@ +import unittest +from unittest import TestCase + +from werkzeug.exceptions import ClientDisconnected + +#noinspection PyPep8Naming +import snappass.main as snappass + +__author__ = 'davedash' + + +class SnapPassTestCase(TestCase): + + def test_set_password(self): + """Ensure we return a 32-bit key.""" + key = snappass.set_password("foo", 30) + self.assertEqual(32, len(key)) + + def test_get_password(self): + password = "melatonin overdose 1337!$" + key = snappass.set_password(password, 30) + self.assertEqual(password, snappass.get_password(key)) + # Assert that we can't look this up a second time. + self.assertEqual(None, snappass.get_password(key)) + + def test_clean_input(self): + # Test Bad Data + with snappass.app.test_request_context( + "/", data={'password': 'foo', 'ttl': 'bar'}, method='POST'): + self.assertRaises(ClientDisconnected, snappass.clean_input) + + # No Password + with snappass.app.test_request_context( + "/", method='POST'): + self.assertRaises(ClientDisconnected, snappass.clean_input) + + # No TTL + with snappass.app.test_request_context( + "/", data={'password': 'foo'}, method='POST'): + self.assertRaises(ClientDisconnected, snappass.clean_input) + + with snappass.app.test_request_context( + "/", data={'password': 'foo', 'ttl': 'hour'}, method='POST'): + self.assertEqual((3600, 'foo'), snappass.clean_input()) + + +class SnapPassRoutesTestCase(TestCase): + #noinspection PyPep8Naming + def setUp(self): + snappass.app.config['TESTING'] = True + self.app = snappass.app.test_client() + + def test_show_password(self): + password = "I like novelty kitten statues!" + key = snappass.set_password(password, 30) + rv = self.app.get('/{}'.format(key)) + self.assertIn(password, rv.data) + + +if __name__ == '__main__': + unittest.main() + diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..5d83da1 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py27 + +[testenv] +deps = + pytest + pytest-cov +commands = + pip install -r requirements.txt --use-wheel + py.test --junitxml=junit-{envname}.xml --cov-report xml tests.py