import web
import os
import hmac
import base64
import struct
import hashlib
import random
import string
import time
import bcrypt
import logging
import smtplib
from email.message import EmailMessage
from email.headerregistry import Address
from uuid import uuid4
logger = logging.getLogger(__name__)
def sendmail(subject, message, to_name, to_email, from_name="Beelance", from_email="beelance@ntnu.no"):
try:
msg = EmailMessage()
msg['From'] = Address(from_name, from_email)
msg['To'] = Address(to_name, to_email)
msg['Subject'] = subject
msg.set_content(message)
logger.info("Sending email: %s", msg)
with get_smtp() as smtp:
smtp.set_debuglevel(2)
smtp.send_message(msg)
except Exception:
logging.exception("Exception when sending email")
def get_smtp(timeout=2):
smtp_server = os.getenv("smtp_server", default="molde.idi.ntnu.no") + ":25"
return smtplib.SMTP(smtp_server, timeout=timeout)
def get_render(path='templates/', globals={}, **kwargs):
default_globals = {
'session': web.config._session,
'csrf_field': csrf_field,
}
return web.template.render(
path,
globals={**default_globals, **globals},
**kwargs,
)
def get_nav_bar(session):
"""
Generates the page nav bar
:return: The navigation bar HTML markup
"""
result = ''
return result
def get_element_count(data, element):
"""
Determine the number of tasks created by removing
the four other elements from count and divide by the
number of variables in one task.
:param data: The data object from web.input
:return: The number of tasks opened by the client
"""
task_count = 0
while True:
try:
data[element+str(task_count)]
task_count += 1
except:
break
return task_count
def csrf_token():
"""
Get the CSRF token for the session
"""
session = web.ctx.session
if 'csrf_token' not in session:
session.csrf_token = uuid4().hex
return session.csrf_token
def csrf_field():
"""
Return a HTML form field for the CSRF token
"""
return f''
def csrf_protected(f):
"""
Decorate a function to do a CSRF check.
"""
def decorated(*args, **kwargs):
session = web.ctx.session
inp = web.input()
if not ('csrf_token' in inp and inp.csrf_token == session.pop('csrf_token', None)):
raise web.HTTPError(
'400 Bad request',
{'content-type': 'text/html'},
'Cross-site request forgery attempt',
)
return f(*args, **kwargs)
return decorated
def is_common_password(password):
"""Helper function that checks various common passwords."""
def common_sequences(n):
# Check sequences of the same number
for i in range(n):
for j in range(n):
yield ''.join([str(i) for _ in range(j)])
# Check incrementing sequences
for i in range(n):
# Starting at 0
seq = ''.join([str(j) for j in range(i)])
yield seq
# Starting at 1
yield seq[1:]
# Decrementing
# Starting at 0
yield seq[::-1]
# Starting at 1
yield seq[1::-1]
common_passwords = [
'password', 'qwerty', 'iloveyou', '123123', 'abc123', 'admin',
'passwrod', 'password1', 'beelance', 'beelance2'
]
if password in common_passwords or password in common_sequences(12):
return True
return False
def password_weakness(password, username):
"""
Check if the password fulfills the password policy.
The policy is:
- At least 8 characters, but not more than 70 (due to bcrypt)
- Does not overlap with the username
- Not a common password
:param password: The password to check
:param username: The username of the user (used to check similarity)
:return: The most important weakness of the password, or None if it fulfills the policy
"""
if len(password) < 8:
return "The password must be at least 5 characters long."
elif len(password) > 70:
return "The password can't be longer than 70 characters."
elif password in username or username in password:
return "The password can't overlap with your username."
elif is_common_password(password):
return "The password is too common. Choose something more unique."
return None
def hash_password(password):
"""
Create a hash of the given password, with a random salt.
:param password: The password to hash
:return: The generated hash
"""
return bcrypt.hashpw(password.encode('UTF-8'), bcrypt.gensalt())
def check_password(password, password_hash):
"""
Check if the entered password matches the hashed password.
:param password: The password to check
:param password_hash: The password hash to check against
:return: True if the password matches, or False if it doesn't
"""
return password and password_hash and bcrypt.checkpw(password.encode('UTF-8'), password_hash.encode('UTF-8'))
# Authenticator code copied from https://github.com/jakobsn/google-authenticator.
# The license for this code can be found in the file LICENSE.google-authenticator
def get_hotp_token(secret, intervals_no):
"""This is where the magic happens."""
key = base64.b32decode(normalize(secret), True) # True is to fold lower into uppercase
msg = struct.pack(">Q", intervals_no)
h = bytearray(hmac.new(key, msg, hashlib.sha1).digest())
o = h[19] & 15
h = str((struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000)
return prefix0(h)
def get_totp_token(secret):
"""The TOTP token is just a HOTP token seeded with every 30 seconds."""
return get_hotp_token(secret, intervals_no=int(time.time())//30)
def normalize(key):
"""Normalizes secret by removing spaces and padding with = to a multiple of 8"""
k2 = key.strip().replace(' ', '')
# k2 = k2.upper() # skipped b/c b32decode has a foldcase argument
if len(k2) % 8 != 0:
k2 += '=' * (8 - len(k2) % 8)
return k2
def prefix0(h):
"""Prefixes code with leading zeros if missing."""
if len(h) < 6:
h = '0'*(6-len(h)) + h
return h
# End of code copied from https://github.com/jakobsn/google-authenticator.
def generate_authenticator_secret():
length = 16
alphabet = string.ascii_uppercase
return ''.join(random.SystemRandom().choice(alphabet) for _ in range(length))