# -*- coding: utf-8 -*-
"""The NOOP password module
Copyright © 2015-2016, Anders Andersen, The Arctic University of Norway.
See http://www.cs.uit.no/~aa/dist/tools/noop/COPYING (../COPYING) for
details.
"""
# Default service name in keyring
SERVICENAME = "noop.crypto.password"
# Default key size
SYMKEYSIZE = 256
# Import system modules
import sys, os #, base64
# Python 3 only!
assert (sys.version_info[0] > 2 and sys.version_info[1] > 3), \
"This NOOP module \"%s\" is Python 3.4 (or greater) only!" % (__name__,)
# Import modules
import keyring
from string import ascii_letters, digits, printable
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# Use SystemRandom to replace random.choice with random.SystemRandom().choice
from random import SystemRandom
# Load noop libraries (if available)
try:
from noop.core.signature import signature, one, opt
from noop.core.misc import idstr
except ImportError:
def signature(f): return f
class one:
def __init__(*args): pass
opt = one
def idstr(self=None): return __name__
# Default characters in passwords/names
PASSWDCHARS = ascii_letters + digits
PASSWDLENGTH = 16
SALTCHARS = printable.encode()
SALTLENGTH = PASSWDLENGTH
SALTEXT = ".salt"
@signature
def mkpasswd(
length:opt(int) = PASSWDLENGTH,
letters:opt(str) = PASSWDCHARS) -> str:
return "".join(SystemRandom().choice(letters) for i in range(length))
@signature
def mksalt(
length:opt(int) = SALTLENGTH,
letters:opt(bytes) = SALTCHARS) -> bytes:
return bytes([SystemRandom().choice(letters) for i in range(length)])
class Password:
R"""Create and manage password in the keychain
The `Password` class is used to store and access passwords in the
keychain. It is implemented using the `keyring` module.
Warning: You should not first create a temporary password and then
a persistent password with the same name. This might give
unexpected results since the password will be deleted from the
keychan when the first password object is deleted.
"""
@signature
def __init__(
self,
name:opt(str) = None,
passwd:opt(str) = None,
service:opt(str) = SERVICENAME,
persistent:opt(bool) = False,
hastobefresh:opt(bool) = False):
R"""Password constructor
Initialize the password with name, password and service. The
password in not stored as an attribute in the object. It is
only stored in the keychain on the computer. The password has
a name and is part of a service. A non-persistent password is
delted from the keychain when the object is deleted (this is
the default behavior). If the password is persistent and a
password with the same name allready exists, the default
behaviour is to use that password. This behaviour can be
overridden by the `hastobefresh` argument.
"""
# Save service name and if it is a temporary password
self.service = service
self.persistent = persistent
self.salt = None
# If the name is provided we use it, otherwise we generate a random name
if name:
self.name = name
# Another password with the same name in keychain?
if keyring.get_password(self.service, self.name) != None:
# This will not work for a temporary password
if not self.persistent:
raise ValueError(
idstr(self) + \
": Persistent password with this name exists.")
# If password has to be fresh
elif hastobefresh:
raise ValueError(
idstr(self) + \
": Non-fresh password with this name exists.")
# Update with new password
if passwd != None:
keyring.set_password(self.service, self.name, passwd)
# Check to see if the password has a salt
saltx = keyring.get_password(self.service, self.name + SALTEXT)
if saltx != None:
self.salt = saltx.encode()
# Store either provided or generated password in keychain
elif passwd != None:
keyring.set_password(self.service, self.name, passwd)
else:
keyring.set_password(self.service, self.name, mkpasswd())
# No name provided. Can we use a random name?
else:
# Only non-persistent password can have a random name
if not self.persistent:
self.name = mkpasswd()
# Find a random name that is not used (just in case...)
while keyring.get_password(self.service, self.name) != None:
self.name = mkpasswd()
# Store either provided or generated password in keychain
if passwd != None:
keyring.set_password(self.service, self.name, passwd)
else:
keyring.set_password(self.service, self.name, mkpasswd())
# A persistent password can not have a random name
else:
raise ValueError(
idstr(self) + \
": Only non-persistent password can have a random name.")
@signature
def __str__(self) -> str:
R"""Get password string
Fetches and returns the password from the keychain.
"""
if keyring.get_password(self.service, self.name):
return keyring.get_password(self.service, self.name)
else:
return ""
@signature
def __bytes__(self) -> bytes:
R"""Get password as a byte array
Fetches and returns the password as a byte array.
"""
if keyring.get_password(self.service, self.name):
return keyring.get_password(self.service, self.name).encode()
else:
return b""
@signature
def __del__(self):
R"""Delete object
If it is a temporary password the password is also deleted
from the keychain.
"""
if not self.persistent:
self.delete()
@signature
def delete(self):
R"""Delete object
Delete the password from the keychain.
"""
try:
keyring.delete_password(self.service, self.name)
except:
pass
if self.salt and self.persistent:
try:
keyring.delete_password(self.service, self.name + SALTEXT)
except:
pass
self.salt = None
@signature
def update(self, passwd:opt(str) = None):
R"""Update password
Change the password.
"""
if passwd != None:
keyring.set_password(self.service, self.name, passwd)
else:
keyring.set_password(self.service, self.name, mkpasswd())
@signature
def addsalt(
self,
salt:opt(bytes) = None,
override:opt(bool) = False):
R"""Add salt
Add salt to the password (used when generating key from password).
"""
if keyring.get_password(self.service, self.name) == None:
raise ValueError(
idstr(self) + ": Can not add salt: password not set.")
if self.salt and not override:
raise ValueError(
idstr(self) + ": Can not add salt: salt already set.")
if salt:
self.salt = salt
else:
self.salt = mksalt()
if self.persistent:
keyring.set_password(
self.service, self.name + SALTEXT, self.salt.decode())
@signature
def mkkey(self, keysize:opt(int) = SYMKEYSIZE) -> bytes:
R"""Create a key
Create a key from the password (and salt)
"""
if str(self) and self.salt:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=keysize//8,
salt=self.salt,
iterations=100000,
backend=default_backend())
return kdf.derive(bytes(self))
else:
raise ValueError(
idstr(self) + ": Can not create key: no password or salt.")
@signature
def mkiv(self, blocksize:opt(int) = algorithms.AES.block_size) -> bytes:
R"""Create IV
Create IV from the the salt.
"""
if str(self) and self.salt:
if len(self.salt) == blocksize//8:
return self.salt
else:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=blocksize//8,
salt=self.salt,
iterations=100000,
backend=default_backend())
return kdf.derive(self.salt)
else:
raise ValueError(
idstr(self) + ": Can not create iv: no password or salt.")