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' X-ORDERED 'VALUES'
EQUALITY caseExactIA5Match ) 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 # 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 # as a reminder of where the secret is stored when logging in (where
# the authenticator app is, that holds the secret). eg "my samsung # the authenticator app is, that holds the secret). eg "my samsung
# phone" # phone"
attributetype ( MiabLDAPmfaAttributeType:3 attributetype ( MiabLDAPmfaAttributeType:4
DESC 'TOTP device label' DESC 'TOTP device label'
NAME 'totpLabel' NAME 'totpLabel'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 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' DESC 'MiaB-LDAP TOTP settings for a user'
SUP top SUP top
AUXILIARY 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 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: if not user:
raise ValueError("User does not exist.") raise ValueError("User does not exist.")
strip_order_prefix(user, ['totpSecret','totpMruToken','totpLabel']) strip_order_prefix(user, ['totpSecret','totpMruToken','totpLabel'])

View File

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

View File

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

View File

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

View File

@ -3,16 +3,16 @@
# Access assertions: # Access assertions:
# service accounts, except management: # service accounts, except management:
# can bind but not change passwords, including their own # 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 not write any user attributes, including shadowLastChange
# can read config subtree (permitted-senders, domains) # can read config subtree (permitted-senders, domains)
# no access to services subtree, except their own dn # no access to services subtree, except their own dn
# users: # users:
# can bind and change their own password # can bind and change their own password
# can read and change their own shadowLastChange # 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: # can read attributess of all users except:
# mailaccess, totpSecret, totpMruToken, totpLabel # mailaccess, totpSecret, totpMruToken, totpMruTokenTime, totpLabel
# no access to config subtree # no access to config subtree
# no access to services subtree # no access to services subtree
# other: # other:
@ -38,11 +38,11 @@ test_user_change_password() {
test_user_access() { 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 # 2. can read and change their own shadowLastChange
# 3. no access to config subtree # 3. no access to config subtree
# 4. no access to services 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" test_start "user-access"
@ -65,27 +65,27 @@ test_user_access() {
# test that alice can read her own attributes # test that alice can read her own attributes
assert_r_access "$alice_dn" "$alice_dn" "alice" read mail maildrop cn sn shadowLastChange 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 # 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 totpLabel assert_r_access "$alice_dn" "$alice_dn" "alice" no-read mailaccess totpSecret totpMruToken totpMruTokenTime totpLabel
# test that alice cannot change her own select attributes # test that alice cannot change her own select attributes
assert_w_access "$alice_dn" "$alice_dn" "alice" assert_w_access "$alice_dn" "$alice_dn" "alice"
# test that alice cannot change her own totpSecret, totpMruToken or totpLabel # 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" "totpLabel=x-phone" 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 # test that alice can read bob's attributes
assert_r_access "$bob_dn" "$alice_dn" "alice" read mail maildrop cn sn 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 # 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 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 # test that alice cannot change bob's select attributes
assert_w_access "$bob_dn" "$alice_dn" "alice" assert_w_access "$bob_dn" "$alice_dn" "alice"
# test that alice cannot change bob's attributes # 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 # 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 # 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 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 # 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 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 # 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"
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 fi
# service accounts can read config subtree (permitted-senders, domains) # service accounts can read config subtree (permitted-senders, domains)