mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-04-03 00:07:05 +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:
parent
a5ebd07549
commit
100acb119b
@ -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 ) )
|
||||
|
@ -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'])
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)"
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user