#!/usr/bin/python -tt
##
# Copyright (C) 2012 by Konstantin Ryabitsev and contributors
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.
#
import os
import sys
import cgi
import syslog
import logging
import cgitb
cgitb.enable(display=0, logdir="/tmp")
import totpcgi
import totpcgi.backends
import totpcgi.utils
import qrcode
from qrcode.image import svg
from StringIO import StringIO
from string import Template
if len(sys.argv) > 1:
# blindly assume it's the config file
config_file = sys.argv[1]
else:
config_file = '/etc/totpcgi/provisioning.conf'
import ConfigParser
config = ConfigParser.RawConfigParser()
config.read(config_file)
backends = totpcgi.backends.Backends()
try:
backends.load_from_config(config)
except totpcgi.backends.BackendNotSupported, ex:
syslog.syslog(syslog.LOG_CRIT,
'Backend engine not supported: %s' % ex)
sys.exit(1)
syslog.openlog('provisioning.cgi', syslog.LOG_PID, syslog.LOG_AUTH)
def bad_request(config, why):
templates_dir = config.get('secret', 'templates_dir')
fh = open(os.path.join(templates_dir, 'error.html'))
tpt = Template(fh.read())
fh.close()
vals = {
'action_url': config.get('secret', 'action_url'),
'css_root': config.get('secret', 'css_root'),
'errormsg': cgi.escape(why)
}
out = tpt.safe_substitute(vals)
sys.stdout.write('Status: 400 BAD REQUEST\n')
sys.stdout.write('Content-type: text/html\n')
sys.stdout.write('Content-Length: %s\n' % len(out))
sys.stdout.write('\n')
sys.stdout.write(out)
sys.exit(0)
def show_qr_code(data):
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=5,
border=4)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image()
fh = StringIO()
img.save(fh)
out = fh.getvalue()
fh.close()
sys.stdout.write('Status: 200 OK\n')
sys.stdout.write('Content-type: image/png\n')
sys.stdout.write('Content-Length: %s\n' % len(out))
sys.stdout.write('\n')
sys.stdout.write(out)
sys.exit(0)
def show_login_form(config):
templates_dir = config.get('secret', 'templates_dir')
fh = open(os.path.join(templates_dir, 'login.html'))
tpt = Template(fh.read())
fh.close()
vals = {
'action_url': config.get('secret', 'action_url'),
'css_root': config.get('secret', 'css_root')
}
out = tpt.safe_substitute(vals)
sys.stdout.write('Status: 200 OK\n')
sys.stdout.write('Content-type: text/html\n')
sys.stdout.write('Content-Length: %s\n' % len(out))
sys.stdout.write('\n')
sys.stdout.write(out)
sys.exit(0)
def show_totp_page(config, user, gaus):
# generate provisioning URI
tpt = Template(config.get('secret', 'totp_user_mask'))
totp_user = tpt.safe_substitute(username=user)
totp_qr_uri = gaus.totp.provisioning_uri(totp_user)
action_url = config.get('secret', 'action_url')
qrcode_embed = '
' % (action_url, totp_qr_uri)
templates_dir = config.get('secret', 'templates_dir')
fh = open(os.path.join(templates_dir, 'totp.html'))
tpt = Template(fh.read())
fh.close()
if gaus.scratch_tokens:
scratch_tokens = '
'.join(gaus.scratch_tokens)
else:
scratch_tokens = ' '
vals = {
'action_url': action_url,
'css_root': config.get('secret', 'css_root'),
'qrcode_embed': qrcode_embed,
'scratch_tokens': scratch_tokens
}
out = tpt.safe_substitute(vals)
sys.stdout.write('Status: 200 OK\n')
sys.stdout.write('Content-type: text/html\n')
sys.stdout.write('Content-Length: %s\n' % len(out))
sys.stdout.write('\n')
sys.stdout.write(out)
sys.exit(0)
def generate_secret(config):
encrypt_secret = config.getboolean('secret', 'encrypt_secret')
window_size = config.getint('secret', 'window_size')
rate_limit = config.get('secret', 'rate_limit')
# scratch tokens don't make any sense with encrypted secret
if not encrypt_secret:
scratch_tokens_n = config.getint('secret', 'scratch_tokens_n')
else:
scratch_tokens_n = 0
(times, secs) = rate_limit.split(',')
rate_limit = (int(times), int(secs))
gaus = totpcgi.utils.generate_secret(rate_limit, window_size,
scratch_tokens_n)
return gaus
def cgimain():
form = cgi.FieldStorage()
if 'qrcode' in form:
#if os.environ['HTTP_REFERER'].find(os.environ['SERVER_NAME']) == -1:
# bad_request(config, 'Sorry, you failed the HTTP_REFERER check')
qrcode = form.getfirst('qrcode')
show_qr_code(qrcode)
remote_host = os.environ['REMOTE_ADDR']
try:
trust_http_auth = config.getboolean('secret', 'trust_http_auth')
except ConfigParser.NoOptionError:
trust_http_auth = False
if trust_http_auth and os.environ.has_key('REMOTE_USER'):
user = os.environ['REMOTE_USER']
pincode = None
syslog.syslog(syslog.LOG_NOTICE,
'Success (http-auth): user=%s, host=%s' % (user, remote_host))
else:
must_keys = ('username', 'pincode')
for must_key in must_keys:
if must_key not in form:
show_login_form(config)
user = form.getfirst('username')
pincode = form.getfirst('pincode')
# start by verifying the pincode
try:
backends.pincode_backend.verify_user_pincode(user, pincode)
except Exception, ex:
syslog.syslog(syslog.LOG_NOTICE,
'Failure: user=%s, host=%s, message=%s' % (user, remote_host,
str(ex)))
bad_request(config, str(ex))
# pincode verified
syslog.syslog(syslog.LOG_NOTICE,
'Success: user=%s, host=%s' % (user, remote_host))
# is there an existing secret for this user?
exists = True
try:
backends.secret_backend.get_user_secret(user, pincode)
except totpcgi.UserNotFound:
# if we got it, then there isn't an existing secret in place
exists = False
if exists:
syslog.syslog(syslog.LOG_NOTICE,
'Secret exists: user=%s, host=%s' % (user, remote_host))
bad_request(config, 'Existing secret found. It must be removed first.')
# now generate the secret and store it
gaus = generate_secret(config)
# if we don't need to encrypt the secret, set pincode to None
encrypt_secret = config.getboolean('secret', 'encrypt_secret')
if not encrypt_secret:
pincode = None
backends.secret_backend.save_user_secret(user, gaus, pincode)
# purge all old state, as it's now obsolete
backends.state_backend.delete_user_state(user)
show_totp_page(config, user, gaus)
if __name__ == '__main__':
cgimain()