package io.fathom.cloud.identity.services; import io.fathom.cloud.CloudException; import io.fathom.cloud.identity.AuthServiceImpl; import io.fathom.cloud.identity.model.AuthenticatedProject; import io.fathom.cloud.identity.model.AuthenticatedUser; import io.fathom.cloud.identity.secrets.AppSecrets; import io.fathom.cloud.identity.secrets.SecretToken; import io.fathom.cloud.identity.secrets.Secrets; import io.fathom.cloud.identity.secrets.SecretToken.SecretTokenType; import io.fathom.cloud.identity.state.AuthRepository; import io.fathom.cloud.protobuf.CloudCommons.SecretData; import io.fathom.cloud.protobuf.IdentityModel.AttachmentData; import io.fathom.cloud.protobuf.IdentityModel.ClientAppData; import io.fathom.cloud.protobuf.IdentityModel.ClientAppSecretData; import io.fathom.cloud.protobuf.IdentityModel.SecretStoreData; import io.fathom.cloud.server.auth.Auth; import io.fathom.cloud.server.model.Project; import io.fathom.cloud.services.Attachments; import io.fathom.cloud.state.DuplicateValueException; import io.fathom.cloud.state.NamedItemCollection; import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response.Status; import org.keyczar.exceptions.KeyczarException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Strings; import com.google.inject.persist.Transactional; @Singleton @Transactional public class AttachmentsImpl implements Attachments { private static final Logger log = LoggerFactory.getLogger(AttachmentsImpl.class); @Inject AuthRepository authRepository; @Inject AppSecrets appSecrets; @Inject IdentityService identityService; @Inject AuthServiceImpl authService; protected NamedItemCollection<ClientAppData> getStore(long projectId) { NamedItemCollection<ClientAppData> store = authRepository.getClientApps(projectId); return store; } public static class ClientAppImpl implements ClientApp { final ClientAppData data; final ClientAppSecretData secrets; public ClientAppImpl(ClientAppData data, ClientAppSecretData secrets) { this.data = data; this.secrets = secrets; } @Override public String getAppName() { return data.getKey(); } @Override public String getAppId() { return data.getProject() + ":" + data.getKey(); } } @Override public ClientApp findClientAppByName(Project project, String appName, String appPassword) throws CloudException { ClientAppData clientApp = getStore(project.getId()).find(appName); if (clientApp == null) { log.debug("App '{}' not found in project", appName); return null; } ClientAppSecretData secret = findClientAppSecretData(clientApp, appPassword); if (secret == null) { log.debug("Password mismatch on app '{}'", appName); return null; } return new ClientAppImpl(clientApp, secret); } @Override public ClientApp findClientAppById(String appId, String appPassword) throws CloudException { int firstColon = appId.indexOf(':'); if (firstColon == -1) { log.debug("Invalid app id: " + appId); return null; } String project = appId.substring(0, firstColon); String appName = appId.substring(firstColon + 1); ClientAppData clientApp = getStore(Long.valueOf(project)).find(appName); if (clientApp == null) { log.debug("App '{}' not found in project", appId); return null; } ClientAppSecretData secret = findClientAppSecretData(clientApp, appPassword); if (secret == null) { log.debug("Password mismatch on app '{}'", appId); return null; } return new ClientAppImpl(clientApp, secret); } @Override public void setUserSecret(ClientApp app, Auth auth, byte[] payload) throws CloudException { AuthenticatedUser user = authService.toAuthenticatedUser(auth); long userId = user.getUserId(); NamedItemCollection<AttachmentData> store = authRepository.getUserAttachments(userId); SecretData.Builder s = SecretData.newBuilder(); try { appSecrets.setUserSecret(user, s, payload); } catch (KeyczarException e) { throw new IllegalStateException("Error setting secret", e); } setSecret(store, app, s.build()); } @Override public void setProjectSecret(ClientApp app, Auth auth, Project project, byte[] payload) throws CloudException { AuthenticatedUser user = authService.toAuthenticatedUser(auth); Project authProject = auth.getProject(); if (project == null) { throw new IllegalArgumentException(); } if (authProject.getId() != project.getId()) { throw new IllegalArgumentException(); } long projectId = project.getId(); AuthenticatedProject authenticatedProject = identityService.authenticateToProject(user, projectId); if (authenticatedProject == null) { throw new IllegalStateException(); } NamedItemCollection<AttachmentData> store = authRepository.getProjectAttachments(projectId); SecretData.Builder s = SecretData.newBuilder(); try { appSecrets.setProjectSecret(authenticatedProject, s, payload); } catch (KeyczarException e) { throw new IllegalStateException("Error setting secret", e); } setSecret(store, app, s.build()); } private void setSecret(NamedItemCollection<AttachmentData> store, ClientApp app, SecretData data) throws CloudException { String appKey = app.getAppId(); AttachmentData existing = store.find(appKey); AttachmentData.Builder b; if (existing == null) { b = AttachmentData.newBuilder(); b.setKey(appKey); } else { b = AttachmentData.newBuilder(existing); } b.setData(data); if (existing == null) { try { store.create(b); } catch (DuplicateValueException e) { throw new IllegalStateException(); } } else { store.update(b); } } @Override public byte[] findUserSecret(ClientApp app, Auth auth) throws CloudException { AuthenticatedUser user = authService.toAuthenticatedUser(auth); long userId = user.getUserId(); NamedItemCollection<AttachmentData> store = authRepository.getUserAttachments(userId); return findUserSecret(store, app, user); } @Override public byte[] findProjectSecret(ClientApp app, Auth auth, Project project) throws CloudException { AuthenticatedUser user = authService.toAuthenticatedUser(auth); Project authProject = auth.getProject(); if (project == null) { throw new IllegalArgumentException(); } if (authProject.getId() != project.getId()) { throw new IllegalArgumentException(); } long projectId = project.getId(); AuthenticatedProject authenticatedProject = identityService.authenticateToProject(user, projectId); if (authenticatedProject == null) { throw new IllegalStateException(); } NamedItemCollection<AttachmentData> store = authRepository.getProjectAttachments(projectId); return findProjectSecret(store, app, authenticatedProject); } private byte[] findUserSecret(NamedItemCollection<AttachmentData> store, ClientApp app, AuthenticatedUser user) throws CloudException { String appKey = app.getAppId(); AttachmentData existing = store.find(appKey); if (existing == null) { return null; } SecretData secretData = existing.getData(); try { byte[] plaintext = appSecrets.decryptUserSecret(user, secretData); return plaintext; } catch (KeyczarException e) { throw new IllegalStateException("Error decrypting secret", e); } } private byte[] findProjectSecret(NamedItemCollection<AttachmentData> store, ClientApp app, AuthenticatedProject project) throws CloudException { String appKey = app.getAppId(); AttachmentData existing = store.find(appKey); if (existing == null) { return null; } SecretData secretData = existing.getData(); try { byte[] plaintext = appSecrets.decryptProjectSecret(project, secretData); return plaintext; } catch (KeyczarException e) { throw new IllegalStateException("Error decrypting secret", e); } } @Override public ClientApp createClientApp(Auth auth, Project project, String appName, String appPassword) throws CloudException { AuthenticatedUser authenticatedUser = authService.toAuthenticatedUser(auth); AuthenticatedProject authenticatedProject = identityService.authenticateToProject(authenticatedUser, project.getId()); if (authenticatedProject == null) { throw new IllegalStateException(); } ClientAppSecretData secrets; { ClientAppSecretData.Builder sb = ClientAppSecretData.newBuilder(); sb.setAppPassword(appPassword); secrets = sb.build(); } ClientAppData existing = getStore(project.getId()).find(appName); if (existing != null) { throw new WebApplicationException(Status.CONFLICT); } ClientAppData.Builder b = ClientAppData.newBuilder(); b.setProject(project.getId()); b.setKey(appName); SecretToken secretToken = SecretToken.create(SecretTokenType.CLIENT_APP_SECRET); buildSecretStore(b, secretToken, appPassword, authenticatedProject); b.setSecretData(Secrets.buildClientAppSecret(secretToken, secrets)); try { ClientAppData app = getStore(project.getId()).create(b); return new ClientAppImpl(app, secrets); } catch (DuplicateValueException e) { throw new WebApplicationException(Status.CONFLICT); } } // private String deriveKey(String name, long projectId) { // Hasher hasher = Hashing.md5().newHasher(); // hasher.putString(name, Charsets.UTF_8); // hasher.putString("::", Charsets.UTF_8); // hasher.putString(Long.toString(projectId), Charsets.UTF_8); // // long id = hasher.hash().asLong(); // return Long.toHexString(id); // } protected ClientAppSecretData findClientAppSecretData(ClientAppData app, String appPassword) { SecretData secretData = app.getSecretData(); SecretToken secretToken; try { secretToken = Secrets.getSecretFromPassword(app.getSecretStore(), appPassword); } catch (KeyczarException e) { log.warn("Keyczar error while decrypting; likely bad password", e); return null; } if (secretToken == null) { return null; } ClientAppSecretData secrets; try { secrets = Secrets.unlock(secretData, secretToken, ClientAppSecretData.newBuilder()); } catch (Exception e) { // Wrong password log.warn("Error while decrypting client app, likely wrong secret", e); return null; } // if (!secrets.getDeprecatedVerifyName().equals(app.getName())) { // log.warn("Name mismatch when verifying decrypted data"); // return null; // } return secrets; } // protected void buildSecretData(SecretData.Builder s, ClientAppSecretData // secrets, String name, String password) { // byte[] seed = name.getBytes(Charsets.UTF_8); // AesKey passwordKey = KeyczarUtils.deriveKey(ITERATION_COUNT, seed, // password); // byte[] plaintext = secrets.toByteArray(); // byte[] ciphertext = KeyczarUtils.encrypt(passwordKey, plaintext); // // s.setCiphertext(ByteString.copyFrom(ciphertext)); // s.setVersion(1); // } void buildSecretStore(ClientAppData.Builder app, SecretToken secret, String appPassword, AuthenticatedProject project) { SecretStoreData.Builder secretStore = app.getSecretStoreBuilder(); if (!Strings.isNullOrEmpty(appPassword)) { Secrets.setPassword(secretStore, appPassword, secret); } else { // Without a password, there's going to be no way to get the key throw new UnsupportedOperationException(); } Secrets.storeLockedByProject(app.getSecretStoreBuilder(), project, secret); } }