package io.fathom.cloud.identity.secrets; import io.fathom.cloud.CloudException; import io.fathom.cloud.identity.Users; import io.fathom.cloud.identity.model.AuthenticatedProject; import io.fathom.cloud.identity.model.AuthenticatedUser; import io.fathom.cloud.identity.secrets.SecretToken.SecretTokenType; import io.fathom.cloud.identity.state.AuthRepository; import io.fathom.cloud.openstack.client.identity.ChallengeResponses; import io.fathom.cloud.protobuf.CloudCommons.EncryptedWith; import io.fathom.cloud.protobuf.CloudCommons.SecretData; import io.fathom.cloud.protobuf.CloudCommons.TokenInfo; import io.fathom.cloud.protobuf.IdentityModel.ClientAppSecretData; import io.fathom.cloud.protobuf.IdentityModel.CredentialData; import io.fathom.cloud.protobuf.IdentityModel.KeyData; import io.fathom.cloud.protobuf.IdentityModel.ProjectData; import io.fathom.cloud.protobuf.IdentityModel.ProjectRoles; import io.fathom.cloud.protobuf.IdentityModel.ProjectRolesSecretData; import io.fathom.cloud.protobuf.IdentityModel.SecretKeyData; import io.fathom.cloud.protobuf.IdentityModel.SecretKeyType; import io.fathom.cloud.protobuf.IdentityModel.SecretStoreData; import io.fathom.cloud.protobuf.IdentityModel.UserData; import io.fathom.cloud.protobuf.IdentityModel.UserSecretData; import io.fathom.cloud.server.auth.SharedKeystore; import io.fathom.cloud.server.resources.ClientCertificate; import java.io.IOException; import javax.inject.Inject; import javax.inject.Singleton; import org.keyczar.AesKey; import org.keyczar.Crypter; import org.keyczar.Encrypter; import org.keyczar.HmacKey; import org.keyczar.KeyczarReaderWrapper; import org.keyczar.KeyczarUtils; import org.keyczar.RsaPrivateKey; import org.keyczar.RsaPublicKey; import org.keyczar.exceptions.KeyczarException; import org.keyczar.interfaces.KeyczarReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fathomdb.crypto.AesCbcCryptoKey; import com.fathomdb.crypto.CryptoKey; import com.fathomdb.crypto.FathomdbCrypto; import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.protobuf.ByteString; import com.google.protobuf.GeneratedMessage; import com.google.protobuf.InvalidProtocolBufferException; @Singleton public class Secrets { private static final Logger log = LoggerFactory.getLogger(Secrets.class); public static final String KEY_TOKEN_ENCRYPT = "token_encrypt"; public static final String KEY_FORGOT_PASSWORD_PUBLIC = "forgot_password_public"; @Inject AuthRepository authRepository; @Inject SharedKeystore keystore; @Inject Migrations migrations; public ByteString buildTokenSecret(AuthenticatedUser user) { AesKey userKey = user.getKeys().getSecretToken().cryptoKey; AesKey tokenKey = userKey; byte[] plaintext = KeyczarUtils.pack(tokenKey); // TODO: Key rotation byte[] tokenKeySerialized; try { tokenKeySerialized = getCrypter().encrypt(plaintext); } catch (KeyczarException e) { throw new IllegalStateException(); } return ByteString.copyFrom(tokenKeySerialized); } Crypter crypter; private synchronized Crypter getCrypter() { if (crypter == null) { crypter = keystore.buildCrypter(KEY_TOKEN_ENCRYPT); } return crypter; } public UserWithSecret getFromToken(UserData user, TokenInfo token) throws KeyczarException { byte[] tokenKeySerialized; try { ByteString tokenSecret = token.getTokenSecret(); tokenKeySerialized = getCrypter().decrypt(tokenSecret.toByteArray()); } catch (KeyczarException e) { // This should have been validated log.warn("Error decrypting user key"); return null; } AesKey tokenKey = KeyczarUtils.unpack(tokenKeySerialized); // We could have extra layers here, but I don't think they achieve // anything AesKey userKey = tokenKey; SecretToken secretToken = new SecretToken(SecretTokenType.USER_SECRET, userKey, null); return checkSecret(user, secretToken); } public UserWithSecret checkPassword(UserData user, CredentialData credential, String password) { // if (user.getName().equals("admin")) { // try { // user = hotfixDomainAdmin(user); // } catch (Exception e) { // throw new IllegalStateException("Error migrating user", e); // } // } if (credential.hasDeprecatedPasswordHash()) { // TODO: We need to remove these log.warn("Credential still has deprecated password hash"); try { user = migrations.migrateUser(credential, user, password); } catch (Exception e) { throw new IllegalStateException("Error migrating user", e); } } SecretToken secretToken = null; try { secretToken = getSecretFromPassword(user.getSecretStore(), password); } catch (KeyczarException e) { log.info("Error while checking password", e); } if (secretToken == null) { return null; } UserWithSecret ret = checkSecret(user, secretToken); if (ret != null) { try { ret = migrations.migrateUser(ret, password, secretToken); } catch (Exception e) { throw new IllegalStateException("Error migrating user", e); } } if (ret != null) { try { ret = migrations.migrateUserAddPasswordRecovery(ret, secretToken); } catch (Exception e) { throw new IllegalStateException("Error migrating user", e); } } return ret; } public UserWithSecret checkPasswordRecovery(UserData user, CredentialData credential, KeyczarReader recoveryKey) { SecretToken secretToken = null; try { secretToken = getSecretWithRecovery(user.getSecretStore(), recoveryKey); } catch (KeyczarException e) { log.info("Error while checking recovery key", e); } if (secretToken == null) { return null; } UserWithSecret ret = checkSecret(user, secretToken); return ret; } // private UserData hotfixDomainAdmin(UserData user) throws CloudException { // if (user.getDomainRolesCount() != 0) { // return user; // } // // log.warn("Hotfixing admin user with domain admin role"); // // UserData.Builder b = UserData.newBuilder(user); // // DomainRoles.Builder rb = b.addDomainRolesBuilder(); // rb.setDomain(user.getDomainId()); // rb.addRole(WellKnownRoles.ROLE_ID_ADMIN); // // user = authRepository.getUsers().update(b); // // return user; // } private UserWithSecret checkSecret(UserData user, SecretToken secretToken) { SecretData secretData = user.getSecretData(); if (secretData.hasEncryptedWith() && secretData.getEncryptedWith() != EncryptedWith.SECRET_KEY) { throw new IllegalStateException(); } UserSecretData userSecretData; try { userSecretData = unlock(secretData, secretToken, UserSecretData.newBuilder()); } catch (IOException e) { log.debug("Error decoding secret data (likely because of wrong password)"); return null; } // This stuff is deprecated because we sign the secret data if (userSecretData.hasDeprecatedVerifyPublicKey()) { // throw new IllegalStateException(); log.warn("UserSecretData has deprecated verify public key"); // // long hash = // Hashing.md5().hashBytes(user.getPublicKey().getEncoded().toByteArray()).asLong(); // if (userSecretData.getDeprecatedVerifyPublicKey() != hash) { // // This is unexpected // log.warn("Verify public key did not match (but decode did not fail)"); // return null; // } } else if (userSecretData.hasDeprecatedVerifyUserId()) { log.warn("UserSecretData has deprecated verify user id"); // // This is deprecated because it makes creation painful. A // // pretty crappy reason, really. // log.warn("Falling back to deprecated user id validation for user: {}", // user.getName()); // // if (userSecretData.getDeprecatedVerifyUserId() != user.getId()) { // // This is unexpected // log.warn("VerifyUserId did not match (but decode did not fail)"); // return null; // } } return new UserWithSecret(user, userSecretData, secretToken); } public AuthenticatedProject authenticate(ProjectData project, AuthenticatedUser user) { ProjectRoles projectRoles = Users.findProjectRoles(user.getUserData(), project.getId()); if (projectRoles == null) { // TODO: We probably need another path for domain admins return null; } if (!projectRoles.hasSecretData()) { throw new IllegalStateException("Project role has no secret data"); } ProjectRolesSecretData projectRolesSecretData; try { projectRolesSecretData = unlock(projectRoles.getSecretData(), user, ProjectRolesSecretData.newBuilder()); } catch (IOException e) { throw new IllegalStateException("Error unlocking project", e); } int version = 0; if (projectRolesSecretData.hasProjectKeyVersion()) { version = projectRolesSecretData.getProjectKeyVersion(); } ByteString projectKeyBytes = projectRolesSecretData.getProjectKey(); if (projectKeyBytes == null) { throw new IllegalStateException(); } if (version == 1) { AesKey projectKey; try { projectKey = KeyczarUtils.unpack(projectKeyBytes.toByteArray()); } catch (KeyczarException e) { throw new IllegalStateException("Error reading project key", e); } SecretToken token = new SecretToken(SecretTokenType.PROJECT_SECRET, projectKey, null); return new AuthenticatedProject(project, token); } else if (version == 0) { // We had a project key in version 0, but we didn't use it! return new AuthenticatedProject(project, null); } else { throw new IllegalStateException(); } } public static SecretData buildProjectRolesSecret(UserData user, AuthenticatedProject authenticatedProject) throws KeyczarException { byte[] plaintext; { ProjectRolesSecretData.Builder b = ProjectRolesSecretData.newBuilder(); byte[] serialized = KeyczarUtils.pack(authenticatedProject.getKeys().getSecretToken().cryptoKey); b.setProjectKey(ByteString.copyFrom(serialized)); b.setProjectKeyVersion(1); plaintext = b.build().toByteArray(); } Encrypter publicKeyEncrypter = getPublicKeyEncrypter(user.getPublicKey()); byte[] ciphertext = publicKeyEncrypter.encrypt(plaintext); SecretData.Builder s = SecretData.newBuilder(); s.setCiphertext(ByteString.copyFrom(ciphertext)); s.setEncryptedWith(EncryptedWith.PUBLIC_KEY); s.setVersion(1); return s.build(); } public static SecretData buildUserSecret(SecretToken userSecret, UserSecretData data) { byte[] plaintext = data.toByteArray(); // CryptoKey userKey = userSecret.cryptoKey; byte[] ciphertext = userSecret.encrypt(plaintext); SecretData.Builder s = SecretData.newBuilder(); s.setCiphertext(ByteString.copyFrom(ciphertext)); s.setEncryptedWith(EncryptedWith.SECRET_KEY); s.setVersion(1); return s.build(); } public static SecretData buildClientAppSecret(SecretToken secretToken, ClientAppSecretData data) { byte[] plaintext = data.toByteArray(); byte[] ciphertext = secretToken.encrypt(plaintext); SecretData.Builder s = SecretData.newBuilder(); s.setCiphertext(ByteString.copyFrom(ciphertext)); s.setEncryptedWith(EncryptedWith.SECRET_KEY); s.setVersion(1); return s.build(); } public static <T extends GeneratedMessage> T unlockWithSecretKey(ByteString secured, CryptoKey key, GeneratedMessage.Builder newBuilder) throws InvalidProtocolBufferException { byte[] plaintext = key.decrypt(secured.toByteArray()); newBuilder.mergeFrom(plaintext); return (T) newBuilder.build(); } // static <T extends GeneratedMessage> T unlockWithPrivateKey(ByteString // secured, PrivateKey key, // GeneratedMessage.Builder newBuilder) throws // InvalidProtocolBufferException { // byte[] plaintext = KeyPairs.decrypt(key, secured.toByteArray()); // newBuilder.mergeFrom(plaintext); // return (T) newBuilder.build(); // } static <T extends GeneratedMessage> T unlock(ByteString secured, Crypter crypter, GeneratedMessage.Builder newBuilder) throws IOException { byte[] plaintext; try { plaintext = crypter.decrypt(secured.toByteArray()); } catch (KeyczarException e) { throw new IOException("Error decrypting data", e); } newBuilder.mergeFrom(plaintext); return (T) newBuilder.build(); } public static <T extends GeneratedMessage> T unlock(ByteString secured, AesKey key, GeneratedMessage.Builder newBuilder) throws IOException { Crypter crypter = KeyczarUtils.buildCrypter(key); return unlock(secured, crypter, newBuilder); } static <T extends GeneratedMessage> T unlock(SecretData secured, AuthenticatedUser user, GeneratedMessage.Builder newBuilder) throws IOException { EncryptedWith encryptedWith = EncryptedWith.PUBLIC_KEY; if (secured.hasEncryptedWith()) { encryptedWith = secured.getEncryptedWith(); } int version = 0; if (secured.hasVersion()) { version = secured.getVersion(); } if (version == 0) { switch (encryptedWith) { case PUBLIC_KEY: // We could actually support this fairly easily... throw new IllegalStateException("Unwrapping with deprecated private key not supported"); // log.warn("Using deprecated private key for user: {}", user); // return unlockWithPrivateKey(secured.getCiphertext(), // user.getKeys().getDeprecatedPrivateKey(), // newBuilder); case SECRET_KEY: return unlockWithSecretKey(secured.getCiphertext(), user.getKeys().getSecretToken().getDeprecatedKey(), newBuilder); default: throw new IllegalArgumentException(); } } else if (version == 1) { Crypter crypter; switch (encryptedWith) { case PUBLIC_KEY: crypter = user.getKeys().getAsymetricCrypter(); break; case SECRET_KEY: crypter = user.getKeys().getSecretToken().getCrypter(); break; default: throw new IllegalArgumentException(); } return unlock(secured.getCiphertext(), crypter, newBuilder); } else { throw new IllegalArgumentException(); } } public static <T extends GeneratedMessage> T unlock(SecretData secured, SecretToken secretToken, GeneratedMessage.Builder newBuilder) throws IOException { EncryptedWith encryptedWith = EncryptedWith.PUBLIC_KEY; if (secured.hasEncryptedWith()) { encryptedWith = secured.getEncryptedWith(); } int version = 0; if (secured.hasVersion()) { version = secured.getVersion(); } if (version == 0) { try { switch (encryptedWith) { case PUBLIC_KEY: throw new IllegalArgumentException(); case SECRET_KEY: log.warn("Trying deprecated secret key"); return unlockWithSecretKey(secured.getCiphertext(), secretToken.getDeprecatedKey(), newBuilder); default: throw new IllegalArgumentException(); } } catch (Exception e) { log.warn("Error decrypting with v0, trying v1: {}", e.getMessage()); version = 1; } } if (version == 1) { Crypter crypter; switch (encryptedWith) { case PUBLIC_KEY: throw new IllegalArgumentException(); case SECRET_KEY: crypter = secretToken.getCrypter(); break; default: throw new IllegalArgumentException(); } return unlock(secured.getCiphertext(), crypter, newBuilder); } else { throw new IllegalArgumentException(); } } public static Encrypter getPublicKeyEncrypter(KeyData keyData) throws KeyczarException { if (keyData.hasKeyczar()) { RsaPublicKey key; try { key = KeyczarUtils.readRsaPublicKey(keyData.getKeyczar()); } catch (KeyczarException e) { throw new IllegalStateException("Error reading public key", e); } return new Encrypter(new KeyczarReaderWrapper(key)); } else { throw new IllegalStateException("Only deprecated public key available"); } // ByteString encoded = keyData.getEncoded(); // if (encoded == null) { // throw new IllegalArgumentException(); // } // // PublicKey publicKey = // KeyPairs.deserializePublicKey(encoded.toByteArray()); // // // return publicKey; } public static KeyczarReaderWrapper getPrivateKey(KeyData keyData) { if (keyData.hasKeyczar()) { RsaPrivateKey key; try { key = KeyczarUtils.readRsaPrivateKey(keyData.getKeyczar()); } catch (KeyczarException e) { throw new IllegalStateException("Error reading private key", e); } return new KeyczarReaderWrapper(key); } else { throw new IllegalStateException("Only deprecated private key available"); } // if keyczar // // // ByteString encoded = keyData.getEncoded(); // if (encoded == null) { // throw new IllegalArgumentException(); // } // // PrivateKey privateKey = // KeyPairs.deserializePrivateKey(encoded.toByteArray()); // return privateKey; } public void addPasswordAuth(UserData.Builder user, SecretToken userSecret, String password) { SecretStoreData.Builder secretStore = user.getSecretStoreBuilder(); if (Strings.isNullOrEmpty(password)) { throw new IllegalArgumentException(); } setPassword(secretStore, password, userSecret); } public void addPublicKeyAuth(UserData.Builder user, String credentialKey, ByteString challenge) { if (Strings.isNullOrEmpty(credentialKey)) { throw new IllegalArgumentException(); } if (challenge == null) { throw new IllegalArgumentException(); } SecretStoreData.Builder secretStore = user.getSecretStoreBuilder(); SecretKeyData.Builder b = getSecretKeyData(secretStore, SecretKeyType.ENCRYPTED_WITH_CREDENTIAL); b.setCredentialKey(credentialKey); b.setCiphertext(challenge); } public void addTokenRecovery(UserData.Builder user, SecretToken secret) { SecretStoreData.Builder secretStore = user.getSecretStoreBuilder(); SecretKeyData.Builder b = getSecretKeyData(secretStore, SecretKeyType.ENCRYPTED_WITH_FORGOTPASSWORD_PUBKEY); Encrypter encrypter = keystore.buildEncrypter(KEY_FORGOT_PASSWORD_PUBLIC); b.setVersion(1); byte[] serialized = KeyczarUtils.pack(secret.cryptoKey); byte[] ciphertext; try { ciphertext = encrypter.encrypt(serialized); } catch (KeyczarException e) { throw new IllegalStateException("Error encrypting key", e); } b.setCiphertext(ByteString.copyFrom(ciphertext)); } public static void storeLockedByProject(SecretStoreData.Builder data, AuthenticatedProject project, SecretToken secret) { SecretKeyData.Builder b = getSecretKeyData(data, SecretKeyType.ENCRYPTED_WITH_PROJECT_KEY); SecretToken secretToken = project.getKeys().getSecretToken(); if (secretToken.type != SecretTokenType.PROJECT_SECRET) { throw new IllegalArgumentException(); } Encrypter encrypter = secretToken.getCrypter(); b.setVersion(1); byte[] serialized = KeyczarUtils.pack(secret.cryptoKey); byte[] ciphertext; try { ciphertext = encrypter.encrypt(serialized); } catch (KeyczarException e) { throw new IllegalStateException("Error encrypting key", e); } b.setCiphertext(ByteString.copyFrom(ciphertext)); } private static SecretKeyData.Builder getSecretKeyData(SecretStoreData.Builder data, SecretKeyType secretKeyType) { for (SecretKeyData.Builder entry : data.getSecretKeyBuilderList()) { if (entry.getType() != secretKeyType) { continue; } return entry; } SecretKeyData.Builder b = data.addSecretKeyBuilder(); b.setType(secretKeyType); return b; } public static void setPassword(SecretStoreData.Builder data, String password, AuthenticatedUserKeys secret) { setPassword(data, password, secret.getSecretToken()); } public static void setPassword(SecretStoreData.Builder data, String password, SecretToken secret) { SecretKeyData.Builder b = getSecretKeyData(data, SecretKeyType.ENCRYPTED_WITH_USER_PASSWORD); // b.setUserId(value); b.setIterations(2000); { byte[] seed = KeyczarUtils.generateSecureRandom(16); b.setSeed(ByteString.copyFrom(seed)); } AesKey passwordKey = KeyczarUtils.deriveKey(b.getIterations(), b.getSeed().toByteArray(), password); b.setVersion(1); byte[] ciphertext = encryptSymetricKey(passwordKey, secret.cryptoKey); b.setCiphertext(ByteString.copyFrom(ciphertext)); } public static SecretToken getSecretWithRecovery(SecretStoreData data, KeyczarReader recoveryKey) throws KeyczarException { for (SecretKeyData entry : data.getSecretKeyList()) { if (entry.getType() != SecretKeyType.ENCRYPTED_WITH_FORGOTPASSWORD_PUBKEY) { continue; } int version = 0; if (entry.hasVersion()) { version = entry.getVersion(); } AesKey aesKey; if (version == 1) { Crypter crypter = new Crypter(recoveryKey); byte[] plaintext; try { plaintext = crypter.decrypt(entry.getCiphertext().toByteArray()); } catch (KeyczarException e) { throw new IllegalStateException("Error decrypting token", e); } aesKey = KeyczarUtils.unpack(plaintext); } else { throw new IllegalStateException(); } return new SecretToken(SecretTokenType.USER_SECRET, aesKey, null); } return null; } public static SecretToken getSecretFromPassword(SecretStoreData data, final String password) throws KeyczarException { for (SecretKeyData entry : data.getSecretKeyList()) { if (entry.getType() != SecretKeyType.ENCRYPTED_WITH_USER_PASSWORD) { continue; } // if (entry.getUserId() != userId) { // continue; // } int version = 0; if (entry.hasVersion()) { version = entry.getVersion(); } CryptoKey v0; AesKey v1; if (version == 0) { AesCbcCryptoKey passwordKey = AesCbcCryptoKey.deriveKey(entry.getIterations(), entry.getSeed() .toByteArray(), password); byte[] plaintext = FathomdbCrypto.decrypt(passwordKey, entry.getCiphertext().toByteArray()); v0 = FathomdbCrypto.deserializeKey(plaintext); HmacKey hmacKey = KeyczarUtils.deriveHmac(plaintext, entry.getSeed().toByteArray(), password); v1 = new AesKey(((AesCbcCryptoKey) v0).getJce().getEncoded(), hmacKey); } else if (version == 1) { AesKey passwordKey = KeyczarUtils.deriveKey(entry.getIterations(), entry.getSeed().toByteArray(), password); byte[] plaintext = KeyczarUtils.decrypt(passwordKey, entry.getCiphertext().toByteArray()); v1 = KeyczarUtils.unpack(plaintext); v0 = null; } else { throw new IllegalStateException(); } return new SecretToken(SecretTokenType.USER_SECRET, v1, v0); } return null; } // private static CryptoKey decryptSymetricKey(CryptoKey key, ByteString // ciphertext) { // return toSecretKey(FathomdbCrypto.decrypt(key, // ciphertext.toByteArray())); // } // // private static CryptoKey toSecretKey(byte[] keyData) { // return FathomdbCrypto.deserializeKey(keyData); // } private static byte[] encryptSymetricKey(AesKey passwordKey, AesKey secret) { byte[] serialized = KeyczarUtils.pack(secret); return KeyczarUtils.encrypt(passwordKey, serialized); } public UserWithSecret checkPublicKey(UserData user, CredentialData credential, ClientCertificate clientCertificate, ByteString challenge, ByteString responseData) { AesKey secretKey; if (!ChallengeResponses.hasPrefix(responseData.toByteArray())) { log.warn("Challenge response was not valid"); return null; } byte[] payload = ChallengeResponses.getPayload(responseData.toByteArray()); try { secretKey = KeyczarUtils.unpack(payload); } catch (KeyczarException e) { log.warn("Error unpacking key", e); return null; } SecretToken secretToken = new SecretToken(SecretTokenType.USER_SECRET, secretKey, null); // if (secretToken == null) { // return null; // } UserWithSecret ret = checkSecret(user, secretToken); return ret; } public ByteString buildAuthChallenge(UserData user, CredentialData credential, ClientCertificate clientCertificate) { for (SecretKeyData secretKey : user.getSecretStore().getSecretKeyList()) { SecretKeyType type = secretKey.getType(); if (type != SecretKeyType.ENCRYPTED_WITH_CREDENTIAL) { continue; } if (!Objects.equal(secretKey.getCredentialKey(), credential.getKey())) { continue; } ByteString ciphertext = secretKey.getCiphertext(); // TODO: Encrypt?? I think this would need a commutative encryption // algorithm. // We could also return a pair of challenges, one repeated, one not. return ByteString.copyFrom(ChallengeResponses.addHeader(ciphertext.toByteArray())); } log.warn("Unable to build auth challenge for credential: {}", credential); return null; } public void changePassword(UserData user, CredentialData credential, String password, KeyczarReader recoveryKey) throws CloudException { if (Strings.isNullOrEmpty(password)) { throw new IllegalArgumentException(); } UserWithSecret userWithSecret = checkPasswordRecovery(user, credential, recoveryKey); if (userWithSecret == null) { throw new IllegalArgumentException(); } UserData.Builder b = UserData.newBuilder(user); SecretStoreData.Builder secretStore = b.getSecretStoreBuilder(); Secrets.setPassword(secretStore, password, userWithSecret.getSecretToken()); UserSecretData.Builder s = UserSecretData.newBuilder(userWithSecret.userSecretData); b.setSecretData(Secrets.buildUserSecret(userWithSecret.getSecretToken(), s.build())); user = authRepository.getUsers().update(b); } }