auth2_oath: comletely remove this module, as it does not depend entirely on Entr'ouvert copyright
We will recreate it as an external plugin.
This commit is contained in:
parent
b67842207b
commit
493c89eb6b
2
COPYING
2
COPYING
|
@ -663,7 +663,5 @@ if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
External modules oath and totp-js modules are licensed under a BSD-like licence.
|
||||
|
||||
OpenID idp module is derived of the project django_openid_provider which is
|
||||
distributed under the Apache 2.0 license.
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import logging
|
||||
|
||||
from django.db import transaction
|
||||
from django.conf import settings
|
||||
|
||||
from authentic2.compat import get_user_model
|
||||
import authentic2.vendor.oath.hotp as hotp
|
||||
from authentic2.nonce import accept_nonce
|
||||
import models
|
||||
logger = logging.getLogger('authentic.auth.auth2_oath')
|
||||
|
||||
NONCE_TIMEOUT = getattr(settings, 'OATH_NONCE_TIMEOUT',
|
||||
getattr(settings, 'NONCE_TIMEOUT', 3600))
|
||||
|
||||
class OATHTOTPBackend:
|
||||
supports_object_permissions = False
|
||||
supports_anonymous_user = False
|
||||
|
||||
@transaction.commit_on_success()
|
||||
def authenticate(self, username, oath_otp, format='dec6'):
|
||||
'''Lookup the TOTP or HOTP secret for the user and try to authenticate
|
||||
the proposed OTP using it.
|
||||
'''
|
||||
User = get_user_model()
|
||||
try:
|
||||
secret = models.OATHTOTPSecret.objects.get(user__username=username)
|
||||
except models.OATHTOTPSecret.DoesNotExist:
|
||||
return None
|
||||
try:
|
||||
accepted, drift = hotp.accept_totp(secret.key, oath_otp, format=format)
|
||||
except Exception, e:
|
||||
logger.exception('hotp.accept_totp raised', e)
|
||||
raise
|
||||
|
||||
if accepted:
|
||||
if not accept_nonce(oath_otp, 'OATH_OTP', NONCE_TIMEOUT):
|
||||
logger.error('already used OTP %r', oath_otp)
|
||||
return None
|
||||
|
||||
secret.drift = drift
|
||||
return User.objects.get(username=username)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
"""
|
||||
simply return the user object. That way, we only need top look-up the
|
||||
certificate once, when loggin in
|
||||
"""
|
||||
User = get_user_model()
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
|
||||
def setup_totp(self, user):
|
||||
'''Create a model containing a TOTP secret for the given user and the
|
||||
current time drift which initially is zero
|
||||
'''
|
||||
pass
|
|
@ -1,64 +0,0 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext_noop
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.contrib.auth import login, authenticate
|
||||
from django.http import HttpResponseRedirect
|
||||
import django.forms as forms
|
||||
|
||||
import authentic2.auth2_auth.models as models
|
||||
import views
|
||||
|
||||
# Only difference with login/password form is the user of 'otp' intead of
|
||||
# password as an argument to the authenticate() method
|
||||
# So you need an OTP enabled backend to this to work
|
||||
class OATHOTPHAuthenticationForm(AuthenticationForm):
|
||||
def clean(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
if username and password:
|
||||
self.user_cache = authenticate(username=username, oath_otp=password)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(_("Please enter a correct username and one-time password. Note that both fields are case-sensitive."))
|
||||
elif not self.user_cache.is_active:
|
||||
raise forms.ValidationError(_("This account is inactive."))
|
||||
|
||||
# TODO: determine whether this should move to its own method.
|
||||
if self.request:
|
||||
if not self.request.session.test_cookie_worked():
|
||||
raise forms.ValidationError(_("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in."))
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class OATHOTPFrontend(object):
|
||||
def enabled(self):
|
||||
return True
|
||||
|
||||
def name(self):
|
||||
return gettext_noop('One time password')
|
||||
|
||||
def id(self):
|
||||
return 'oath-otp'
|
||||
|
||||
def form(self):
|
||||
return OATHOTPHAuthenticationForm
|
||||
|
||||
def post(self, request, form, nonce, next):
|
||||
# Login the user
|
||||
login(request, form.get_user())
|
||||
# Keep a trace
|
||||
if 'HTTPS' in request.environ.get('HTTPS','').lower() == 'on':
|
||||
how = 'oath-totp-on-https'
|
||||
else:
|
||||
how = 'oath-totp'
|
||||
if nonce:
|
||||
models.AuthenticationEvent(who=form.get_user().username, how=how,
|
||||
nonce=nonce).save()
|
||||
return HttpResponseRedirect(next)
|
||||
|
||||
def profile(self, request, next=''):
|
||||
return views.totp_profile(request, next)
|
||||
|
||||
def template(self):
|
||||
return 'auth/login_form_oath.html'
|
|
@ -1,120 +0,0 @@
|
|||
# French translation of Authentic
|
||||
# Copyright (C) 2010, 2011 Entr'ouvert
|
||||
# This file is distributed under the same license as the Authentic package.
|
||||
# Frederic Peters <fpeters@entrouvert.com>, 2010.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Authentic\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-07-23 17:44+0200\n"
|
||||
"PO-Revision-Date: 2013-07-23 17:42+0200\n"
|
||||
"Last-Translator: Mikaël Ates <mates@entrouvert.com>\n"
|
||||
"Language-Team: None\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n>1;\n"
|
||||
|
||||
#: frontend.py:22
|
||||
msgid ""
|
||||
"Please enter a correct username and one-time password. Note that both fields "
|
||||
"are case-sensitive."
|
||||
msgstr ""
|
||||
"Veuillez taper un nom d'utilisateur correct et un mot de passe à usage "
|
||||
"unique. Notez que les deux champs sont sensibles à la casse."
|
||||
|
||||
#: frontend.py:24
|
||||
msgid "This account is inactive."
|
||||
msgstr "Ce compte est inactif."
|
||||
|
||||
#: frontend.py:29
|
||||
msgid ""
|
||||
"Your Web browser doesn't appear to have cookies enabled. Cookies are "
|
||||
"required for logging in."
|
||||
msgstr ""
|
||||
"Il semblerait que votre navigateur ne supporte pas les cookies. Les cookies "
|
||||
"sont requis pour se connecter."
|
||||
|
||||
#: frontend.py:39
|
||||
msgid "One time password"
|
||||
msgstr "Mot de passe à usage unique"
|
||||
|
||||
#: templates/auth/login_form_oath.html:5
|
||||
msgid ""
|
||||
"\n"
|
||||
"Once you have created your account, log in with an other authentication "
|
||||
"method.\n"
|
||||
"Then, in account management, follow the instructions to deploy the\n"
|
||||
"One Time password authentication method.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Une fois votre compte créé, connectez-vous à l'aide d'une autre méthode.\n"
|
||||
"Alors, dans l'interface de gestion de votre compte, suivez les instructions "
|
||||
"pour déployer la\n"
|
||||
"méthode d'authentification basée sur le mot de passe à usage unique.\n"
|
||||
|
||||
#: templates/auth/login_form_oath.html:16
|
||||
msgid "Log in"
|
||||
msgstr "S'identifier"
|
||||
|
||||
#: templates/auth/login_form_oath.html:18
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#: templates/auth/login_form_oath.html:24
|
||||
msgid "Forgot password?"
|
||||
msgstr "Mot de passe oublié ?"
|
||||
|
||||
#: templates/auth/login_form_oath.html:24
|
||||
msgid "Reset it!"
|
||||
msgstr "Le réinitialiser !"
|
||||
|
||||
#: templates/auth/login_form_oath.html:25
|
||||
msgid "Not a member?"
|
||||
msgstr "Pas un membre ?"
|
||||
|
||||
#: templates/auth/login_form_oath.html:25
|
||||
msgid "Register!"
|
||||
msgstr "S'inscrire !"
|
||||
|
||||
#: templates/oath/totp_profile.html:5
|
||||
msgid "Time based one-time password"
|
||||
msgstr "Mot de passe à usage unique basé sur le temps"
|
||||
|
||||
#: templates/oath/totp_profile.html:8
|
||||
msgid "Secret"
|
||||
msgstr "Secret"
|
||||
|
||||
#: templates/oath/totp_profile.html:10
|
||||
msgid "Google authenticator"
|
||||
msgstr "Google authenticator"
|
||||
|
||||
#: templates/oath/totp_profile.html:12
|
||||
msgid "Bookmarklet"
|
||||
msgstr "Marque-page générateur de mot de passe"
|
||||
|
||||
#: templates/oath/totp_profile.html:13
|
||||
msgid ""
|
||||
"Copy this link to your bookmarks. When clicking on it it will generate a new "
|
||||
"one-time password which will allow you to login"
|
||||
msgstr ""
|
||||
"Copier ce lien dans vos marque-pages. Cliquez le pour générer un nouveau mot "
|
||||
"de passe à usage unique pour vous connecter."
|
||||
|
||||
#: templates/oath/totp_profile.html:16
|
||||
msgid "Delete this credential"
|
||||
msgstr "Supprimer ce moyen d'authentification"
|
||||
|
||||
#: templates/oath/totp_profile.html:19
|
||||
msgid ""
|
||||
"This kind of authentication is actually not possible, because you do not "
|
||||
"have any secret setup."
|
||||
msgstr ""
|
||||
"Ce moyen d'authentification n'est pas disponible actuellement car vous "
|
||||
"n'avez pas encore généré de secret."
|
||||
|
||||
#: templates/oath/totp_profile.html:23
|
||||
msgid "Generate a new credential"
|
||||
msgstr "Générer un nouveau secret"
|
|
@ -1,39 +0,0 @@
|
|||
# encoding: utf-8
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
|
||||
|
||||
from authentic.compat import user_model_label
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'OATHTOTPSecret'
|
||||
db.create_table('auth2_oath_oathtotpsecret', (
|
||||
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='oath_totp_secret', unique=True, primary_key=True, to=orm[user_model_label])),
|
||||
('key', self.gf('django.db.models.fields.CharField')(max_length=40)),
|
||||
('drift', self.gf('django.db.models.fields.IntegerField')(default=0, max_length=4)),
|
||||
))
|
||||
db.send_create_signal('auth2_oath', ['OATHTOTPSecret'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'OATHTOTPSecret'
|
||||
db.delete_table('auth2_oath_oathtotpsecret')
|
||||
|
||||
|
||||
models = {
|
||||
user_model_label: {
|
||||
'Meta': {'object_name': user_model_label.split('.')[-1]},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
},
|
||||
'auth2_oath.oathtotpsecret': {
|
||||
'Meta': {'object_name': 'OATHTOTPSecret'},
|
||||
'drift': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '4'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oath_totp_secret'", 'unique': 'True', 'primary_key': 'True', 'to': "orm['%s']" % user_model_label})
|
||||
},
|
||||
}
|
||||
|
||||
complete_apps = ['auth2_oath']
|
|
@ -1,9 +0,0 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
class OATHTOTPSecret(models.Model):
|
||||
user = models.OneToOneField(getattr(settings, 'AUTH_USER_MODEL', 'auth.User'),
|
||||
primary_key= True, related_name='oath_totp_secret')
|
||||
# 20 bytes string as hexadecimal
|
||||
key = models.CharField(max_length=40)
|
||||
drift = models.IntegerField(default=0,max_length=4)
|
|
@ -1,28 +0,0 @@
|
|||
{% load i18n %}
|
||||
<div id="login-oath">
|
||||
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Once you have created your account, log in with an other authentication method.
|
||||
Then, in account management, follow the instructions to deploy the
|
||||
One Time password authentication method.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" name="{{ submit_name }}" value="{% trans "Log in" %}"/>
|
||||
{% if cancel %}
|
||||
<input type="submit" name="cancel" value="{% trans 'Cancel' %}"/>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="login-actions">
|
||||
<p>→ {% trans "Forgot password?" %} <a href="{% url 'auth_password_reset' %}">{% trans "Reset it!" %}</a></p>
|
||||
<!--<p>→ {% trans "Not a member?" %} <a href="{% url 'registration_register' %}">{% trans "Register!" %}</a></p>-->
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,32 +0,0 @@
|
|||
{% load i18n %}
|
||||
<script src="{{ STATIC_URL }}jquery/js/jquery.js"></script>
|
||||
<script src="{{ STATIC_URL }}jquery/js/qrcode.js"></script>
|
||||
<script src="{{ STATIC_URL }}jquery/js/jquery.qrcode.js"></script>
|
||||
<h4>{% trans "Time based one-time password" %}</h4>
|
||||
<div>
|
||||
{% if key %}
|
||||
<p>{% trans "Secret" %}: {{ key }}</p>
|
||||
{% if google_authenticator %}
|
||||
<p>{% trans "Google authenticator" %}: <div class="google_authenticator">{{ google_authenticator|safe }}</div></p>
|
||||
{% endif %}
|
||||
<p><a href="{{ bookmarklet|safe }}">{% trans "Bookmarklet" %}</a>
|
||||
<p>{% trans "Copy this link to your bookmarks. When clicking on it it will generate a new one-time password which will allow you to login" %}</p></p>
|
||||
<p><form action="{{ base|safe }}/delete_totp_secret{{ next|safe }}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="submit" class="submit-link" value="{% trans "Delete this credential" %}">
|
||||
</form></p>
|
||||
{% else %}
|
||||
<p>{% trans "This kind of authentication is actually not possible, because you do not have any secret setup." %}</p>
|
||||
{% endif %}
|
||||
<p><form action="{{ base|safe }}/new_totp_secret{{ next|safe }}" class="submit-link" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="{% trans "Generate a new credential" %}">
|
||||
</form></p>
|
||||
</div>
|
||||
<script>
|
||||
$('.google_authenticator').each(function (index, element) {
|
||||
var content = $(element).text();
|
||||
$(element).text("");
|
||||
$(element).qrcode({ "render": "table", "width": 500, "height": 500, "text": content, 'correctLevel': QRErrorCorrectLevel.M, 'typeNumber': 5 });
|
||||
});
|
||||
</script>
|
|
@ -1,6 +0,0 @@
|
|||
from django.conf.urls import patterns
|
||||
import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^new_totp_secret$', views.new_totp_secret),
|
||||
(r'^delete_totp_secret$', views.delete_totp_secret))
|
|
@ -1,58 +0,0 @@
|
|||
import urllib
|
||||
import random
|
||||
import base64
|
||||
|
||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
import models
|
||||
import authentic2.vendor.totp_js.totp_bookmarklet as totp_bookmarklet
|
||||
|
||||
_hexachars = '0123456789abcdef'
|
||||
|
||||
def new_totp_secret(request, next_url='/'):
|
||||
if request.user is None or not hasattr(request.user, '_meta') \
|
||||
or request.method != 'POST':
|
||||
return HttpResponseBadRequest()
|
||||
key = ''.join([random.choice(_hexachars) for x in range(40)])
|
||||
secret, _ = models.OATHTOTPSecret.objects.get_or_create(user=request.user)
|
||||
secret.key = key
|
||||
secret.save()
|
||||
next_url = request.REQUEST.get('next', next_url)
|
||||
return HttpResponseRedirect(next_url)
|
||||
|
||||
def delete_totp_secret(request, next_url='/'):
|
||||
if request.user is None or not hasattr(request.user, '_meta') \
|
||||
or request.method != 'POST':
|
||||
return HttpResponseBadRequest()
|
||||
try:
|
||||
models.OATHTOTPSecret.objects.filter(user=request.user).delete()
|
||||
except models.OATHTOTPSecret.DoesNotExist:
|
||||
pass
|
||||
next_url = request.REQUEST.get('next', next_url)
|
||||
return HttpResponseRedirect(next_url)
|
||||
|
||||
def totp_profile(request, next_url='', template_name='oath/totp_profile.html'):
|
||||
if request.user is None or not hasattr(request.user, '_meta'):
|
||||
return ''
|
||||
if next_url:
|
||||
next_url = '?next=%s' % urllib.quote(next_url)
|
||||
google_authenticator, key, bookmarklet = '', '', ''
|
||||
try:
|
||||
secret = models.OATHTOTPSecret.objects.get(user=request.user)
|
||||
key = secret.key
|
||||
bookmarklet = totp_bookmarklet.otp_doc(secret.key)
|
||||
google_authenticator = 'otpauth://totp/%(user)s@localhost?secret=%(b32_secret)s' % \
|
||||
{ 'user': request.user.username,
|
||||
'domain': request.get_host(),
|
||||
'b32_secret': base64.b32encode(key.decode('hex')) }
|
||||
except models.OATHTOTPSecret.DoesNotExist:
|
||||
pass
|
||||
return render_to_string(template_name,
|
||||
{ 'key': key,
|
||||
'bookmarklet': bookmarklet,
|
||||
'google_authenticator': google_authenticator,
|
||||
'next': next_url,
|
||||
'base': '/oath'},
|
||||
RequestContext(request))
|
|
@ -260,9 +260,6 @@ def build_assertion(request, login, nid_format='transient', attributes=None):
|
|||
elif backend == \
|
||||
'authentic2.authsaml2.backends.AuthSAML2TransientBackend':
|
||||
authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED
|
||||
elif backend == \
|
||||
'authentic2.auth2_auth.auth2_oath.backend.OATHTOTPBackend':
|
||||
authn_context = lasso.SAML2_AUTHN_CONTEXT_TIME_SYNC_TOKEN
|
||||
else:
|
||||
backend = load_backend(backend)
|
||||
if hasattr(backend, 'get_saml2_authn_context'):
|
||||
|
|
|
@ -317,7 +317,6 @@ ADMIN_TOOLS_MENU = 'authentic2.menu.CustomMenu'
|
|||
AUTH_SAML2 = 'AUTH_SAML2' in os.environ
|
||||
AUTH_OPENID = 'AUTH_OPENID' in os.environ
|
||||
AUTH_SSL = 'AUTH_SSL' in os.environ
|
||||
AUTH_OATH = 'AUTH_OATH' in os.environ
|
||||
IDP_SAML2 = 'IDP_SAML2' in os.environ
|
||||
IDP_OPENID = 'IDP_OPENID' in os.environ
|
||||
IDP_CAS = 'IDP_CAS' in os.environ
|
||||
|
@ -354,11 +353,6 @@ if AUTH_SSL:
|
|||
AUTH_FRONTENDS += ('authentic2.auth2_auth.auth2_ssl.frontend.SSLFrontend',)
|
||||
INSTALLED_APPS += ('authentic2.auth2_auth.auth2_ssl',)
|
||||
|
||||
if AUTH_OATH:
|
||||
INSTALLED_APPS += ('authentic2.auth2_auth.auth2_oath',)
|
||||
AUTHENTICATION_BACKENDS += ('authentic2.auth2_auth.auth2_oath.backend.OATHTOTPBackend',)
|
||||
AUTH_FRONTENDS += ('authentic2.auth2_auth.auth2_oath.frontend.OATHOTPFrontend',)
|
||||
|
||||
if IDP_SAML2:
|
||||
IDP_BACKENDS += ('authentic2.idp.saml.backend.SamlBackend',)
|
||||
|
||||
|
|
|
@ -41,10 +41,6 @@ if getattr(settings, 'IDP_OPENID', False):
|
|||
urlpatterns += patterns('',
|
||||
(r'^openid/', include('authentic2.idp.idp_openid.urls')))
|
||||
|
||||
if 'authentic2.auth2_auth.auth2_oath' in settings.INSTALLED_APPS:
|
||||
urlpatterns += patterns('',
|
||||
(r'^oath/', include('authentic2.auth2_auth.auth2_oath.urls')))
|
||||
|
||||
try:
|
||||
if settings.DISCO_SERVICE:
|
||||
urlpatterns += patterns('',
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
- implement accept_hotp
|
||||
- add truncation functions for hashing algorithm with a larger output like SHA2
|
||||
variant.
|
|
@ -1,133 +0,0 @@
|
|||
import hashlib
|
||||
import hmac
|
||||
import binascii
|
||||
import time
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
'''
|
||||
Python implementation of HOTP and TOTP algorithms from the OATH project.
|
||||
|
||||
Copyright 2010, Benjamin Dauvergne
|
||||
|
||||
* All rights reserved.
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.'''
|
||||
|
||||
def __truncated_value(h):
|
||||
bytes = map(ord, h)
|
||||
offset = bytes[19] & 0xf
|
||||
v = (bytes[offset] & 0x7f) << 24 | (bytes[offset+1] & 0xff) << 16 | \
|
||||
(bytes[offset+2] & 0xff) << 8 | (bytes[offset+3] & 0xff)
|
||||
return v
|
||||
|
||||
def dec(h,p):
|
||||
v = str(__truncated_value(h))
|
||||
return v[len(v)-p:]
|
||||
|
||||
def __hotp(key, counter, hash=hashlib.sha1):
|
||||
hex_counter = hex(long(counter))[2:-1]
|
||||
hex_counter = '0' * (16 - len(hex_counter)) + hex_counter
|
||||
bin_counter = binascii.unhexlify(hex_counter)
|
||||
bin_key = binascii.unhexlify(key)
|
||||
|
||||
return hmac.new(bin_key, bin_counter, hash).digest()
|
||||
|
||||
def hotp(key,counter,format='dec6',hash=hashlib.sha1):
|
||||
'''Compute a HOTP value as prescribed by RFC4226
|
||||
|
||||
See http://tools.ietf.org/html/rfc4226
|
||||
'''
|
||||
bin_hotp = __hotp(key, counter, hash)
|
||||
|
||||
if format == 'hex40':
|
||||
return binascii.hexlify(bin_hotp[0:5])
|
||||
elif format == 'dec6':
|
||||
return dec(bin_hotp, 6)
|
||||
elif format == 'dec7':
|
||||
return dec(bin_hotp, 7)
|
||||
elif format == 'dec8':
|
||||
return dec(bin_hotp, 8)
|
||||
else:
|
||||
raise ValueError('unknown format')
|
||||
|
||||
def totp(key, format='dec8', period=30, t=None, hash=hashlib.sha1):
|
||||
'''Compute a TOTP value as prescribed by OATH specifications.
|
||||
|
||||
See http://tools.ietf.org/html/draft-mraihi-totp-timebased-06
|
||||
'''
|
||||
if t is None:
|
||||
t = time.time()
|
||||
else:
|
||||
if isinstance(t, datetime.datetime):
|
||||
t = calendar.timegm(t.utctimetuple())
|
||||
else:
|
||||
t = int(t)
|
||||
T = int(t/period)
|
||||
return hotp(key, T, format=format, hash=hash)
|
||||
|
||||
def accept_totp(key, response, period=30, format='dec8', hash=hashlib.sha1,
|
||||
forward_drift=1, backward_drift=1, drift=0, t=None):
|
||||
'''Validate a TOTP value inside a window of
|
||||
[drift-bacward_drift:drift+forward_drift] of time steps.
|
||||
Where drift is the drift obtained during the last call to accept_totp.
|
||||
|
||||
Return a pair (v,d) where v is a boolean giving the result, and d the
|
||||
needed drift to validate the value. The drift value should be saved for
|
||||
user with later call to accept_totp in order to accept a slowly
|
||||
accumulating drift with a token clock.
|
||||
'''
|
||||
t = t or time.time()
|
||||
for i in range(-backward_drift,forward_drift+1):
|
||||
d = (drift+i) * period
|
||||
if totp(key, format=format, period=period, hash=hash, t=t+d) == response:
|
||||
return True, drift+i
|
||||
return False, 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Test vectors extracted from RFC 4226
|
||||
secret = '3132333435363738393031323334353637383930'
|
||||
tvector = [
|
||||
(0, 'cc93cf18508d94934c64b65d8ba7667fb7cde4b0'),
|
||||
(1, '75a48a19d4cbe100644e8ac1397eea747a2d33ab'),
|
||||
(2, '0bacb7fa082fef30782211938bc1c5e70416ff44'),
|
||||
(3, '66c28227d03a2d5529262ff016a1e6ef76557ece'),
|
||||
(4, 'a904c900a64b35909874b33e61c5938a8e15ed1c'),
|
||||
(5, 'a37e783d7b7233c083d4f62926c7a25f238d0316'),
|
||||
(6, 'bc9cd28561042c83f219324d3c607256c03272ae'),
|
||||
(7, 'a4fb960c0bc06e1eabb804e5b397cdc4b45596fa'),
|
||||
(8, '1b3c89f65e6c9e883012052823443f048b4332db'),
|
||||
(9, '1637409809a679dc698207310c8c7fc07290d9e5'), ]
|
||||
for counter, value in tvector:
|
||||
assert(binascii.hexlify(__hotp(secret, counter)) == value)
|
||||
tvector2 = [
|
||||
(0, '4c93cf18', '1284755224', '755224',),
|
||||
(1, '41397eea', '1094287082', '287082',),
|
||||
(2, '82fef30', '137359152', '359152',),
|
||||
(3, '66ef7655', '1726969429', '969429',),
|
||||
(4, '61c5938a', '1640338314', '338314',),
|
||||
(5, '33c083d4', '868254676', '254676',),
|
||||
(6, '7256c032', '1918287922', '287922',),
|
||||
(7, '4e5b397', '82162583', '162583',),
|
||||
(8, '2823443f', '673399871', '399871',),
|
||||
(9, '2679dc69', '645520489', '520489',),]
|
||||
for counter, hexa, deci, trunc in tvector2:
|
||||
h = __hotp(secret, counter)
|
||||
v = __truncated_value(h)
|
||||
assert(hex(v)[2:] == hexa)
|
||||
assert(str(v) == deci)
|
||||
assert(dec(h,6) == trunc)
|
||||
secret = binascii.hexlify('12345678901234567890')
|
||||
tvector3 = [
|
||||
(59, hashlib.sha1, '94287082'),
|
||||
(1111111109, hashlib.sha1, '07081804') ]
|
||||
for timestamp, hash, value in tvector3:
|
||||
assert (totp(secret,t=datetime.datetime.utcfromtimestamp(timestamp),hash=hash) == value)
|
||||
assert(accept_totp(secret, '94287082', t=65) == (True, -1))
|
||||
assert(accept_totp(secret, '94287082', t=65, drift=-1) == (True, -1))
|
|
@ -1,8 +0,0 @@
|
|||
Simple data document generator containing a TOTP soft token
|
||||
===========================================================
|
||||
|
||||
To use it from your python application just do:
|
||||
|
||||
import totp_bookmarklet
|
||||
|
||||
html_fragment = '<a href="%s">OTP Bookmarklet</a>' % totp_bookmarklet.otp_doc('my_secret')
|
File diff suppressed because one or more lines are too long
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
A simple Javascript HOTP implementation (HMAC-Based One-Time Password Algorithm) as described in RFC 4226.
|
||||
|
||||
The library is relying on crypto-js (http://code.google.com/p/crypto-js/) for the javascript HMAC-SHA1 implementation.
|
||||
|
||||
The library can be used to create software token (don't forget to protect the key of the token...).
|
||||
|
||||
If you want to use the library, you'll need to load the crypto-js (sha1 and hmac) and hotp.js.
|
||||
|
||||
Calling the library is easy, you just have to set the hex key of the token, the counter plus the output format.
|
||||
|
||||
otp = hotp("3132333435363738393031323334353637383930","4","dec6");
|
||||
|
||||
Current output formats are : hex40 (format used by ootp, a free software library) and dec6 (the 6 decimal digit as described in the RFC 4226).
|
||||
|
||||
A demo page with the test vector of the RFC 4226 : http://www.foo.be/hotp/example.html*
|
||||
|
||||
http://www.gitorious.org/hotp-js/
|
||||
|
||||
Copyright (C) 2009 Alexandre Dulaunoy
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
function hotp(key, counter, format) {
|
||||
|
||||
function hotp_hexkeytobytestream(s) {
|
||||
// s is the key to be converted in bytes
|
||||
var b = new Array();
|
||||
var last = s.length;
|
||||
for (var i = 0; i < last; i = i + 2) {
|
||||
var x = s[i] + s[i + 1];
|
||||
x.toUpperCase();
|
||||
x = "0x" + x;
|
||||
x = parseInt(x);
|
||||
b[i] = String.fromCharCode(x);
|
||||
}
|
||||
var ret = new String();
|
||||
ret = b.join('');
|
||||
return ret;
|
||||
|
||||
}
|
||||
function hotp_movingfactortohex(count) {
|
||||
// count is the moving factor in OTP to be converted in bytes
|
||||
v = decimaltohex(count, 16);
|
||||
var decb = new Array();
|
||||
lhex = Crypto.util.hexToBytes(v);
|
||||
for (var i = 0; i < lhex.length; i++) {
|
||||
decb[i] = String.fromCharCode(lhex[i]);
|
||||
}
|
||||
var retval = new String();
|
||||
retval = decb.join('');
|
||||
return retval;
|
||||
}
|
||||
|
||||
function decimaltohex(d, padding) {
|
||||
// d is the decimal value
|
||||
// padding is the padding to apply (O pad)
|
||||
var hex = Number(d).toString(16);
|
||||
padding = typeof(padding) === "undefined" || padding === null ? padding = 2 : padding;
|
||||
while (hex.length < padding) {
|
||||
hex = "0" + hex;
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
function truncatedvalue(h, p) {
|
||||
// h is the hash value
|
||||
// p is precision
|
||||
offset = h[19] & 0xf;
|
||||
v = (h[offset] & 0x7f) << 24 | (h[offset + 1] & 0xff) << 16 | (h[offset + 2] & 0xff) << 8 | (h[offset + 3] & 0xff);
|
||||
v = "" + v;
|
||||
v = v.substr(v.length - p, p);
|
||||
return v;
|
||||
}
|
||||
|
||||
var hmacBytes = Crypto.HMAC(Crypto.SHA1, Crypto.charenc.Binary.stringToBytes((hotp_movingfactortohex(counter))), Crypto.charenc.Binary.stringToBytes(hotp_hexkeytobytestream(key)));
|
||||
|
||||
if (format == "hex40") {
|
||||
return hmacBytes.substring(0, 10);
|
||||
} else if (format == "dec6") {
|
||||
return truncatedvalue(Crypto.util.hexToBytes(hmacBytes), 6);
|
||||
} else if (format == "dec7") {
|
||||
return truncatedvalue(Crypto.util.hexToBytes(hmacBytes), 7);
|
||||
} else if (format == "dec8") {
|
||||
return truncatedvalue(Crypto.util.hexToBytes(hmacBytes), 8);
|
||||
}
|
||||
else {
|
||||
return "unknown format";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function totp(key,format) {
|
||||
var T = parseInt(new Date().getTime()/30000);
|
||||
return hotp(key,T,format);
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
function otp() {
|
||||
var key = 'FAFA';
|
||||
alert('Code: ' + totp(key, 'MODE'));
|
||||
};
|
||||
otp();
|
|
@ -1,30 +0,0 @@
|
|||
import binascii
|
||||
import base64
|
||||
import os.path
|
||||
|
||||
def __content(f):
|
||||
return open(os.path.join(os.path.dirname(__file__), f)).read()
|
||||
|
||||
crypto_js = __content('js/crypto.js')
|
||||
hotp_js = __content('js/hotp.js')
|
||||
myotp_js = __content('js/my-otp.js')
|
||||
|
||||
|
||||
def dataize(document, type='text/html'):
|
||||
return 'data:%s;base64,%s' % (type, base64.b64encode(document))
|
||||
|
||||
def otp_doc(key,mode='dec6'):
|
||||
'''Convert an hexadecimal key to a document able to produce TOTP keys using
|
||||
the dec6 mode
|
||||
'''
|
||||
doc = ''''<html>
|
||||
<body>
|
||||
<script type="text/javascript">%s;history.back()</script>
|
||||
</body>
|
||||
</html>''' % (crypto_js + ';' + hotp_js + ';' + \
|
||||
myotp_js.replace('FAFA',key).replace('MODE',mode))
|
||||
return dataize(doc)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
print '''<html><body><a href="%s" title="Drag me to your bookmark">OTP Password</a></body></html>''' % otp_doc(sys.argv[1])
|
Loading…
Reference in New Issue