Major work

- Django 1.7 and other mode upgrades
- Move code for each service to it's own app
- Password decrypt should now work (RIJNDAEL-256 !#%!@)
This commit is contained in:
nikolark committed 2015-02-16 23:09:52 +01:00
1 parent 2fe608a33b
commit 3462e2865c
32 files changed
+893 -353

No files matched your search

View File
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+54
View File
@@ -0,0 +1,54 @@
import datetime
from django.conf import settings
import logging
import re
from subprocess import Popen, PIPE
logger = logging.getLogger(__name__)
def get_kerberos_principal(username):
principal = "{0}@{1}".format(username, settings.KERBEROS_REALM)
kadmin_query = " -p '{0}' -w {1} -q 'get_principal {2}'".format(
settings.KERBEROS_PASSWORD_CHANGING_PRINCIPAL,
settings.KERBEROS_PASSWORD,
principal)
p = Popen('kadmin' + kadmin_query, shell=True, stdout=PIPE, stderr=PIPE)
output, error = p.communicate()
if error:
# Can not init with KDC?
# KADM5_UNK_PRINC (principal does not exist)
# KADM5_AUTH_GET (requires the get (inquire) privilege)
logger.error(error)
return None
cleaned_output = {}
match = re.findall(r'([\w ]*): (.+)', output)
for key, value in match:
cleaned_output[key] = value
return cleaned_output
def set_kerberos_password(username, raw_password):
principal = "{0}@{1}".format(username, settings.KERBEROS_REALM)
kadmin_query = " -p {0} -w {1} -q 'change_password -pw {2} {3}'".format(
settings.KERBEROS_PASSWORD_CHANGING_PRINCIPAL,
settings.KERBEROS_PASSWORD,
raw_password,
principal)
# FIXME: Running this shell command needs escaping: http://qntm.org/bash
p = Popen('kadmin' + kadmin_query, shell=True, stdout=PIPE, stderr=PIPE)
output, error = p.communicate()
if error:
# Can not init with KDC?
# KADM5_AUTH_MODIFY (requires the modify privilege)
logger.error(error)
def format_krb5_date(date):
try:
formatted_date = datetime.datetime.strptime(date, '%a %b %d %H:%M:%S %Z %Y')
formatted_date = formatted_date.strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
formatted_date = 'never'
return formatted_date
+20
View File
@@ -0,0 +1,20 @@
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from neuf_kerberos.utils import get_kerberos_principal, format_krb5_date
@login_required
def client_status(request):
krb5_principal = get_kerberos_principal(request.GET.get('username'))
if not krb5_principal:
return JsonResponse({'active': False})
last_succ_auth = format_krb5_date(krb5_principal['Last successful authentication'])
status = {
'active': True,
'last_successful_auth': last_succ_auth,
'last_modified': krb5_principal['Last modified']
}
return JsonResponse(status)
View File
Whitespace-only changes.
+17
View File
@@ -0,0 +1,17 @@
from django.contrib import admin
from neuf_ldap.models import LdapGroup, LdapUser
class LdapGroupAdmin(admin.ModelAdmin):
exclude = ['dn', 'usernames']
list_display = ['name', 'gid']
search_fields = ['name']
class LdapUserAdmin(admin.ModelAdmin):
exclude = ['dn', 'password', 'photo']
list_display = ['username', 'first_name', 'last_name', 'email', 'id']
search_fields = ['first_name', 'last_name', 'full_name', 'username']
admin.site.register(LdapGroup, LdapGroupAdmin)
admin.site.register(LdapUser, LdapUserAdmin)
View File
Whitespace-only changes.
+70
View File
@@ -0,0 +1,70 @@
import datetime
import ldapdb.models
from ldapdb.models.fields import CharField, ImageField, IntegerField, ListField
from neuf_ldap.utils import ldap_create, ldap_validate
class LdapUser(ldapdb.models.Model):
""" Represents an LDAP posixAccount,inetOrgPerson entry. """
connection_name = 'ldap'
# LDAP meta-data
base_dn = "ou=People,dc=neuf,dc=no" # FXIME: CONFIG
object_classes = ['inetOrgPerson', 'posixAccount', 'shadowAccount']
# inetOrgPerson
first_name = CharField(db_column='givenName')
last_name = CharField(db_column='sn')
full_name = CharField(db_column='cn')
email = CharField(db_column='mail')
phone = CharField(db_column='telephoneNumber', blank=True)
mobile_phone = CharField(db_column='mobile', blank=True)
photo = ImageField(db_column='jpegPhoto')
# posixAccount
id = IntegerField(db_column='uidNumber', unique=True) # referenced in reset password form
group = IntegerField(db_column='gidNumber')
gecos = CharField(db_column='gecos')
home_directory = CharField(db_column='homeDirectory')
login_shell = CharField(db_column='loginShell', default='/bin/bash')
username = CharField(db_column='uid', primary_key=True)
password = CharField(db_column='userPassword')
# TODO shadowAccount
# ugly hack to use django internals for password reset
last_login = datetime.datetime(2001, 1, 1)
def set_password(self, raw_password):
if raw_password is None:
self.password = "!"
return
self.password = ldap_create(raw_password)
self.save()
def check_password(self, raw_password):
return ldap_validate(raw_password, self.password)
def __unicode__(self):
return self.username
class LdapGroup(ldapdb.models.Model):
""" Represents an LDAP posixGroup entry. """
connection_name = 'ldap'
# LDAP meta-data
base_dn = "ou=Groups,dc=neuf,dc=no" # CONFIG
object_classes = ['posixGroup']
# posixGroup attributes
gid = IntegerField(db_column='gidNumber', unique=True)
name = CharField(db_column='cn', max_length=200, primary_key=True)
usernames = ListField(db_column='memberUid')
def __unicode__(self):
return self.name
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+15 -8
View File
@@ -2,14 +2,17 @@ import hashlib
import os
from base64 import encodestring as encode
from base64 import decodestring as decode
import os.path
# Ref:
# - http://www.openldap.org/faq/data/cache/347.html
# - http://www.openldap.org/doc/admin24/security.html
"""
Ref:
- http://www.openldap.org/faq/data/cache/347.html
- http://www.openldap.org/doc/admin24/security.html
"""
def ldap_create(raw_password, hash_type='ssha'):
# Generate a salted SHA hash
# Generate a salted SHA1 hash
salt = os.urandom(4)
h = hashlib.sha1(raw_password)
h.update(salt)
@@ -27,7 +30,11 @@ def ldap_validate(raw_password, challenge_password):
return digest == hr.digest()
def radius_create(raw_password):
# found in the wild.
nt_password = hashlib.new('md4', raw_password.encode('utf-16le')).hexdigest()
return nt_password
def set_ldap_password(username, raw_password):
from neuf_ldap.models import LdapUser
try:
# LDAP: Lookup the Ldap user with the identical username (1-to-1).
user = LdapUser.objects.get(username=username)
user.set_password(raw_password)
except LdapUser.DoesNotExist:
pass
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
View File
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
View File
Whitespace-only changes.
+106
View File
@@ -0,0 +1,106 @@
from django.db import models
class Radacct(models.Model):
connection_name = 'radius'
radacctid = models.BigIntegerField(primary_key=True)
acctsessionid = models.CharField(max_length=192)
acctuniqueid = models.CharField(max_length=96)
username = models.CharField(max_length=192)
groupname = models.CharField(max_length=192)
realm = models.CharField(max_length=192, blank=True)
nasipaddress = models.CharField(max_length=45)
nasportid = models.CharField(max_length=45, blank=True)
nasporttype = models.CharField(max_length=96, blank=True)
acctstarttime = models.DateTimeField(null=True, blank=True)
acctstoptime = models.DateTimeField(null=True, blank=True)
acctsessiontime = models.IntegerField(null=True, blank=True)
acctauthentic = models.CharField(max_length=96, blank=True)
connectinfo_start = models.CharField(max_length=150, blank=True)
connectinfo_stop = models.CharField(max_length=150, blank=True)
acctinputoctets = models.BigIntegerField(null=True, blank=True)
acctoutputoctets = models.BigIntegerField(null=True, blank=True)
calledstationid = models.CharField(max_length=150)
callingstationid = models.CharField(max_length=150)
acctterminatecause = models.CharField(max_length=96)
servicetype = models.CharField(max_length=96, blank=True)
framedprotocol = models.CharField(max_length=96, blank=True)
framedipaddress = models.CharField(max_length=45)
acctstartdelay = models.IntegerField(null=True, blank=True)
acctstopdelay = models.IntegerField(null=True, blank=True)
xascendsessionsvrkey = models.CharField(max_length=30, blank=True)
class Meta:
db_table = u'radacct'
class Radcheck(models.Model):
connection_name = 'radius'
username = models.CharField(max_length=192)
attribute = models.CharField(max_length=192)
op = models.CharField(max_length=6)
value = models.CharField(max_length=759)
class Meta:
db_table = u'radcheck'
class Radgroupcheck(models.Model):
connection_name = 'radius'
groupname = models.CharField(max_length=192)
attribute = models.CharField(max_length=192)
op = models.CharField(max_length=6)
value = models.CharField(max_length=759)
class Meta:
db_table = u'radgroupcheck'
class Radgroupreply(models.Model):
connection_name = 'radius'
groupname = models.CharField(max_length=192)
attribute = models.CharField(max_length=192)
op = models.CharField(max_length=6)
value = models.CharField(max_length=759)
class Meta:
db_table = u'radgroupreply'
class Radpostauth(models.Model):
connection_name = 'radius'
username = models.CharField(max_length=192)
pass_field = models.CharField(max_length=192, db_column='pass')
reply = models.CharField(max_length=96)
authdate = models.DateTimeField()
class Meta:
db_table = u'radpostauth'
class Radreply(models.Model):
connection_name = 'radius'
username = models.CharField(max_length=192)
attribute = models.CharField(max_length=192)
op = models.CharField(max_length=6)
value = models.CharField(max_length=759)
class Meta:
db_table = u'radreply'
class Radusergroup(models.Model):
connection_name = 'radius'
username = models.CharField(max_length=192)
groupname = models.CharField(max_length=192)
priority = models.IntegerField()
class Meta:
db_table = u'radusergroup'
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+28
View File
@@ -0,0 +1,28 @@
import hashlib
from neuf_radius.models import Radcheck
def radius_create(raw_password):
# found in the wild.
nt_password = hashlib.new('md4', raw_password.encode('utf-16le')).hexdigest()
return nt_password
def set_radius_password(username, raw_password):
"""
Set password in Radius mysql backend.
Create user if one with an NT-Password does not exist.
"""
pw_type = 'NT-Password'
radius_defaults = {'attribute': pw_type, 'op': ':='}
radius_user, created = Radcheck.objects.get_or_create(
username=username,
defaults=radius_defaults)
# Some have clear text passwords!
if radius_user.attribute != pw_type:
radius_user.attribute = pw_type
radius_user.value = radius_create(raw_password)
radius_user.save()
+20
View File
@@ -0,0 +1,20 @@
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from neuf_radius.models import Radcheck, Radpostauth
@login_required
def wireless_status(request):
username = request.GET.get('username')
try:
radius_user = Radcheck.objects.get(username=username)
except Radcheck.DoesNotExist:
return JsonResponse({'active': False})
# get last authentication
last_auth = Radpostauth.objects.filter(username__iexact=username, reply='Access-Accept').order_by('authdate')
status = {'active': True, 'hash': radius_user.attribute}
if len(last_auth) != 0:
status['last_successful_auth'] = last_auth[0].authdate.strftime('%Y-%m-%d %H:%M:%S')
return JsonResponse(status)
-16
View File
@@ -1,17 +1 @@
from django.contrib import admin
from neuf_userinfo.models import LdapGroup, LdapUser
class LdapGroupAdmin(admin.ModelAdmin):
exclude = ['dn', 'usernames']
list_display = ['name', 'gid']
search_fields = ['name']
class LdapUserAdmin(admin.ModelAdmin):
exclude = ['dn', 'password', 'photo']
list_display = ['username', 'first_name', 'last_name', 'email', 'id']
search_fields = ['first_name', 'last_name', 'full_name', 'username']
admin.site.register(LdapGroup, LdapGroupAdmin)
admin.site.register(LdapUser, LdapUserAdmin)
+41 -22
View File
@@ -1,6 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
from django.contrib.auth.forms import SetPasswordForm, PasswordResetForm
from django.contrib.auth.forms import SetPasswordForm
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import get_current_site
from django.core.validators import MinLengthValidator
@@ -9,32 +9,40 @@ from django.template import loader
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import ugettext as _
import sys
from inside.models import InsideUser
from inside.utils import set_inside_password
from neuf_userinfo.models import LdapUser
from neuf_userinfo.utils import set_kerberos_password, set_radius_password
from neuf_kerberos.utils import set_kerberos_password
from neuf_radius.utils import set_radius_password
from neuf_ldap.utils import set_ldap_password
from neuf_userinfo.validators import PasswordValidator
class NeufSetPasswordForm(SetPasswordForm):
"""
Saves a user password in each of the following services:
- Inside (all)
- Kerberos
- LDAP
- RADIUS
Note: self.user is a local Django User object (which does not have a usable password)
"""
def save(self, commit=True):
username = self.user.username
password = self.cleaned_data['new_password1']
# Membership database
set_inside_password(self.user.username, self.cleaned_data['new_password1'])
set_inside_password(username, password)
# Active services
set_kerberos_password(self.user.username, self.cleaned_data['new_password1'])
set_radius_password(self.user.username, self.cleaned_data['new_password1'])
set_kerberos_password(username, password)
set_radius_password(username, password)
set_ldap_password(username, password)
# LDAP: Lookup the Ldap user with the identical username (1-to-1).
self.user = LdapUser.objects.get(username=self.user.username)
self.user.set_password(self.cleaned_data['new_password1'])
if commit:
self.user.save()
return self.user
return self.user # Local Django User
def clean_new_password1(self):
raw_password = self.cleaned_data.get('new_password1')
@@ -49,12 +57,13 @@ class NeufPasswordChangeForm(NeufSetPasswordForm):
pass
class NeufPasswordResetForm(PasswordResetForm):
class NeufPasswordResetForm(forms.Form):
EMAIL_ERROR_MSG = "That e-mail address doesn't have an associated user account. Are you sure you've registered?"
email = forms.EmailField(label=_("Email"), max_length=254)
def clean_email(self):
email = self.cleaned_data["email"].lower()
self.users_cache = InsideUser.objects.filter(email=email)
self.users_cache = InsideUser.objects.filter(email=email) # From Membership database (Inside)
if len(self.users_cache) == 0:
raise forms.ValidationError(_(self.EMAIL_ERROR_MSG))
return email
@@ -63,15 +72,24 @@ class NeufPasswordResetForm(PasswordResetForm):
subject_template_name='registration/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html',
use_https=False, token_generator=default_token_generator,
from_email=None, request=None):
from_email=None, request=None, html_email_template_name=None):
"""
Generates a one-use only link for resetting password and sends to the user.
Generates a one-use only link for resetting password and sends to the
user.
Note: this is the same form as django.contrib.auth.forms.PasswordResetForm
with some changes for looking up a custom user object.
"""
from django.core.mail import send_mail
# UserModel = get_user_model()
# email = self.cleaned_data["email"]
# active_users = UserModel._default_manager.filter(
# email__iexact=email, is_active=True)
for user in self.users_cache:
# Make sure that no email is sent to a user that actually has
# a password marked as unusable
# if not user.has_usable_password():
# continue
if not domain_override:
current_site = get_current_site(request)
site_name = current_site.name
@@ -91,8 +109,9 @@ class NeufPasswordResetForm(PasswordResetForm):
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
email = loader.render_to_string(email_template_name, c)
if sys.version_info < (2, 6, 6):
# Workaround for http://bugs.python.org/issue1368247
email = email.encode('utf-8')
send_mail(subject, email, from_email, [user.email])
if html_email_template_name:
html_email = loader.render_to_string(html_email_template_name, c)
else:
html_email = None
send_mail(subject, email, from_email, [user.email], html_message=html_email)
+1 -195
View File
@@ -1,195 +1 @@
import datetime
from django.db import models
import ldapdb.models
from ldapdb.models.fields import CharField, ImageField, IntegerField, ListField
import passwd
class LdapUser(ldapdb.models.Model):
"""
Class for representing an LDAP user entry.
"""
connection_name = 'ldap'
# LDAP meta-data
base_dn = "ou=People,dc=neuf,dc=no" # FXIME: CONFIG
object_classes = ['inetOrgPerson', 'posixAccount', 'shadowAccount']
# inetOrgPerson
first_name = CharField(db_column='givenName')
last_name = CharField(db_column='sn')
full_name = CharField(db_column='cn')
email = CharField(db_column='mail')
phone = CharField(db_column='telephoneNumber', blank=True)
mobile_phone = CharField(db_column='mobile', blank=True)
photo = ImageField(db_column='jpegPhoto')
# posixAccount
id = IntegerField(db_column='uidNumber', unique=True) # referenced in reset password form
group = IntegerField(db_column='gidNumber')
gecos = CharField(db_column='gecos')
home_directory = CharField(db_column='homeDirectory')
login_shell = CharField(db_column='loginShell', default='/bin/bash')
username = CharField(db_column='uid', primary_key=True)
password = CharField(db_column='userPassword')
# ugly hack to use django internals for password reset
last_login = datetime.datetime(2001, 1, 1)
def set_password(self, raw_password):
if raw_password is None:
self.password = "!"
else:
self.password = passwd.ldap_create(raw_password)
self.save()
def check_password(self, raw_password):
"""
Returns a boolean of whether the raw_password was correct.
"""
return passwd.ldap_validate(raw_password, self.password)
def __str__(self):
return self.username
def __unicode__(self):
return self.full_name
class LdapGroup(ldapdb.models.Model):
"""
Class for representing an LDAP group entry.
"""
connection_name = 'ldap'
# LDAP meta-data
base_dn = "ou=Groups,dc=neuf,dc=no" # CONFIG
object_classes = ['posixGroup']
# posixGroup attributes
gid = IntegerField(db_column='gidNumber', unique=True)
name = CharField(db_column='cn', max_length=200, primary_key=True)
usernames = ListField(db_column='memberUid')
def __str__(self):
return self.name
def __unicode__(self):
return self.name
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
# * Rearrange models' order
# * Make sure each model has one field with primary_key=True
# Feel free to rename the models, but don't rename db_table values or field names.
#
# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'
# into your database.
class Radacct(models.Model):
connection_name = 'radius'
radacctid = models.BigIntegerField(primary_key=True)
acctsessionid = models.CharField(max_length=192)
acctuniqueid = models.CharField(max_length=96)
username = models.CharField(max_length=192)
groupname = models.CharField(max_length=192)
realm = models.CharField(max_length=192, blank=True)
nasipaddress = models.CharField(max_length=45)
nasportid = models.CharField(max_length=45, blank=True)
nasporttype = models.CharField(max_length=96, blank=True)
acctstarttime = models.DateTimeField(null=True, blank=True)
acctstoptime = models.DateTimeField(null=True, blank=True)
acctsessiontime = models.IntegerField(null=True, blank=True)
acctauthentic = models.CharField(max_length=96, blank=True)
connectinfo_start = models.CharField(max_length=150, blank=True)
connectinfo_stop = models.CharField(max_length=150, blank=True)
acctinputoctets = models.BigIntegerField(null=True, blank=True)
acctoutputoctets = models.BigIntegerField(null=True, blank=True)
calledstationid = models.CharField(max_length=150)
callingstationid = models.CharField(max_length=150)
acctterminatecause = models.CharField(max_length=96)
servicetype = models.CharField(max_length=96, blank=True)
framedprotocol = models.CharField(max_length=96, blank=True)
framedipaddress = models.CharField(max_length=45)
acctstartdelay = models.IntegerField(null=True, blank=True)
acctstopdelay = models.IntegerField(null=True, blank=True)
xascendsessionsvrkey = models.CharField(max_length=30, blank=True)
class Meta:
db_table = u'radacct'
class Radcheck(models.Model):
connection_name = 'radius'
username = models.CharField(max_length=192)
attribute = models.CharField(max_length=192)
op = models.CharField(max_length=6)
value = models.CharField(max_length=759)
class Meta:
db_table = u'radcheck'
class Radgroupcheck(models.Model):
connection_name = 'radius'
groupname = models.CharField(max_length=192)
attribute = models.CharField(max_length=192)
op = models.CharField(max_length=6)
value = models.CharField(max_length=759)
class Meta:
db_table = u'radgroupcheck'
class Radgroupreply(models.Model):
connection_name = 'radius'
groupname = models.CharField(max_length=192)
attribute = models.CharField(max_length=192)
op = models.CharField(max_length=6)
value = models.CharField(max_length=759)
class Meta:
db_table = u'radgroupreply'
class Radpostauth(models.Model):
connection_name = 'radius'
username = models.CharField(max_length=192)
pass_field = models.CharField(max_length=192, db_column='pass')
reply = models.CharField(max_length=96)
authdate = models.DateTimeField()
class Meta:
db_table = u'radpostauth'
class Radreply(models.Model):
connection_name = 'radius'
username = models.CharField(max_length=192)
attribute = models.CharField(max_length=192)
op = models.CharField(max_length=6)
value = models.CharField(max_length=759)
class Meta:
db_table = u'radreply'
class Radusergroup(models.Model):
connection_name = 'radius'
username = models.CharField(max_length=192)
groupname = models.CharField(max_length=192)
priority = models.IntegerField()
class Meta:
db_table = u'radusergroup'
from django.db import models
+378
View File
@@ -0,0 +1,378 @@
"""
Ref: http://wiki.birth-online.de/_media/snippets/python/rijndael.py
A pure python (slow) implementation of rijndael with a decent interface
To include -
from rijndael import rijndael
To do a key setup -
r = rijndael(key, block_size = 16)
key must be a string of length 16, 24, or 32
blocksize must be 16, 24, or 32. Default is 16
To use -
ciphertext = r.encrypt(plaintext)
plaintext = r.decrypt(ciphertext)
If any strings are of the wrong length a ValueError is thrown
"""
# ported from the Java reference code by Bram Cohen, April 2001
# this code is public domain, unless someone makes
# an intellectual property claim against the reference
# code, in which case it can be made public domain by
# deleting all the comments and renaming all the variables
import copy
import string
shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]],
[[0, 0], [1, 5], [2, 4], [3, 3]],
[[0, 0], [1, 7], [3, 5], [4, 4]]]
# [keysize][block_size]
num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}}
A = [[1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 1, 1, 1, 1],
[1, 1, 0, 0, 0, 1, 1, 1],
[1, 1, 1, 0, 0, 0, 1, 1],
[1, 1, 1, 1, 0, 0, 0, 1]]
# produce log and alog tables, needed for multiplying in the
# field GF(2^m) (generator = 3)
alog = [1]
for i in range(255):
j = (alog[-1] << 1) ^ alog[-1]
if j & 0x100 != 0:
j ^= 0x11B
alog.append(j)
log = [0] * 256
for i in range(1, 255):
log[alog[i]] = i
# multiply two elements of GF(2^m)
def mul(a, b):
if a == 0 or b == 0:
return 0
return alog[(log[a & 0xFF] + log[b & 0xFF]) % 255]
# substitution box based on F^{-1}(x)
box = [[0] * 8 for i in range(256)]
box[1][7] = 1
for i in range(2, 256):
j = alog[255 - log[i]]
for t in range(8):
box[i][t] = (j >> (7 - t)) & 0x01
B = [0, 1, 1, 0, 0, 0, 1, 1]
# affine transform: box[i] <- B + A*box[i]
cox = [[0] * 8 for i in range(256)]
for i in range(256):
for t in range(8):
cox[i][t] = B[t]
for j in range(8):
cox[i][t] ^= A[t][j] * box[i][j]
# S-boxes and inverse S-boxes
S = [0] * 256
Si = [0] * 256
for i in range(256):
S[i] = cox[i][0] << 7
for t in range(1, 8):
S[i] ^= cox[i][t] << (7-t)
Si[S[i] & 0xFF] = i
# T-boxes
G = [[2, 1, 1, 3],
[3, 2, 1, 1],
[1, 3, 2, 1],
[1, 1, 3, 2]]
AA = [[0] * 8 for i in range(4)]
for i in range(4):
for j in range(4):
AA[i][j] = G[i][j]
AA[i][i+4] = 1
for i in range(4):
pivot = AA[i][i]
if pivot == 0:
t = i + 1
while AA[t][i] == 0 and t < 4:
t += 1
assert t != 4, 'G matrix must be invertible'
for j in range(8):
AA[i][j], AA[t][j] = AA[t][j], AA[i][j]
pivot = AA[i][i]
for j in range(8):
if AA[i][j] != 0:
AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255]
for t in range(4):
if i != t:
for j in range(i+1, 8):
AA[t][j] ^= mul(AA[i][j], AA[t][i])
AA[t][i] = 0
iG = [[0] * 4 for i in range(4)]
for i in range(4):
for j in range(4):
iG[i][j] = AA[i][j + 4]
def mul4(a, bs):
if a == 0:
return 0
r = 0
for b in bs:
r <<= 8
if b != 0:
r = r | mul(a, b)
return r
T1 = []
T2 = []
T3 = []
T4 = []
T5 = []
T6 = []
T7 = []
T8 = []
U1 = []
U2 = []
U3 = []
U4 = []
for t in range(256):
s = S[t]
T1.append(mul4(s, G[0]))
T2.append(mul4(s, G[1]))
T3.append(mul4(s, G[2]))
T4.append(mul4(s, G[3]))
s = Si[t]
T5.append(mul4(s, iG[0]))
T6.append(mul4(s, iG[1]))
T7.append(mul4(s, iG[2]))
T8.append(mul4(s, iG[3]))
U1.append(mul4(t, iG[0]))
U2.append(mul4(t, iG[1]))
U3.append(mul4(t, iG[2]))
U4.append(mul4(t, iG[3]))
# round constants
rcon = [1]
r = 1
for t in range(1, 30):
r = mul(2, r)
rcon.append(r)
del A
del AA
del pivot
del B
del G
del box
del log
del alog
del i
del j
del r
del s
del t
del mul
del mul4
del cox
del iG
class rijndael:
def __init__(self, key, block_size = 16):
if block_size != 16 and block_size != 24 and block_size != 32:
raise ValueError('Invalid block size: ' + str(block_size))
if len(key) != 16 and len(key) != 24 and len(key) != 32:
raise ValueError('Invalid key size: ' + str(len(key)))
self.block_size = block_size
ROUNDS = num_rounds[len(key)][block_size]
BC = block_size // 4
# encryption round keys
Ke = [[0] * BC for i in range(ROUNDS + 1)]
# decryption round keys
Kd = [[0] * BC for i in range(ROUNDS + 1)]
ROUND_KEY_COUNT = (ROUNDS + 1) * BC
KC = len(key) // 4
# copy user material bytes into temporary ints
tk = []
for i in range(0, KC):
tk.append((ord(key[i * 4]) << 24) | (ord(key[i * 4 + 1]) << 16) |
(ord(key[i * 4 + 2]) << 8) | ord(key[i * 4 + 3]))
# copy values into round key arrays
t = 0
j = 0
while j < KC and t < ROUND_KEY_COUNT:
Ke[t // BC][t % BC] = tk[j]
Kd[ROUNDS - (t // BC)][t % BC] = tk[j]
j += 1
t += 1
tt = 0
rconpointer = 0
while t < ROUND_KEY_COUNT:
# extrapolate using phi (the round key evolution function)
tt = tk[KC - 1]
tk[0] ^= (S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^ \
(S[(tt >> 8) & 0xFF] & 0xFF) << 16 ^ \
(S[ tt & 0xFF] & 0xFF) << 8 ^ \
(S[(tt >> 24) & 0xFF] & 0xFF) ^ \
(rcon[rconpointer] & 0xFF) << 24
rconpointer += 1
if KC != 8:
for i in range(1, KC):
tk[i] ^= tk[i-1]
else:
for i in range(1, KC // 2):
tk[i] ^= tk[i-1]
tt = tk[KC // 2 - 1]
tk[KC // 2] ^= (S[ tt & 0xFF] & 0xFF) ^ \
(S[(tt >> 8) & 0xFF] & 0xFF) << 8 ^ \
(S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \
(S[(tt >> 24) & 0xFF] & 0xFF) << 24
for i in range(KC // 2 + 1, KC):
tk[i] ^= tk[i-1]
# copy values into round key arrays
j = 0
while j < KC and t < ROUND_KEY_COUNT:
Ke[t // BC][t % BC] = tk[j]
Kd[ROUNDS - (t // BC)][t % BC] = tk[j]
j += 1
t += 1
# inverse MixColumn where needed
for r in range(1, ROUNDS):
for j in range(BC):
tt = Kd[r][j]
Kd[r][j] = U1[(tt >> 24) & 0xFF] ^ \
U2[(tt >> 16) & 0xFF] ^ \
U3[(tt >> 8) & 0xFF] ^ \
U4[ tt & 0xFF]
self.Ke = Ke
self.Kd = Kd
def encrypt(self, plaintext):
if len(plaintext) != self.block_size:
raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext)))
Ke = self.Ke
BC = self.block_size // 4
ROUNDS = len(Ke) - 1
if BC == 4:
SC = 0
elif BC == 6:
SC = 1
else:
SC = 2
s1 = shifts[SC][1][0]
s2 = shifts[SC][2][0]
s3 = shifts[SC][3][0]
a = [0] * BC
# temporary work array
t = []
# plaintext to ints + key
for i in range(BC):
t.append((ord(plaintext[i * 4 ]) << 24 |
ord(plaintext[i * 4 + 1]) << 16 |
ord(plaintext[i * 4 + 2]) << 8 |
ord(plaintext[i * 4 + 3]) ) ^ Ke[0][i])
# apply round transforms
for r in range(1, ROUNDS):
for i in range(BC):
a[i] = (T1[(t[ i ] >> 24) & 0xFF] ^
T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^
T3[(t[(i + s2) % BC] >> 8) & 0xFF] ^
T4[ t[(i + s3) % BC] & 0xFF] ) ^ Ke[r][i]
t = copy.copy(a)
# last round is special
result = []
for i in range(BC):
tt = Ke[ROUNDS][i]
result.append((S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append((S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
result.append((S[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
result.append((S[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF)
return ''.join(map(chr, result))
def decrypt(self, ciphertext):
if len(ciphertext) != self.block_size:
raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(ciphertext)))
Kd = self.Kd
BC = self.block_size // 4
ROUNDS = len(Kd) - 1
if BC == 4:
SC = 0
elif BC == 6:
SC = 1
else:
SC = 2
s1 = shifts[SC][1][1]
s2 = shifts[SC][2][1]
s3 = shifts[SC][3][1]
a = [0] * BC
# temporary work array
t = [0] * BC
# ciphertext to ints + key
for i in range(BC):
t[i] = (ord(ciphertext[i * 4 ]) << 24 |
ord(ciphertext[i * 4 + 1]) << 16 |
ord(ciphertext[i * 4 + 2]) << 8 |
ord(ciphertext[i * 4 + 3]) ) ^ Kd[0][i]
# apply round transforms
for r in range(1, ROUNDS):
for i in range(BC):
a[i] = (T5[(t[ i ] >> 24) & 0xFF] ^
T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^
T7[(t[(i + s2) % BC] >> 8) & 0xFF] ^
T8[ t[(i + s3) % BC] & 0xFF] ) ^ Kd[r][i]
t = copy.copy(a)
# last round is special
result = []
for i in range(BC):
tt = Kd[ROUNDS][i]
result.append((Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
result.append((Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
result.append((Si[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF)
result.append((Si[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF)
return ''.join(map(chr, result))
def encrypt(key, block):
return rijndael(key, len(block)).encrypt(block)
def decrypt(key, block):
return rijndael(key, len(block)).decrypt(block)
def test():
def t(kl, bl):
b = 'b' * bl
r = rijndael('a' * kl, bl)
assert r.decrypt(r.encrypt(b)) == b
t(16, 16)
t(16, 24)
t(16, 32)
t(24, 16)
t(24, 24)
t(24, 32)
t(32, 16)
t(32, 24)
t(32, 32)
+36 -63
View File
@@ -1,78 +1,51 @@
# coding: utf-8
from __future__ import unicode_literals
import datetime
from django.conf import settings
import base64
import rijndael
import logging
import passwd
import re
from subprocess import Popen, PIPE
from models import Radcheck
logger = logging.getLogger(__name__)
"""
Rijndael stuff, thank you SO!
Ref: http://stackoverflow.com/questions/8217269/decrypting-strings-in-python-that-were-encrypted-with-mcrypt-rijndael-256-in-php#answers-header
"""
def get_kerberos_principal(username):
principal = "{0}@{1}".format(username, settings.KERBEROS_REALM)
kadmin_query = " -p '{0}' -w {1} -q 'get_principal {2}'".format(
settings.KERBEROS_PASSWORD_CHANGING_PRINCIPAL,
settings.KERBEROS_PASSWORD,
principal)
p = Popen('kadmin' + kadmin_query, shell=True, stdout=PIPE, stderr=PIPE)
output, error = p.communicate()
if error:
# Can not init with KDC?
# KADM5_UNK_PRINC (principal does not exist)
# KADM5_AUTH_GET (requires the get (inquire) privilege)
logger.error(error)
return None
else:
cleaned_output = {}
match = re.findall(r'([\w ]*): (.+)', output)
for key, value in match:
cleaned_output[key] = value
return cleaned_output
KEY_SIZE = 16
BLOCK_SIZE = 32
def set_kerberos_password(username, raw_password):
principal = "{0}@{1}".format(username, settings.KERBEROS_REALM)
kadmin_query = " -p {0} -w {1} -q 'change_password -pw {2} {3}'".format(
settings.KERBEROS_PASSWORD_CHANGING_PRINCIPAL,
settings.KERBEROS_PASSWORD,
raw_password,
principal)
# FIXME: Running this shell command needs escaping: http://qntm.org/bash
p = Popen('kadmin' + kadmin_query, shell=True, stdout=PIPE, stderr=PIPE)
output, error = p.communicate()
if error:
# Can not init with KDC?
# KADM5_AUTH_MODIFY (requires the modify privilege)
logger.error(error)
def encrypt_rijndael(key, plaintext):
padded_key = key.ljust(KEY_SIZE, '\0')
padded_text = plaintext + (BLOCK_SIZE - len(plaintext) % BLOCK_SIZE) * '\0'
# could also be one of
#if len(plaintext) % BLOCK_SIZE != 0:
# padded_text = plaintext.ljust((len(plaintext) / BLOCK_SIZE) + 1 * BLOCKSIZE), '\0')
# -OR-
#padded_text = plaintext.ljust((len(plaintext) + (BLOCK_SIZE - len(plaintext) % BLOCK_SIZE)), '\0')
r = rijndael.rijndael(padded_key, BLOCK_SIZE)
ciphertext = ''
for start in range(0, len(padded_text), BLOCK_SIZE):
ciphertext += r.encrypt(padded_text[start:start+BLOCK_SIZE])
encoded = base64.b64encode(ciphertext)
return encoded
def set_radius_password(username, raw_password):
"""
Set password in Radius mysql backend.
Create user if one with an NT-Password does not exist.
"""
pw_type = 'NT-Password'
radius_defaults = {'attribute': pw_type, 'op': ':='}
radius_user, created = Radcheck.objects.get_or_create(
username=username,
defaults=radius_defaults)
def decrypt_rijndael(key, encoded):
padded_key = key.ljust(KEY_SIZE, '\0')
# Some have clear text passwords!
if radius_user.attribute != pw_type:
radius_user.attribute = pw_type
ciphertext = base64.b64decode(encoded)
radius_user.value = passwd.radius_create(raw_password)
radius_user.save()
r = rijndael.rijndael(padded_key, BLOCK_SIZE)
padded_text = ''
for start in range(0, len(ciphertext), BLOCK_SIZE):
padded_text += r.decrypt(ciphertext[start:start+BLOCK_SIZE])
def format_krb5_date(date):
try:
formatted_date = datetime.datetime.strptime(date, '%a %b %d %H:%M:%S %Z %Y')
formatted_date = formatted_date.strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
formatted_date = 'never'
return formatted_date
plaintext = padded_text.split('\x00', 1)[0]
return plaintext
+53 -37
View File
@@ -1,21 +1,22 @@
# coding: utf-8
from django.conf import settings
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.forms import SetPasswordForm, AuthenticationForm
from django.contrib.auth import login, logout as auth_logout
from django.contrib.auth.tokens import default_token_generator
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render_to_response, resolve_url, render
from django.template import RequestContext
from django.template.response import TemplateResponse
from django.utils.http import urlsafe_base64_decode
from django.views.decorators.cache import never_cache
from django.views.decorators.debug import sensitive_post_parameters
import json
from inside.models import InsideUser
from django.views.generic import View
from models import *
import utils
from inside.models import InsideUser
from neuf_ldap.models import LdapGroup, LdapUser
from neuf_userinfo.utils import decrypt_rijndael
def index(request):
@@ -35,6 +36,7 @@ def index(request):
@login_required
def profile(request):
# FIXME: Allmost identical to below
username = request.user.username
groups = request.user.groups.all()
ldap_user = None
@@ -63,38 +65,6 @@ def user_profile(request, username):
return render_to_response('private/profile.html', locals(), context_instance=RequestContext(request))
# TODO security
def client_status(request):
krb5_principal = utils.get_kerberos_principal(request.GET.get('username'))
if krb5_principal:
last_succ_auth = utils.format_krb5_date(krb5_principal['Last successful authentication'])
status = {
'active': True,
'last_successful_auth': last_succ_auth,
'last_modified': krb5_principal['Last modified']
}
else:
status = {'active': False}
return HttpResponse(json.dumps(status), content_type='application/javascript; charset=utf8')
# TODO security
def wireless_status(request):
username = request.GET.get('username')
try:
radius_user = Radcheck.objects.get(username=username)
except Radcheck.ObjectDoesNotExist:
return HttpResponse(json.dumps({'active': False}), content_type='application/javascript; charset=utf8')
# get last authentication
last_auth = Radpostauth.objects.filter(username__iexact=username, reply='Access-Accept').order_by('authdate')
status = {'active': True, 'hash': radius_user.attribute}
if len(last_auth) != 0:
status['last_successful_auth'] = last_auth[0].authdate.strftime('%Y-%m-%d %H:%M:%S')
return HttpResponse(json.dumps(status), content_type='application/javascript; charset=utf8')
def logout(request):
auth_logout(request)
return render(request, 'registration/logout.html')
@@ -148,3 +118,49 @@ def password_reset_confirm(request, uidb64=None, token=None,
context.update(extra_context)
return TemplateResponse(request, template_name, context, current_app=current_app)
class AddNewUserView(View):
ATTRS = ['username', 'firstname', 'lastname', 'email', 'password', 'groups']
def get(self, request):
"""
- Create new user in LDAP (username, first_name, last_name, email)
- add to groups (usergroup, dns-alle), it not exists, create
- Set LDAP password
- Create automount entry
- Create kerberos principal and set password
- Create homedir on wii
- Set RADIUS password
"""
if not self._validate_api_key(request.GET.get('api_key', '')):
return HttpResponse('Invalid api_key')
if not self._validate_attributes(request):
return HttpResponse('Missing at least one required attribute: '.format(', '.join(self.ATTRS)))
user_data = self._get_user(request.GET)
# TODO: you are here
result = {'yousir': 'are a scholar!'}
return JsonResponse(result)
def _validate_api_key(self, api_key):
if api_key == '' or api_key != settings.INSIDE_USERSYNC_API_KEY:
return False
return True
def _validate_attributes(self, request):
for a in self.ATTRS:
if a not in request.GET:
return False
return True
def _get_user(self, params):
user = {a: params[a] for a in self.ATTRS}
user['password'] = decrypt_rijndael(settings.INSIDE_USERSYNC_ENC_KEY, user['password'])
user['groups'] = user['groups'].strip().split(',')
return user
+5 -6
View File
@@ -1,9 +1,8 @@
Django==1.6.5
Django==1.7.4
django-auth-ldap==1.2.0
django-extensions==1.3.8
django-extensions==1.5.0
django-ldapdb==0.3.2
dj-static==0.0.5
MySQL-python==1.2.5
dj-static==0.0.6
mysqlclient==1.3.5
passlib==1.6.2
python-ldap==2.4.15
six==1.7.3
South==1.0
@@ -1,2 +1,5 @@
Noen har bedt om at passordet for brukeren {{ user.username }} med epostadresse {{ email }} skal endres. Vennligst følg lenken nedenfor for å sette et nytt passord:
{{ protocol }}://{{ domain }}{% url "neuf_userinfo.views.password_reset_confirm" uidb64=uid token=token %}
{% comment %}
ldap_username is actually just the Inside username [sic].
{% endcomment %}Noen har bedt om at passordet for brukeren {{ user.ldap_username }} med epostadresse {{ email }} skal endres.<br>
<br>
<a href="{{ protocol }}://{{ domain }}{% url "neuf_userinfo.views.password_reset_confirm" uidb64=uid token=token %}">Klikk her for å sette et nytt passord</a>.
@@ -0,0 +1,4 @@
{% comment %}
ldap_username is actually just the Inside username [sic].
{% endcomment %}Noen har bedt om at passordet for brukeren {{ user.ldap_username }} med epostadresse {{ email }} skal endres. Vennligst følg lenken nedenfor for å sette et nytt passord:
{{ protocol }}://{{ domain }}{% url "neuf_userinfo.views.password_reset_confirm" uidb64=uid token=token %}
+5 -1
View File
@@ -36,10 +36,12 @@ INSTALLED_APPS = (
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'south',
)
INSTALLED_APPS += (
'neuf_userinfo',
'neuf_ldap',
'neuf_radius',
'neuf_kerberos',
'inside',
)
@@ -194,6 +196,8 @@ KERBEROS_PASSWORD = ''
# Inside
INSIDE_GROUPS_SYNC_DELETE = True # When user logs in, groups are synced (locally in Django), this deletes aswell
INSIDE_USERSYNC_API_KEY = ''
INSIDE_USERSYNC_ENC_KEY = ''
try:
from local_settings import *
+14 -3
View File
@@ -1,6 +1,8 @@
from django.conf.urls import include, patterns, url
from django.contrib import admin
from neuf_userinfo.forms import NeufPasswordChangeForm, NeufSetPasswordForm, NeufPasswordResetForm
from neuf_userinfo.views import AddNewUserView
admin.autodiscover()
import settings
@@ -10,8 +12,8 @@ urlpatterns = patterns(
url(r'^admin/', include(admin.site.urls)),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^userstatus/client/$', 'neuf_userinfo.views.client_status'),
url(r'^userstatus/wireless/$', 'neuf_userinfo.views.wireless_status'),
url(r'^userstatus/client/$', 'neuf_kerberos.views.client_status'),
url(r'^userstatus/wireless/$', 'neuf_radius.views.wireless_status'),
url(r'^$', 'neuf_userinfo.views.index'),
)
@@ -30,11 +32,20 @@ urlpatterns += patterns(
# Reset password (forgot password)
url(r'^accounts/password/reset$', 'django.contrib.auth.views.password_reset',
{'password_reset_form': NeufPasswordResetForm, 'from_email': settings.DEFAULT_FROM_EMAIL}),
{'password_reset_form': NeufPasswordResetForm,
'from_email': settings.DEFAULT_FROM_EMAIL,
'email_template_name': 'registration/password_reset_email.txt',
'html_email_template_name': 'registration/password_reset_email.html'}),
url(r'^accounts/password/reset/done$', 'django.contrib.auth.views.password_reset_done', name='password_reset_done'),
# Set password
url(r'^accounts/password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
'neuf_userinfo.views.password_reset_confirm',
{'set_password_form': NeufSetPasswordForm}),
url(r'^accounts/password/reset/complete$', 'django.contrib.auth.views.password_reset_complete', name='password_reset_complete'),
)
# Rewrite of usersync.neuf.no
urlpatterns += patterns(
'',
url(r'^usersync/$', AddNewUserView.as_view(), name='usersync'),
)