1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2025-04-04 00:17:06 +00:00

Add a totpMruTokenTime value to record the time when the mru token was used

Use the totpMruTokenTime as the id to uniquely identify a totp entry
This commit is contained in:
downtownallday 2020-09-30 11:00:58 -04:00
parent a5ebd07549
commit 100acb119b
6 changed files with 61 additions and 38 deletions

View File

@ -31,13 +31,23 @@ attributetype ( MiabLDAPmfaAttributeType:2
X-ORDERED 'VALUES'
EQUALITY caseExactIA5Match )
# the time in nanoseconds since the epoch when the mru token was last
# used. the time will also be set when a new entry is created even if
# the corresponding mru token is blank
attributetype ( MiabLDAPmfaAttributeType:3
DESC 'TOTP last token used time'
NAME 'totpMruTokenTime'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
X-ORDERED 'VALUES'
EQUALITY integerMatch )
# The label is currently any text supplied by the user, which is used
# as a reminder of where the secret is stored when logging in (where
# the authenticator app is, that holds the secret). eg "my samsung
# phone"
attributetype ( MiabLDAPmfaAttributeType:3
attributetype ( MiabLDAPmfaAttributeType:4
DESC 'TOTP device label'
NAME 'totpLabel'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
@ -52,4 +62,4 @@ objectClass ( MiabLDAPmfaObjectClass:1
DESC 'MiaB-LDAP TOTP settings for a user'
SUP top
AUXILIARY
MUST ( totpSecret $ totpMruToken $ totpLabel ) )
MUST ( totpSecret $ totpMruToken $ totpMruTokenTime $ totpLabel ) )

View File

@ -38,7 +38,7 @@ def get_mfa_user(email, env, conn=None):
attributes
'''
user = find_mail_user(env, email, ['objectClass','totpSecret','totpMruToken','totpLabel'], conn)
user = find_mail_user(env, email, ['objectClass','totpSecret','totpMruToken','totpMruTokenTime','totpLabel'], conn)
if not user:
raise ValueError("User does not exist.")
strip_order_prefix(user, ['totpSecret','totpMruToken','totpLabel'])

View File

@ -1,43 +1,46 @@
# -*- indent-tabs-mode: t; tab-width: 4; python-indent-offset: 4; -*-
import hashlib
import base64
import hmac
import pyotp
import qrcode
import io
import os
import time
from mailconfig import open_database
def totp_id_from_index(user, index):
'''return the sha-256 hash of the corresponding totpSecret as the
unique id for the totp entry. use the hash and not the index
itself to ensure a change in the totp order does not cause an
unexpected change
def id_from_index(user, index):
'''return a unique id for the user's totp entry. the index itself
should be avoided to ensure a change in the order does not cause
an unexpected change.
'''
m = hashlib.sha256()
m.update(user['totpSecret'][index].encode("utf8"))
return 'totp:' + m.hexdigest()
return 'totp:' + user['totpMruTokenTime'][index]
def totp_index_from_id(user, id):
def index_from_id(user, id):
'''return the index of the corresponding id from the list of totp
entries for a user, or -1 if not found
'''
for index in range(0, len(user['totpSecret'])):
xid = totp_id_from_index(user, index)
xid = id_from_index(user, index)
if xid == id:
return index
return -1
def time_ns():
if "time_ns" in dir(time):
return time.time_ns()
else:
return int(time.time() * 1000000000)
def get_state(user):
state_list = []
# totp
for idx in range(0, len(user['totpSecret'])):
state_list.append({
'id': totp_id_from_index(user, idx),
'id': id_from_index(user, idx),
'type': 'totp',
'secret': user['totpSecret'][idx],
'mru_token': user['totpMruToken'][idx],
@ -55,6 +58,7 @@ def enable(user, secret, token, label, env):
mods = {
"totpSecret": user['totpSecret'].copy() + [secret],
"totpMruToken": user['totpMruToken'].copy() + [''],
"totpMruTokenTime": user['totpMruTokenTime'].copy() + [time_ns()],
"totpLabel": user['totpLabel'].copy() + [label or '']
}
if 'totpUser' not in user['objectClass']:
@ -68,13 +72,17 @@ def set_mru_token(user, id, token, env):
if 'totpUser' not in user['objectClass']: return
# ensure the id is valid
idx = totp_index_from_id(user, id)
idx = index_from_id(user, id)
if idx<0:
raise ValueError('MFA/totp mru index is out of range')
# store the token
mods = { "totpMruToken": user['totpMruToken'].copy() }
mods = {
"totpMruToken": user['totpMruToken'].copy(),
"totpMruTokenTime": user['totpMruTokenTime'].copy()
}
mods['totpMruToken'][idx] = token
mods['totpMruTokenTime'][idx] = time_ns()
conn = open_database(env)
conn.modify_record(user, mods)
@ -86,6 +94,7 @@ def disable(user, id, env):
mods = {
"objectClass": user["objectClass"].copy(),
"totpMruToken": None,
"totpMruTokenTime": None,
"totpSecret": None,
"totpLabel": None
}
@ -94,16 +103,18 @@ def disable(user, id, env):
else:
# Disable totp at the index specified
idx = totp_index_from_id(user, id)
idx = index_from_id(user, id)
if idx<0 or idx>=len(user['totpSecret']):
raise ValueError('MFA/totp mru index is out of range')
mods = {
"objectClass": user["objectClass"].copy(),
"totpMruToken": user["totpMruToken"].copy(),
"totpMruTokenTime": user["totpMruTokenTime"].copy(),
"totpSecret": user["totpSecret"].copy(),
"totpLabel": user["totpLabel"].copy()
}
mods["totpMruToken"].pop(idx)
mods["totpMruTokenTime"].pop(idx)
mods["totpSecret"].pop(idx)
mods["totpLabel"].pop(idx)
if len(mods["totpSecret"])==0:

View File

@ -575,7 +575,7 @@ apply_access_control() {
# service accounts (except management):
# can bind but not change passwords, including their own
# can read all attributes of all users but not userPassword,
# totpSecret, totpMruToken, or totpLabel
# totpSecret, totpMruToken, totpMruTokenTime, or totpLabel
# can read config subtree (permitted-senders, domains)
# no access to services subtree, except their own dn
# management service account:
@ -584,8 +584,8 @@ apply_access_control() {
# users:
# can bind and change their own password
# can read and change their own shadowLastChange
# cannot read or modify totpSecret, totpMruToken, totpLabel
# can read attributess of other users except mailaccess, totpSecret, totpMruToken, totpLabel
# cannot read or modify totpSecret, totpMruToken, totpMruTokenTime, totpLabel
# can read attributess of other users except mailaccess, totpSecret, totpMruToken, totpMruTokenTime, totpLabel
# no access to config subtree
# no access to services subtree
#
@ -607,7 +607,7 @@ olcAccess: to attrs=userPassword
by self =wx
by anonymous auth
by * none
olcAccess: to attrs=totpSecret,totpMruToken,totpLabel
olcAccess: to attrs=totpSecret,totpMruToken,totpMruTokenTime,totpLabel
by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
by * none

View File

@ -47,12 +47,14 @@ create_user() {
local totpObjectClass=""
local totpSecret="$(awk -F, '{print $1}' <<< "$totpVal")"
local totpMruToken="$(awk -F, '{print $2}' <<< "$totpVal")"
local totpMruTokenTime=""
local totpLabel="$(awk -F, '{print $3}' <<< "$totpVal")"
if [ ! -z "$totpVal" ]; then
local nl=$'\n'
totpObjectClass="${nl}objectClass: totpUser"
totpSecret="${nl}totpSecret: {0}${totpSecret}"
totpMruToken="${nl}totpMruToken: {0}${totpMruToken}"
totpMruTokenTime="${nl}totpMruTokenTime: $(date +%s)0000000000"
totpLabel="${nl}totpLabel: {0}${totpLabel}"
fi
@ -67,7 +69,7 @@ sn: $localpart
displayName: $localpart
mail: $email
maildrop: $email
mailaccess: $priv${totpSecret}${totpMruToken}${totpLabel}
mailaccess: $priv${totpSecret}${totpMruToken}${totpMruTokenTime}${totpLabel}
userPassword: $(slappasswd_hash "$pass")
EOF
[ $? -ne 0 ] && die "Unable to add user $dn (as admin)"

View File

@ -3,16 +3,16 @@
# Access assertions:
# service accounts, except management:
# can bind but not change passwords, including their own
# can read all attributes of all users but not userPassword, totpSecret, totpMruToken, totpLabel
# can read all attributes of all users but not userPassword, totpSecret, totpMruTokenTime, totpMruToken, totpLabel
# can not write any user attributes, including shadowLastChange
# can read config subtree (permitted-senders, domains)
# no access to services subtree, except their own dn
# users:
# can bind and change their own password
# can read and change their own shadowLastChange
# no read or write access to user's own totpSecret, totpMruToken or totpLabel
# no read or write access to user's own totpSecret, totpMruToken, totpMruTokenTime or totpLabel
# can read attributess of all users except:
# mailaccess, totpSecret, totpMruToken, totpLabel
# mailaccess, totpSecret, totpMruToken, totpMruTokenTime, totpLabel
# no access to config subtree
# no access to services subtree
# other:
@ -38,11 +38,11 @@ test_user_change_password() {
test_user_access() {
# 1. can read attributess of all users except mailaccess, totpSecret, totpMruToken, totpLabel
# 1. can read attributess of all users except mailaccess, totpSecret, totpMruToken, totpMruTokenTime, totpLabel
# 2. can read and change their own shadowLastChange
# 3. no access to config subtree
# 4. no access to services subtree
# 5. no read or write access to own totpSecret, totpMruToken, or totpLabel
# 5. no read or write access to own totpSecret, totpMruToken, totpMruTokenTime, or totpLabel
test_start "user-access"
@ -65,27 +65,27 @@ test_user_access() {
# test that alice can read her own attributes
assert_r_access "$alice_dn" "$alice_dn" "alice" read mail maildrop cn sn shadowLastChange
# alice should not have access to her own mailaccess, totpSecret, totpMruToken or totpLabel, though
assert_r_access "$alice_dn" "$alice_dn" "alice" no-read mailaccess totpSecret totpMruToken totpLabel
# alice should not have access to her own mailaccess, totpSecret, totpMruToken, totpMruTokenTime or totpLabel, though
assert_r_access "$alice_dn" "$alice_dn" "alice" no-read mailaccess totpSecret totpMruToken totpMruTokenTime totpLabel
# test that alice cannot change her own select attributes
assert_w_access "$alice_dn" "$alice_dn" "alice"
# test that alice cannot change her own totpSecret, totpMruToken or totpLabel
assert_w_access "$alice_dn" "$alice_dn" "alice" no-write "totpSecret=ABC" "totpMruToken=123456" "totpLabel=x-phone"
# test that alice cannot change her own totpSecret, totpMruToken, totpMruTokenTime or totpLabel
assert_w_access "$alice_dn" "$alice_dn" "alice" no-write "totpSecret=ABC" "totpMruToken=123456" "totpMruTokenTime=123" "totpLabel=x-phone"
# test that alice can read bob's attributes
assert_r_access "$bob_dn" "$alice_dn" "alice" read mail maildrop cn sn
# alice should not have access to bob's mailaccess, totpSecret, totpMruToken, or totpLabel
assert_r_access "$bob_dn" "$alice_dn" "alice" no-read mailaccess totpSecret totpMruToken totpLabel
# alice should not have access to bob's mailaccess, totpSecret, totpMruToken, totpMruTokenTime, or totpLabel
assert_r_access "$bob_dn" "$alice_dn" "alice" no-read mailaccess totpSecret totpMruToken totpMruTokenTime totpLabel
# test that alice cannot change bob's select attributes
assert_w_access "$bob_dn" "$alice_dn" "alice"
# test that alice cannot change bob's attributes
assert_w_access "$bob_dn" "$alice_dn" "alice" no-write "totpSecret=ABC" "totpMruToken=123456" "totpLabel=x-phone"
assert_w_access "$bob_dn" "$alice_dn" "alice" no-write "totpSecret=ABC" "totpMruToken=123456" "totpMruTokenTime=345" "totpLabel=x-phone"
# test that alice cannot read a service account's attributes
@ -176,12 +176,12 @@ test_service_access() {
# check that service account can read user attributes
assert_r_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" read mail maildrop uid cn sn shadowLastChange
# service account should not be able to read user's userPassword, totpSecret, totpMruToken, or totpLabel
assert_r_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-read userPassword totpSecret totpMruToken totpLabel
# service account should not be able to read user's userPassword, totpSecret, totpMruToken, totpMruTokenTime, or totpLabel
assert_r_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-read userPassword totpSecret totpMruToken totpMruTokenTime totpLabel
# service accounts cannot change user attributes
assert_w_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD"
assert_w_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-write "shadowLastChange=1" "totpSecret=ABC" "totpMruToken=333333" "totpLabel=x-phone"
assert_w_access "$alice_dn" "$LDAP_POSTFIX_DN" "$LDAP_POSTFIX_PASSWORD" no-write "shadowLastChange=1" "totpSecret=ABC" "totpMruToken=333333" "totpMruTokenTime=123" "totpLabel=x-phone"
fi
# service accounts can read config subtree (permitted-senders, domains)