diff --git a/conf/mfa-totp.schema b/conf/mfa-totp.schema index 73b4ac6c..a1c29baf 100644 --- a/conf/mfa-totp.schema +++ b/conf/mfa-totp.schema @@ -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 ) ) diff --git a/management/mfa.py b/management/mfa.py index 84659c79..607f200f 100644 --- a/management/mfa.py +++ b/management/mfa.py @@ -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']) diff --git a/management/mfa_totp.py b/management/mfa_totp.py index 3bea84e0..cd73937e 100644 --- a/management/mfa_totp.py +++ b/management/mfa_totp.py @@ -1,35 +1,38 @@ # -*- 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 = [] @@ -37,7 +40,7 @@ def get_state(user): # 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: diff --git a/setup/ldap.sh b/setup/ldap.sh index 6db578a0..f86a67a5 100755 --- a/setup/ldap.sh +++ b/setup/ldap.sh @@ -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 diff --git a/tests/suites/_ldap-functions.sh b/tests/suites/_ldap-functions.sh index 80728807..1d34b63a 100644 --- a/tests/suites/_ldap-functions.sh +++ b/tests/suites/_ldap-functions.sh @@ -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)" diff --git a/tests/suites/ldap-access.sh b/tests/suites/ldap-access.sh index 4897fe11..1c2393ff 100644 --- a/tests/suites/ldap-access.sh +++ b/tests/suites/ldap-access.sh @@ -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)