├── .gitignore ├── README.md ├── app.yaml ├── appengine_config.py ├── cron.yaml ├── main.py ├── requirements.txt ├── static ├── clippy.svg ├── random-password.js └── robots.txt ├── template ├── about.tpl ├── index.tpl └── p.tpl └── vendor.py /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | TODO.txt 3 | *.pyc 4 | *.bak.20* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Temporal.PW] 2 | 3 | [Temporal.PW] provides temporary secure storage for passwords so you can safely transmit them over insecure channels like E-Mail, Instant Messaging, SMS, etc. 4 | - All encryption / decryption is done in the client browser, the AES key is never sent to the server. 5 | - Passwords are only ever viewable a single time, so you can be certain that only one recipient viewed it. 6 | - You can choose to restrict unique temporary password URLs so that they're only accessible from your same IP address (useful for sending passwords securely to someone in the same network). 7 | 8 | 9 | [Temporal.PW]: 10 | [PyCrypto]: https://www.dlitz.net/software/pycrypto/ 11 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | ## https://developers.google.com/appengine/docs/python/config/appconfig 2 | 3 | # gcloud app logs read --project temporalpw-1 --logs=stderr,stdout,crash.log,request_log 4 | 5 | ## 2016-11-17 : tkooda : removed per move from `appcfg.py -A temporalpw-1 --oauth2 update .` to `gcloud app deploy --project temporalpw-1 *.yaml` 6 | #application: temporalpw-1 7 | #version: 1 8 | runtime: python27 9 | api_version: 1 10 | threadsafe: yes 11 | 12 | handlers: 13 | - url: /cleanup 14 | script: main.bottle 15 | login: admin 16 | 17 | - url: /static 18 | static_dir: static 19 | secure: always 20 | 21 | - url: /robots\.txt 22 | static_files: static/robots.txt 23 | upload: static/robots.txt 24 | secure: always 25 | 26 | # for letsencrypt.org SSL cert: 27 | - url: /.well-known/acme-challenge/* 28 | script: main.bottle 29 | secure: never 30 | 31 | - url: .* 32 | script: main.bottle 33 | secure: always 34 | 35 | -------------------------------------------------------------------------------- /appengine_config.py: -------------------------------------------------------------------------------- 1 | """`appengine_config` gets loaded when starting a new application instance.""" 2 | import vendor 3 | # insert `lib` as a site directory so our `main` module can load 4 | # third-party libraries, and override built-ins with newer 5 | # versions. 6 | vendor.add('lib') 7 | -------------------------------------------------------------------------------- /cron.yaml: -------------------------------------------------------------------------------- 1 | cron: 2 | - description: hourly purge of any expired password entries 3 | url: /cleanup 4 | schedule: every 1 hours 5 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ## temporal.pw - 2016-02-14 - Thor J. Kooda 4 | 5 | ## 2016-03-26 : rewrote. move password encryption to client side, server is never sent any part of the key (browser does all encryption/decryption) 6 | 7 | """Provide temporary storage for encrypted passwords so they can be sent via E-Mail.""" 8 | 9 | from bottle import Bottle, request, template, abort, response, redirect 10 | from google.appengine.ext import ndb, webapp 11 | import datetime 12 | import string 13 | import random 14 | import hashlib 15 | import os 16 | 17 | 18 | class Password( ndb.Model ): 19 | """temporary storage for encrypted password""" 20 | ciphertext = ndb.BlobProperty( required = True ) # password only sent to server AFTER being encrypted by browser, key NEVER sent to server 21 | expire_date = ndb.DateTimeProperty( required = True ) 22 | ip_hash = ndb.StringProperty() 23 | 24 | class LetsEncrypt( ndb.Model ): 25 | """LetsEncrypt challenge responses""" 26 | response = ndb.StringProperty( required = True ) 27 | added = ndb.DateTimeProperty( required = True, auto_now_add = True ) 28 | 29 | 30 | bottle = Bottle() 31 | 32 | 33 | # SSL cert from letsencrypt.org 34 | @bottle.get( "/.well-known/acme-challenge/" ) 35 | def letsencrypt( challenge ): 36 | # LetsEncrypt( id = challenge, response = "test response" ).put() 37 | le = LetsEncrypt.get_by_id( challenge ) 38 | if le: 39 | return le.response 40 | return abort( 404 ) 41 | 42 | 43 | @bottle.get( "/" ) 44 | def index(): 45 | return template( "template/index", { "ip": os.environ[ "REMOTE_ADDR" ] } ) 46 | 47 | 48 | @bottle.get( "/p" ) 49 | def p(): 50 | return template( "template/p" ) 51 | 52 | 53 | @bottle.get( "/p" ) 54 | def p_redir( redir ): 55 | redirect( "/p" + redir ) # redirect to catch any user mangling of URL ("/p%23...") 56 | 57 | 58 | @bottle.get( "/about" ) 59 | def about(): 60 | return template( "template/about" ) 61 | 62 | 63 | @bottle.post( "/new" ) 64 | def new(): 65 | cipher = request.POST.get( "cipher" ).strip() 66 | days = int( request.POST.get( "days" ).strip() ) 67 | myiponly = request.POST.get( "myiponly" ) 68 | 69 | if len( cipher ) < 1 or len( cipher ) > 8192 \ 70 | or days < 1 or days > 30: 71 | abort( 400, "invalid options" ) 72 | 73 | ## generate unique pw_id .. 74 | pw_id = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(4)) 75 | while Password.get_by_id( pw_id ): 76 | pw_id = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(4)) 77 | 78 | expire = datetime.datetime.now() + datetime.timedelta( days = days ) 79 | 80 | password = Password( id = pw_id, 81 | ciphertext = cipher, 82 | expire_date = expire ) 83 | 84 | if myiponly == "true": 85 | ## only store a salted hash of the IP, for privacy 86 | ip_salt = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(4)) 87 | password.ip_hash = ip_salt + ":" + hashlib.sha1( ip_salt + os.environ[ "REMOTE_ADDR" ] ).hexdigest() 88 | 89 | password.put() 90 | 91 | response.add_header( "Access-Control-Allow-Origin", "*" ) # for auditable version + development 92 | 93 | return { "pw_id": pw_id } 94 | 95 | 96 | @bottle.get( "/get/" ) 97 | def get( pw_id ): 98 | password = Password.get_by_id( pw_id ) 99 | 100 | if not password: 101 | return abort( 404 ) 102 | 103 | if password.expire_date < datetime.datetime.now(): # delete immediately if expired 104 | password.key.delete() 105 | return abort( 404 ) 106 | 107 | if password.ip_hash: 108 | ## only allow viewing from matching IP if myiponly is set.. 109 | ip_salt, ip_hash = password.ip_hash.split( ":", 1 ) 110 | if not ip_hash == hashlib.sha1( ip_salt + os.environ[ "REMOTE_ADDR" ] ).hexdigest(): 111 | return abort( 404 ) 112 | 113 | cipher = password.ciphertext 114 | 115 | password.key.delete() # only return encrypted password for a SINGLE viewing 116 | 117 | response.add_header( "Access-Control-Allow-Origin", "https://tkooda.github.io" ) # for auditable version 118 | 119 | return { "cipher": cipher } 120 | 121 | 122 | @bottle.get( "/cleanup" ) # cron 123 | def cleanup(): 124 | keys = Password.query( Password.expire_date < datetime.datetime.now() ).fetch( keys_only = True ) 125 | ndb.delete_multi( keys ) 126 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This requirements file lists all dependecies for this project. 2 | # 3 | # Run 'pip install -r requirements.txt -t lib/' to install these dependencies 4 | # in this project's lib/ directory. The `lib` directory is added to `sys.path` 5 | # by `appengine_config.py`. 6 | bottle==0.12.7 7 | -------------------------------------------------------------------------------- /static/clippy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/random-password.js: -------------------------------------------------------------------------------- 1 | // attempt to generate cryptographically secure random byte, with optional max for using as a charset index 2 | function getRandomByte( max = 256 ) { 3 | // http://caniuse.com/#feat=getrandomvalues 4 | var crypto = window.crypto || window.msCrypto; 5 | if ( crypto && crypto.getRandomValues ) { 6 | var a = new Uint8Array( 1 ); 7 | while ( true ) { 8 | crypto.getRandomValues( a ); 9 | if ( a[0] <= max ) return a[0]; 10 | } 11 | } else { 12 | return Math.floor( Math.random() * max ); 13 | } 14 | }; 15 | 16 | function generatePassword( minLength = 20, maxLength = 30, charset = "abcdefghijknopqrstuvwxyzACDEFGHJKLMNPQRSTUVWXYZ2345679" ) { 17 | if ( minLength > maxLength) maxLength = minLength; 18 | var randomLength = Math.floor( Math.random() * ( maxLength - minLength ) ) + minLength; 19 | var password = ""; 20 | for ( var i = 0, maxIndex = charset.length; i < randomLength; i++ ) { 21 | password += charset.charAt( getRandomByte( maxIndex ) ); 22 | } 23 | return password; 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /new 3 | Disallow: /p/ 4 | Disallow: /d/ 5 | Disallow: /.well-known/acme-challenge/ 6 | -------------------------------------------------------------------------------- /template/about.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Temporal.PW - Temporary secure storage for passwords 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 |
15 | 16 |

About Temporal.PW:


17 |

18 | Temporal.PW can convert a password into a unique temporary secure URL that is safe to send via E-Mail.
19 |
20 | I made this because I needed a simple way to send passwords to people with whom I didn't already have end-to-end encryption established.
21 |

22 |
23 | 24 |

How to use it:


25 |

26 |
    27 |
  1. Enter a good, long, random password into temporal.pw, or click "Generate a random password".

  2. 28 |
  3. E-Mail the temporary URL to someone:

  4. 29 |
      30 |
    • If they're able to view the password: you know that nobody else was able to view it, and you know that that URL can't be used again later to determine the password.

    • 31 |
    • If they see a "This password doesn't exist" error: that password was likely viewed by someone else, the recipient needs to tell you to generate a new password and send them a new temporal.pw URL.

    • 32 |
    • If they see an "Invalid password URL" error: the URL they're attempting to view isn't the exact URL that you generated for them.

    • 33 |
    34 |
35 |

36 | 37 |
38 |
39 | 40 |

How it works:


41 |

42 |
    43 |
  • The unencrypted password is NEVER sent anywhere.

  • 44 |
  • The password encryption key is generated by your browser and is NEVER sent anywhere.

  • 45 |
  • The browser generates a random 128 bit AES encryption/decryption key, encrypts the password with it, sends (only) the encrypted version of the password to the server (so that the URL can be rendered useless after viewing the password once, and the URL doesn't expose the password to brute force attack), and then the browser builds a unique temporary secure URL that contains the ID of the encrypted password plus the decryption key.

  • 46 |
  • The password encryption/decryption key only exists in the 'fragment' part of the URL (after the '#' hash symbol) and is never sent to the server.

  • 47 |
  • The encrypted password is only sent to the server so that the unique temporary secure URL can be rendered useless after it has been viewed once or it has expired. (the encrypted password is sent instead of the key so that the password can't be brute forced out of the URL, and so that the URL is a fixed short length independent of the password size)

  • 48 |
  • The encrypted password cannot be decrypted without the decryption key thats in the unique temporary secure URL.

  • 49 |
    50 |
  • All key generation, encryption, and decryption is 100% done in the browser using common public cryptographic libraries (AES-JS).

  • 51 |
  • The encrypted password is deleted from the server after it expires, or immediately after being viewed a single time by someone who has the unique temporary secure URL.

  • 52 |
  • Each password is only viewable a single time so that the intended recipient will NOT be able to view the password if someone else has intercepted and viewed it first.

  • 53 |
  • Optionally, you can choose to tell the server to only allow a password to be viewed from your same IP address (useful for sending passwords to someone in the same office / network).

  • 54 |
55 |

56 | 57 |
58 |
59 | 60 |

Other Information:


61 |

62 | 69 |

70 | 71 |
72 | 73 |
74 | Send another password | Source
75 |
76 | 77 |
78 |
79 | 80 |
81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /template/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Temporal.PW - Temporary secure storage for passwords 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 118 | 119 |
120 | 121 |
122 |
123 |

E-Mail passwords securely with Temporal.PW

124 |
125 |
126 | 127 |
128 |
129 | 130 | 131 |
132 |
133 | 134 | 135 | 138 | 139 |
140 |
or:                
141 |
142 | 143 |
144 |
145 |
146 |
147 |
148 | 149 |

150 | 151 |
152 | Make this URL expire in days. 167 |
168 | 169 |
170 | 172 |
173 | 174 |
175 | 176 |
177 |

178 | 179 | 180 |
181 | 182 |
183 | 184 |
185 | 186 | (Do not include any information that identifies what the password is for)
187 |
188 |
189 | 190 | Send another password | About | Source
191 | 192 |
193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /template/p.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Temporal.PW - Temporary secure storage for passwords 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 72 | 73 |
74 |
75 |
76 |
77 | 78 |

This password doesn't exist

79 | 80 |
81 |

82 |
83 | 84 | 85 | 86 | 87 |
88 |
89 | 90 |

91 | 92 |
93 | 94 | Send another password | About | Source
95 | 96 |
97 | 98 |
99 | 100 | 101 | -------------------------------------------------------------------------------- /vendor.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2014 Jon Wayne Parrott, [proppy], Michael R. Bernstein 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Notes: 18 | # - Imported from https://github.com/jonparrott/Darth-Vendor/. 19 | # - Added license header. 20 | # - Renamed `darth.vendor` to `vendor.add` to match upcoming SDK interface. 21 | # - Renamed `position` param to `index` to match upcoming SDK interface. 22 | # - Removed funny arworks docstring. 23 | 24 | import site 25 | import os.path 26 | import sys 27 | 28 | 29 | def add(folder, index=1): 30 | """ 31 | Adds the given folder to the python path. Supports namespaced packages. 32 | By default, packages in the given folder take precedence over site-packages 33 | and any previous path manipulations. 34 | 35 | Args: 36 | folder: Path to the folder containing packages, relative to ``os.getcwd()`` 37 | position: Where in ``sys.path`` to insert the vendor packages. By default 38 | this is set to 1. It is inadvisable to set it to 0 as it will override 39 | any modules in the current working directory. 40 | """ 41 | 42 | # Check if the path contains a virtualenv. 43 | site_dir = os.path.join(folder, 'lib', 'python' + sys.version[:3], 'site-packages') 44 | if os.path.exists(site_dir): 45 | folder = site_dir 46 | # Otherwise it's just a normal path, make it absolute. 47 | else: 48 | folder = os.path.join(os.path.dirname(__file__), folder) 49 | 50 | # Use site.addsitedir() because it appropriately reads .pth 51 | # files for namespaced packages. Unfortunately, there's not an 52 | # option to choose where addsitedir() puts its paths in sys.path 53 | # so we have to do a little bit of magic to make it play along. 54 | 55 | # We're going to grab the current sys.path and split it up into 56 | # the first entry and then the rest. Essentially turning 57 | # ['.', '/site-packages/x', 'site-packages/y'] 58 | # into 59 | # ['.'] and ['/site-packages/x', 'site-packages/y'] 60 | # The reason for this is we want '.' to remain at the top of the 61 | # list but we want our vendor files to override everything else. 62 | sys.path, remainder = sys.path[:1], sys.path[1:] 63 | 64 | # Now we call addsitedir which will append our vendor directories 65 | # to sys.path (which was truncated by the last step.) 66 | site.addsitedir(folder) 67 | 68 | # Finally, we'll add the paths we removed back. 69 | # The final product is something like this: 70 | # ['.', '/vendor-folder', /site-packages/x', 'site-packages/y'] 71 | sys.path.extend(remainder) 72 | --------------------------------------------------------------------------------