password.pwm.util.operations.OtpService.java Source code

Java tutorial

Introduction

Here is the source code for password.pwm.util.operations.OtpService.java

Source

/*
 * Password Management Servlets (PWM)
 * http://www.pwm-project.org
 *
 * Copyright (c) 2006-2009 Novell, Inc.
 * Copyright (c) 2009-2017 The PWM Project
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package password.pwm.util.operations;

import com.novell.ldapchai.exception.ChaiUnavailableException;
import org.apache.commons.codec.binary.Base32;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.bean.SessionLabel;
import password.pwm.bean.UserIdentity;
import password.pwm.config.Configuration;
import password.pwm.config.PwmSetting;
import password.pwm.config.option.DataStorageMethod;
import password.pwm.config.option.OTPStorageFormat;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmException;
import password.pwm.error.PwmOperationalException;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.health.HealthRecord;
import password.pwm.http.PwmSession;
import password.pwm.ldap.LdapOperationsHelper;
import password.pwm.svc.PwmService;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.StringUtil;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.macro.MacroMachine;
import password.pwm.util.operations.otp.DbOtpOperator;
import password.pwm.util.operations.otp.LdapOtpOperator;
import password.pwm.util.operations.otp.LocalDbOtpOperator;
import password.pwm.util.operations.otp.OTPUserRecord;
import password.pwm.util.operations.otp.OtpOperator;
import password.pwm.util.operations.otp.PasscodeGenerator;
import password.pwm.util.secure.PwmRandom;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.Serializable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * @author Menno Pieters, Jason D. Rivard
 */
public class OtpService implements PwmService {

    private static final PwmLogger LOGGER = PwmLogger.forClass(OtpService.class);

    private final Map<DataStorageMethod, OtpOperator> operatorMap = new EnumMap<>(DataStorageMethod.class);
    private PwmApplication pwmApplication;
    private OtpSettings settings;

    public OtpService() {
    }

    @Override
    public void init(final PwmApplication pwmApplication) throws PwmException {
        this.pwmApplication = pwmApplication;
        operatorMap.put(DataStorageMethod.LDAP, new LdapOtpOperator(pwmApplication));
        operatorMap.put(DataStorageMethod.LOCALDB, new LocalDbOtpOperator(pwmApplication));
        operatorMap.put(DataStorageMethod.DB, new DbOtpOperator(pwmApplication));
        settings = OtpSettings.fromConfig(pwmApplication.getConfig());
    }

    public boolean validateToken(final PwmSession pwmSession, final UserIdentity userIdentity,
            final OTPUserRecord otpUserRecord, final String userInput, final boolean allowRecoveryCodes)
            throws PwmOperationalException, PwmUnrecoverableException {
        boolean otpCorrect = false;
        try {
            final Base32 base32 = new Base32();
            final byte[] rawSecret = base32.decode(otpUserRecord.getSecret());
            final Mac mac = Mac.getInstance("HMACSHA1");
            mac.init(new SecretKeySpec(rawSecret, ""));
            final PasscodeGenerator generator = new PasscodeGenerator(mac, settings.getOtpTokenLength(),
                    settings.getTotpIntervalSeconds());
            switch (otpUserRecord.getType()) {
            case TOTP:
                otpCorrect = generator.verifyTimeoutCode(userInput, settings.getTotpPastIntervals(),
                        settings.getTotpFutureIntervals());
                break;

            //@todo HOTP implementation

            default:
                throw new UnsupportedOperationException("OTP type not supported: " + otpUserRecord.getType());
            }
        } catch (Exception e) {
            LOGGER.error(pwmSession.getLabel(), "error checking otp secret: " + e.getMessage());
        }

        if (!otpCorrect && allowRecoveryCodes && otpUserRecord.getRecoveryCodes() != null
                && otpUserRecord.getRecoveryInfo() != null) {
            final OTPUserRecord.RecoveryInfo recoveryInfo = otpUserRecord.getRecoveryInfo();
            final String userHashedInput = doRecoveryHash(userInput, recoveryInfo);
            for (final OTPUserRecord.RecoveryCode code : otpUserRecord.getRecoveryCodes()) {
                if (code.getHashCode().equals(userInput) || code.getHashCode().equals(userHashedInput)) {
                    if (code.isUsed()) {
                        throw new PwmOperationalException(PwmError.ERROR_OTP_RECOVERY_USED,
                                "recovery code has been previously used");
                    }

                    code.setUsed(true);
                    try {
                        pwmApplication.getOtpService().writeOTPUserConfiguration(null, userIdentity, otpUserRecord);
                    } catch (ChaiUnavailableException e) {
                        throw new PwmUnrecoverableException(
                                new ErrorInformation(PwmError.ERROR_WRITING_OTP_SECRET, e.getMessage()));
                    }
                    otpCorrect = true;
                }
            }
        }

        return otpCorrect;
    }

    private List<String> createRawRecoveryCodes(final int numRecoveryCodes, final SessionLabel sessionLabel)
            throws PwmUnrecoverableException {
        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific(pwmApplication, sessionLabel);
        final String configuredTokenMacro = settings.getRecoveryTokenMacro();
        final List<String> recoveryCodes = new ArrayList<>();
        while (recoveryCodes.size() < numRecoveryCodes) {
            final String code = macroMachine.expandMacros(configuredTokenMacro);
            recoveryCodes.add(code);
        }
        return recoveryCodes;
    }

    public List<String> initializeUserRecord(final OTPUserRecord otpUserRecord, final SessionLabel sessionLabel,
            final String identifier) throws IOException, PwmUnrecoverableException {
        otpUserRecord.setIdentifier(identifier);

        final byte[] rawSecret = generateSecret();
        final String otpEncodedSecret = StringUtil.base32Encode(rawSecret);
        otpUserRecord.setSecret(otpEncodedSecret);

        switch (settings.getOtpType()) {
        case HOTP:
            otpUserRecord.setAttemptCount(PwmRandom.getInstance().nextLong());
            otpUserRecord.setType(OTPUserRecord.Type.HOTP);
            break;

        case TOTP:
            otpUserRecord.setType(OTPUserRecord.Type.TOTP);
            break;

        default:
            JavaHelper.unhandledSwitchStatement(settings.getOtpType());
        }
        final List<String> rawRecoveryCodes;
        if (settings.getOtpStorageFormat().supportsRecoveryCodes()) {
            rawRecoveryCodes = createRawRecoveryCodes(settings.getRecoveryCodesCount(), sessionLabel);
            final List<OTPUserRecord.RecoveryCode> recoveryCodeList = new ArrayList<>();
            final OTPUserRecord.RecoveryInfo recoveryInfo = new OTPUserRecord.RecoveryInfo();
            if (settings.getOtpStorageFormat().supportsHashedRecoveryCodes()) {
                LOGGER.trace(sessionLabel, "hashing the recovery codes");
                final int saltCharLength = Integer
                        .parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.OTP_SALT_CHARLENGTH));
                recoveryInfo.setSalt(PwmRandom.getInstance().alphaNumericString(saltCharLength));
                recoveryInfo.setHashCount(settings.getRecoveryHashIterations());
                recoveryInfo.setHashMethod(settings.getRecoveryHashMethod());
            } else {
                LOGGER.trace(sessionLabel, "not hashing the recovery codes");
                recoveryInfo.setSalt(null);
                recoveryInfo.setHashCount(0);
                recoveryInfo.setHashMethod(null);
            }
            otpUserRecord.setRecoveryInfo(recoveryInfo);
            for (final String rawCode : rawRecoveryCodes) {
                final String hashedCode;
                if (settings.getOtpStorageFormat().supportsHashedRecoveryCodes()) {
                    hashedCode = doRecoveryHash(rawCode, recoveryInfo);
                } else {
                    hashedCode = rawCode;
                }
                final OTPUserRecord.RecoveryCode recoveryCode = new OTPUserRecord.RecoveryCode();
                recoveryCode.setHashCode(hashedCode);
                recoveryCode.setUsed(false);
                recoveryCodeList.add(recoveryCode);
            }
            otpUserRecord.setRecoveryCodes(recoveryCodeList);
        } else {
            rawRecoveryCodes = new ArrayList<>();
        }
        return rawRecoveryCodes;
    }

    private static byte[] generateSecret() {
        final byte[] secArray = new byte[10];
        PwmRandom.getInstance().nextBytes(secArray);
        return secArray;
    }

    public String doRecoveryHash(final String input, final OTPUserRecord.RecoveryInfo recoveryInfo)
            throws IllegalStateException {
        final String algorithm = settings.getRecoveryHashMethod();
        final MessageDigest md;
        try {
            md = MessageDigest.getInstance(algorithm);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(
                    "unable to load " + algorithm + " message digest algorithm: " + e.getMessage());
        }

        final String raw = recoveryInfo.getSalt() == null ? input.trim()
                : recoveryInfo.getSalt().trim() + input.trim();

        final int hashCount = recoveryInfo.getHashCount();
        byte[] hashedBytes = raw.getBytes();
        for (int i = 0; i < hashCount; i++) {
            hashedBytes = md.digest(hashedBytes);
        }
        return StringUtil.base64Encode(hashedBytes);
    }

    @Override
    public STATUS status() {
        return STATUS.OPEN;
    }

    @Override
    public void close() {
        for (final OtpOperator operator : operatorMap.values()) {
            operator.close();
        }
        operatorMap.clear();
    }

    @Override
    public List<HealthRecord> healthCheck() {
        return Collections.emptyList();
    }

    public OTPUserRecord readOTPUserConfiguration(final SessionLabel sessionLabel, final UserIdentity userIdentity)
            throws PwmUnrecoverableException, ChaiUnavailableException {
        OTPUserRecord otpConfig = null;
        final Configuration config = pwmApplication.getConfig();
        final Date methodStartTime = new Date();

        final List<DataStorageMethod> otpSecretStorageLocations = config
                .getOtpSecretStorageLocations(PwmSetting.OTP_SECRET_READ_PREFERENCE);

        if (otpSecretStorageLocations != null) {
            final String userGUID = readGuidIfNeeded(pwmApplication, sessionLabel, otpSecretStorageLocations,
                    userIdentity);
            final Iterator<DataStorageMethod> locationIterator = otpSecretStorageLocations.iterator();
            while (otpConfig == null && locationIterator.hasNext()) {
                final DataStorageMethod location = locationIterator.next();
                final OtpOperator operator = operatorMap.get(location);
                if (operator != null) {
                    try {
                        otpConfig = operator.readOtpUserConfiguration(userIdentity, userGUID);
                    } catch (Exception e) {
                        LOGGER.error(sessionLabel, "unexpected error reading stored otp configuration from "
                                + location + " for user " + userIdentity + ", error: " + e.getMessage());
                    }
                } else {
                    LOGGER.warn(sessionLabel,
                            String.format("storage location %s not implemented", location.toString()));
                }
            }
        }

        LOGGER.trace(sessionLabel,
                "readOTPUserConfiguration completed in "
                        + TimeDuration.fromCurrent(methodStartTime).asCompactString()
                        + (otpConfig == null ? ", no otp record found"
                                : ", recordType=" + otpConfig.getType() + ", identifier="
                                        + otpConfig.getIdentifier() + ", timestamp="
                                        + JavaHelper.toIsoDate(otpConfig.getTimestamp())));
        return otpConfig;
    }

    public void writeOTPUserConfiguration(final PwmSession pwmSession, final UserIdentity userIdentity,
            final OTPUserRecord otp)
            throws PwmOperationalException, ChaiUnavailableException, PwmUnrecoverableException {
        int attempts = 0;
        int successes = 0;

        final Configuration config = pwmApplication.getConfig();
        final List<DataStorageMethod> otpSecretStorageLocations = config
                .getOtpSecretStorageLocations(PwmSetting.OTP_SECRET_READ_PREFERENCE);
        final String userGUID = readGuidIfNeeded(pwmApplication, pwmSession == null ? null : pwmSession.getLabel(),
                otpSecretStorageLocations, userIdentity);

        final StringBuilder errorMsgs = new StringBuilder();
        if (otpSecretStorageLocations != null) {
            for (final DataStorageMethod otpSecretStorageLocation : otpSecretStorageLocations) {
                attempts++;
                final OtpOperator operator = operatorMap.get(otpSecretStorageLocation);
                if (operator != null) {
                    try {
                        operator.writeOtpUserConfiguration(pwmSession, userIdentity, userGUID, otp);
                        successes++;
                    } catch (PwmUnrecoverableException e) {
                        LOGGER.error(pwmSession,
                                "error writing to " + otpSecretStorageLocation + ", error: " + e.getMessage());
                        errorMsgs.append(otpSecretStorageLocation).append(" error: ").append(e.getMessage());
                    }
                } else {
                    LOGGER.warn(pwmSession, String.format("storage location %s not implemented",
                            otpSecretStorageLocation.toString()));
                }
            }
        }

        if (attempts == 0) {
            final String errorMsg = "no OTP secret save methods are available or configured";
            final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_WRITING_OTP_SECRET, errorMsg);
            throw new PwmOperationalException(errorInfo);
        }

        if (attempts != successes) { // should be impossible to read here, but just in case.
            final String errorMsg = "OTP secret write only partially successful; attempts=" + attempts
                    + ", successes=" + successes + ", errors: " + errorMsgs.toString();
            final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_WRITING_OTP_SECRET, errorMsg);
            throw new PwmOperationalException(errorInfo);
        }
    }

    public void clearOTPUserConfiguration(final PwmSession pwmSession, final UserIdentity userIdentity)
            throws PwmOperationalException, ChaiUnavailableException, PwmUnrecoverableException {
        LOGGER.trace(pwmSession, "beginning clear otp user configuration");

        int attempts = 0;
        int successes = 0;

        final Configuration config = pwmApplication.getConfig();
        final List<DataStorageMethod> otpSecretStorageLocations = config
                .getOtpSecretStorageLocations(PwmSetting.OTP_SECRET_READ_PREFERENCE);

        final String userGUID = readGuidIfNeeded(pwmApplication, pwmSession.getLabel(), otpSecretStorageLocations,
                userIdentity);

        final StringBuilder errorMsgs = new StringBuilder();
        if (otpSecretStorageLocations != null) {
            for (final DataStorageMethod otpSecretStorageLocation : otpSecretStorageLocations) {
                attempts++;
                final OtpOperator operator = operatorMap.get(otpSecretStorageLocation);
                if (operator != null) {
                    try {
                        operator.clearOtpUserConfiguration(pwmSession, userIdentity, userGUID);
                        successes++;
                    } catch (PwmUnrecoverableException e) {
                        LOGGER.error(pwmSession,
                                "error clearing " + otpSecretStorageLocation + ", error: " + e.getMessage());
                        errorMsgs.append(otpSecretStorageLocation).append(" error: ").append(e.getMessage());
                    }
                } else {
                    LOGGER.warn(pwmSession, String.format("Storage location %s not implemented",
                            otpSecretStorageLocation.toString()));
                }
            }
        }

        if (attempts == 0) {
            final String errorMsg = "no OTP secret clear methods are available or configured";
            //@todo: replace error message
            final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_WRITING_OTP_SECRET, errorMsg);
            throw new PwmOperationalException(errorInfo);
        }

        if (attempts != successes) { // should be impossible to read here, but just in case.
            final String errorMsg = "OTP secret clearing only partially successful; attempts=" + attempts
                    + ", successes=" + successes + ", error: " + errorMsgs.toString();
            //@todo: replace error message
            final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_WRITING_OTP_SECRET, errorMsg);
            throw new PwmOperationalException(errorInfo);
        }
    }

    public OtpSettings getSettings() {
        return settings;
    }

    public ServiceInfoBean serviceInfo() {
        return new ServiceInfoBean(Collections.<DataStorageMethod>emptyList());
    }

    private static String readGuidIfNeeded(final PwmApplication pwmApplication, final SessionLabel sessionLabel,
            final Collection<DataStorageMethod> otpSecretStorageLocations, final UserIdentity userIdentity

    ) throws ChaiUnavailableException, PwmUnrecoverableException {
        final String userGUID;
        if (otpSecretStorageLocations.contains(DataStorageMethod.DB)
                || otpSecretStorageLocations.contains(DataStorageMethod.LOCALDB)) {
            userGUID = LdapOperationsHelper.readLdapGuidValue(pwmApplication, sessionLabel, userIdentity, false);
        } else {
            userGUID = null;
        }
        return userGUID;
    }

    public static class OtpSettings implements Serializable {
        private OTPStorageFormat otpStorageFormat;
        private OTPUserRecord.Type otpType = OTPUserRecord.Type.TOTP;
        private int recoveryCodesCount;
        private int totpPastIntervals;
        private int totpFutureIntervals;
        private int totpIntervalSeconds;
        private int otpTokenLength;
        private String recoveryTokenMacro;
        private int recoveryHashIterations;
        private String recoveryHashMethod;

        public OTPStorageFormat getOtpStorageFormat() {
            return otpStorageFormat;
        }

        public OTPUserRecord.Type getOtpType() {
            return otpType;
        }

        public int getRecoveryCodesCount() {
            return recoveryCodesCount;
        }

        public int getTotpPastIntervals() {
            return totpPastIntervals;
        }

        public int getTotpFutureIntervals() {
            return totpFutureIntervals;
        }

        public int getTotpIntervalSeconds() {
            return totpIntervalSeconds;
        }

        public int getOtpTokenLength() {
            return otpTokenLength;
        }

        public String getRecoveryTokenMacro() {
            return recoveryTokenMacro;
        }

        public int getRecoveryHashIterations() {
            return recoveryHashIterations;
        }

        public String getRecoveryHashMethod() {
            return recoveryHashMethod;
        }

        public static OtpSettings fromConfig(final Configuration config) {
            final OtpSettings otpSettings = new OtpSettings();

            otpSettings.otpStorageFormat = config.readSettingAsEnum(PwmSetting.OTP_SECRET_STORAGEFORMAT,
                    OTPStorageFormat.class);
            otpSettings.recoveryCodesCount = (int) config.readSettingAsLong(PwmSetting.OTP_RECOVERY_CODES);
            otpSettings.totpPastIntervals = Integer
                    .parseInt(config.readAppProperty(AppProperty.TOTP_PAST_INTERVALS));
            otpSettings.totpFutureIntervals = Integer
                    .parseInt(config.readAppProperty(AppProperty.TOTP_FUTURE_INTERVALS));
            otpSettings.totpIntervalSeconds = Integer.parseInt(config.readAppProperty(AppProperty.TOTP_INTERVAL));
            otpSettings.otpTokenLength = Integer.parseInt(config.readAppProperty(AppProperty.OTP_TOKEN_LENGTH));
            otpSettings.recoveryTokenMacro = config.readAppProperty(AppProperty.OTP_RECOVERY_TOKEN_MACRO);
            otpSettings.recoveryHashIterations = Integer
                    .parseInt(config.readAppProperty(AppProperty.OTP_RECOVERY_HASH_COUNT));
            otpSettings.recoveryHashMethod = config.readAppProperty(AppProperty.OTP_RECOVERY_HASH_METHOD);
            return otpSettings;
        }
    }
}