package com.intrbiz.bergamot.ui.security; import static com.intrbiz.balsa.BalsaContext.*; import java.nio.ByteBuffer; import java.security.Principal; import java.security.SecureRandom; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; import org.apache.log4j.Logger; import com.intrbiz.balsa.engine.impl.security.BaseTwoFactorSecurityEngine; import com.intrbiz.balsa.engine.impl.security.method.BackupCodeAuthenticationMethod; import com.intrbiz.balsa.engine.impl.security.method.HOTPAuthenticationMethod; import com.intrbiz.balsa.engine.impl.security.method.PasswordAuthenticationMethod; import com.intrbiz.balsa.engine.impl.security.method.TokenAuthenticationMethod; import com.intrbiz.balsa.error.BalsaSecurityException; import com.intrbiz.bergamot.data.BergamotDB; import com.intrbiz.bergamot.model.APIToken; import com.intrbiz.bergamot.model.Contact; import com.intrbiz.bergamot.model.ContactBackupCode; import com.intrbiz.bergamot.model.ContactHOTPRegistration; import com.intrbiz.bergamot.model.ContactU2FDeviceRegistration; import com.intrbiz.bergamot.model.Permission; import com.intrbiz.bergamot.model.SecuredObject; import com.intrbiz.bergamot.model.Site; import com.intrbiz.bergamot.ui.security.method.BergamotU2FAuthenticationMethod; import com.intrbiz.crypto.cookie.CookieBaker.Expires; import com.intrbiz.crypto.cookie.CryptoCookie; import com.intrbiz.data.DataException; import com.intrbiz.util.CounterHOTP.CounterHOTPState; import com.intrbiz.util.HOTP.HOTPState; import com.intrbiz.util.HOTPRegistration; import com.yubico.u2f.data.DeviceRegistration; public class BergamotSecurityEngine extends BaseTwoFactorSecurityEngine { private Logger logger = Logger.getLogger(BergamotSecurityEngine.class); private SecureRandom random = new SecureRandom(); public BergamotSecurityEngine() { super(); } @Override public String getEngineName() { return "Bergamot Security Engine"; } protected String getMetricsIntelligenceSourceName() { return "com.intrbiz.bergamot"; } /** * Override the authentication methods registered by default */ @Override protected void setupDefaultAuthenticationMethods() { this.registerAuthenticationMethod(new PasswordAuthenticationMethod()); this.registerAuthenticationMethod(new TokenAuthenticationMethod()); // register 2FA authentication methods this.registerAuthenticationMethod(new BergamotU2FAuthenticationMethod()); this.registerAuthenticationMethod(new HOTPAuthenticationMethod()); this.registerAuthenticationMethod(new BackupCodeAuthenticationMethod()); } @Override public boolean isTwoFactorAuthenticationRequiredForPrincipal(Principal principal) { return ((Contact) principal).isTwoFactorConfigured(); } @Override public boolean isValidPrincipal(Principal principal, ValidationLevel validationLevel) { if (validationLevel == ValidationLevel.STRONG) { // validate that the principal is in a good state return principal instanceof Contact && (! ((Contact) principal).getSite().isDisabled()) && (! (((Contact) principal).isForcePasswordChange() || ((Contact) principal).isLocked())); } return principal instanceof Contact && (! ((Contact) principal).getSite().isDisabled()); } @Override public byte[] tokenForPrincipal(Principal principal) { // we combine the UUID with a NONCE, // packed as: nonce1, nonce1 ^ msb, nonce2, nonce2 ^ lsb // this is a little obfuscation but mainly to reduce obvious // patterns byte[] token = new byte[32]; ByteBuffer bb = ByteBuffer.wrap(token); UUID id = ((Contact) principal).getId(); // a NONCE long nonce1 = this.random.nextLong(); long nonce2 = this.random.nextLong(); // high part bb.putLong(nonce1); bb.putLong(id.getMostSignificantBits() ^ nonce1); // low part bb.putLong(nonce2); bb.putLong(id.getLeastSignificantBits() ^ nonce2); // done return token; } @Override public Principal principalForToken(byte[] token) { ByteBuffer bb = ByteBuffer.wrap(token); // extract the data we need long nonce1 = bb.getLong(); long msb = bb.getLong(); long nonce2 = bb.getLong(); long lsb = bb.getLong(); // build the uuid UUID id = new UUID(nonce1 ^ msb, nonce2 ^ lsb); // lookup try (BergamotDB db = BergamotDB.connect()) { return db.getContact(id); } } @Override public Principal doPasswordLogin(String username, char[] password) throws BalsaSecurityException { try (BergamotDB db = BergamotDB.connect()) { logger.info("Authentication for principal: " + username + ", server: " + Balsa().request().getServerName()); logger.debug("Looking up site: " + Balsa().request().getServerName()); // lookup the site Site site = db.getSiteByName(Balsa().request().getServerName()); // validate if (site == null) { logger.error("Failed to determine the site for the server name: " + Balsa().request().getServerName() + ", authentication cannot continue."); throw new BalsaSecurityException("No such principal"); } // is the site disable if (site.isDisabled()) throw new BalsaSecurityException("Site is disabled"); // lookup the principal Contact contact = db.getContactByNameOrEmail(site.getId(), username); // does the username exist? if (contact == null) return null; // check the password if (! contact.verifyPassword(new String(password))) { logger.error("Password mismatch for principal " + username + " => " + site.getId() + "::" + contact.getId()); throw new BalsaSecurityException("Invalid password"); } // check if the account is locked if (contact.isLocked()) { logger.error("Rejecting valid login for principal " + username + " => " + site.getId() + " :: " + contact.getId() + " as the account has been locked."); throw new BalsaSecurityException("Account locked"); } return contact; } catch (DataException e) { logger.error("Cannot authenticate principal, database error", e); throw new BalsaSecurityException("Error authenticating principal"); } } @Override public void validateAccessToken(String token, CryptoCookie cookie, Principal principal, CryptoCookie.Flag[] requiredFlags) throws BalsaSecurityException { // validate the flags if (requiredFlags != null) { for (CryptoCookie.Flag flag : requiredFlags) { if (! cookie.isFlagSet(flag)) throw new BalsaSecurityException("The flag: " + flag.mask + " is missing from the access token"); } } // only validate perpetual API tokens if (cookie.getExpiryTime() == Expires.never()) { // lookup the API token in the database and validate that it exists and is not revoked try (BergamotDB db = BergamotDB.connect()) { APIToken apiToken = db.getAPIToken(token); if (apiToken == null) { logger.error("Invalid perpetual API token '" + token + "', it does not exist"); throw new BalsaSecurityException("Invalid perpetual API token"); } if ( ! ((Contact) principal).getId().equals(apiToken.getContactId())) { logger.error("Invalid perpetual API token '" + token + "', it does not match the Principal"); throw new BalsaSecurityException("Invalid perpetual API token"); } if (apiToken.isRevoked()) { logger.error("Invalid perpetual API token '" + token + "', it is revoked!"); throw new BalsaSecurityException("Invalid perpetual API token"); } } catch (DataException e) { logger.error("Cannot authenticate perpetual API token, database error", e); throw new BalsaSecurityException("Failed to validate perpetual API token", e); } } // account level checks // check if the account is locked Contact contact = (Contact) principal; if (contact.isLocked()) { logger.error("Rejecting valid token login for principal " + contact.getName() + " => " + contact.getSiteId() + " :: " + contact.getId() + " as the account has been locked."); throw new BalsaSecurityException("Account locked"); } // is the site disable if (contact.getSite().isDisabled()) throw new BalsaSecurityException("Site is disabled"); } /** * Check that the given principal has the given permission */ @Override public boolean check(Principal principal, String permission) { if (principal instanceof Contact) { Contact contact = (Contact) principal; return (! contact.getSite().isDisabled()) && contact.hasPermission(Permission.of(permission)); } return false; } /** * Check that the given principal has permission over the given object or object UUID */ @Override public boolean check(Principal principal, String permission, Object object) { if (principal instanceof Contact) { Contact contact = (Contact) principal; if (contact.getSite().isDisabled()) return false; if (object instanceof SecuredObject) { return contact.hasPermission(Permission.of(permission), (SecuredObject<?,?>) object); } else if (object instanceof UUID) { return contact.hasPermission(Permission.of(permission), (UUID) object); } } return false; } @Override public void verifyBackupCode(Principal principal, String backupCode) throws BalsaSecurityException { ContactBackupCode contactBackupCode = ((Contact) principal).getBackupCodes().stream() .filter((bc) -> bc.getCode().equals(backupCode)).findFirst().orElse(null); if (contactBackupCode == null || contactBackupCode.isUsed()) throw new BalsaSecurityException("The given backup code is invalid"); } @Override public void updateBackupCode(Principal principal, String backupCode) throws BalsaSecurityException { ContactBackupCode contactBackupCode = ((Contact) principal).getBackupCodes().stream() .filter((bc) -> bc.getCode().equals(backupCode)).findFirst().orElse(null); contactBackupCode.used(); // update in the DB try (BergamotDB db = BergamotDB.connect()) { db.setBackupCode(contactBackupCode); } } @Override public List<DeviceRegistration> getDeviceRegistrationsForPrincipal(Principal principal) throws BalsaSecurityException { return ((Contact) principal).getU2FDeviceRegistrations().stream() .map(ContactU2FDeviceRegistration::toDeviceRegistration).collect(Collectors.toList()); } @Override public String getAppIdForPrincipal(Principal principal) throws BalsaSecurityException { return ((Contact) principal).getSite().getU2FAppId(); } @Override public void validateDeviceRegistration(Principal principal, DeviceRegistration device) throws BalsaSecurityException { // all validations are handled by U2F } @Override public void updateDeviceRegistration(Principal principal, DeviceRegistration device) throws BalsaSecurityException { ContactU2FDeviceRegistration authenticatedUsing = ((Contact) principal).getU2FDeviceRegistrations().stream() .filter((d) -> d.getKeyHandle().equals(device.getKeyHandle()) && d.getPublicKey().equals(device.getPublicKey())) .findFirst().get(); // update the U2F state authenticatedUsing.used(device.getCounter()); // update in the DB try (BergamotDB db = BergamotDB.connect()) { db.setU2FDeviceRegistration(authenticatedUsing); } } @SuppressWarnings("unchecked") @Override public List<HOTPRegistration> getHOTPRegistrationsForPrincipal(Principal principal) throws BalsaSecurityException { return (List<HOTPRegistration>) (List<?>) ((Contact) principal).getHOTPRegistrations(); } @Override public void updateHOTPRegistration(Principal principal, HOTPRegistration registration, HOTPState nextState) throws BalsaSecurityException { ContactHOTPRegistration chr = (ContactHOTPRegistration) registration; chr.used((CounterHOTPState) nextState); // update in the DB try (BergamotDB db = BergamotDB.connect()) { db.setHOTPRegistration(chr); } } @Override public void validateHOTPRegistration(Principal principal, HOTPRegistration registration) throws BalsaSecurityException { if (((ContactHOTPRegistration) registration).isRevoked()) throw new BalsaSecurityException("The HOTP registration is revoked"); } }