Java tutorial
/** * Copyright (c) 2017, 2018 Bosch Software Innovations GmbH. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Bosch Software Innovations GmbH - initial creation */ package org.eclipse.hono.util; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Base64; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import javax.security.auth.x500.X500Principal; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; /** * Encapsulates the credentials information for a device that was found by the get operation of the * <a href="https://www.eclipse.org/hono/api/credentials-api/">Credentials API</a>. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class CredentialsObject { @JsonProperty(CredentialsConstants.FIELD_PAYLOAD_DEVICE_ID) private String deviceId; @JsonProperty(CredentialsConstants.FIELD_TYPE) private String type; @JsonProperty(CredentialsConstants.FIELD_AUTH_ID) private String authId; @JsonProperty(CredentialsConstants.FIELD_ENABLED) private boolean enabled = true; private JsonArray secrets = new JsonArray(); /** * Empty default constructor. */ public CredentialsObject() { super(); } /** * Creates new credentials for an authentication identifier. * <p> * Note that an instance created using this constructor does * not contain any secrets. * * @param deviceId The device to which the credentials belong. * @param authId The authentication identifier of the credentials. * @param type The type of credentials. */ public CredentialsObject(final String deviceId, final String authId, final String type) { Objects.requireNonNull(deviceId); Objects.requireNonNull(authId); Objects.requireNonNull(type); setDeviceId(deviceId); setType(type); setAuthId(authId); } /** * Gets the identifier of the device that these credentials belong to. * * @return The identifier or {@code null} if not set. */ public String getDeviceId() { return deviceId; } /** * Sets the identifier of the device that these credentials belong to. * * @param deviceId The identifier. * @return This credentials object for method chaining. */ public CredentialsObject setDeviceId(final String deviceId) { this.deviceId = deviceId; return this; } /** * Gets the type of these credentials. * * @return The type or {@code null} if not set. */ public String getType() { return type; } /** * Sets the type of these credentials. * * @param type The credentials type. * @return This credentials object for method chaining. */ public CredentialsObject setType(final String type) { this.type = type; return this; } /** * Gets the authentication identifier that these credentials are used for. * * @return The identifier or {@code null} if not set. */ public String getAuthId() { return authId; } /** * Sets the authentication identifier that these these credentials are used for. * * @param authId The identifier. * @return This credentials object for method chaining. */ public CredentialsObject setAuthId(final String authId) { this.authId = authId; return this; } /** * Checks whether these credentials are enabled. * <p> * The default value is {@code true}. * * @return {@code true} if these credentials can be used for authenticating devices. */ public boolean isEnabled() { return enabled; } /** * Sets whether these credentials are enabled. * <p> * The default value is {@code true}. * * @param enabled {@code true} if these credentials can be used for authenticating devices. * @return This credentials object for method chaining. */ public CredentialsObject setEnabled(final boolean enabled) { this.enabled = enabled; return this; } /** * Gets this credentials' secret(s) as {@code Map} instances. * * @return The (potentially empty) list of secrets. */ @JsonProperty(CredentialsConstants.FIELD_SECRETS) public List<Map<String, Object>> getSecretsAsMaps() { final List<Map<String, Object>> result = new LinkedList<>(); secrets.forEach(secret -> result.add(((JsonObject) secret).getMap())); return result; } /** * Gets this credentials' secret(s). * <p> * The elements of the returned list are of type {@code JsonObject}. * * @return The (potentially empty) list of secrets. */ @JsonIgnore public JsonArray getSecrets() { return secrets; } /** * Sets this credentials' secret(s). * <p> * The new secret(s) will replace the existing ones. * * @param newSecrets The secrets to set. * @return This credentials object for method chaining. * @throws NullPointerException if secrets is {@code null}. */ @JsonProperty(CredentialsConstants.FIELD_SECRETS) public CredentialsObject setSecrets(final List<Map<String, Object>> newSecrets) { this.secrets.clear(); newSecrets.forEach(secret -> addSecret(secret)); return this; } /** * Adds a secret. * * @param secret The secret to set. * @return This credentials object for method chaining. */ public CredentialsObject addSecret(final JsonObject secret) { if (secret != null) { secrets.add(secret); } return this; } /** * Adds a secret. * * @param secret The secret to set. * @return This credentials object for method chaining. */ public CredentialsObject addSecret(final Map<String, Object> secret) { addSecret(new JsonObject(secret)); return this; } /** * Checks if this credentials object has all mandatory properties set. * * @return {@code true} if all mandatory properties are set. */ @JsonIgnore public boolean isValid() { return deviceId != null && authId != null && type != null && hasValidSecrets(); } /** * Checks if this credentials object contains secrets that comply with the Credentials * API specification. * * @return {@code true} if at least one secret is set and the secrets' not-before * and not-after properties are well formed. */ public boolean hasValidSecrets() { if (secrets == null || secrets.isEmpty()) { return false; } else { return !secrets.stream().filter(obj -> obj instanceof JsonObject).anyMatch(obj -> { final JsonObject secret = (JsonObject) obj; return !containsValidTimestampIfPresentForField(secret, CredentialsConstants.FIELD_SECRETS_NOT_BEFORE) || !containsValidTimestampIfPresentForField(secret, CredentialsConstants.FIELD_SECRETS_NOT_AFTER); }); } } private boolean containsValidTimestampIfPresentForField(final JsonObject secret, final String field) { final Object value = secret.getValue(field); if (value == null) { return true; } else if (String.class.isInstance(value)) { return getInstant((String) value) != null; } else { return false; } } /** * Gets the <em>not before</em> instant of a secret. * * @param secret The secret. * @return The instant or {@code null} if not-before is not set or * uses an invalid time stamp format. */ public static Instant getNotBefore(final JsonObject secret) { if (secret == null) { return null; } else { return getInstant(secret, CredentialsConstants.FIELD_SECRETS_NOT_BEFORE); } } /** * Gets the <em>not after</em> instant of a secret. * * @param secret The secret. * @return The instant or {@code null} if not-after is not set or * uses an invalid time stamp format. */ public static Instant getNotAfter(final JsonObject secret) { if (secret == null) { return null; } else { return getInstant(secret, CredentialsConstants.FIELD_SECRETS_NOT_AFTER); } } private static Instant getInstant(final JsonObject secret, final String field) { final Object value = secret.getValue(field); if (String.class.isInstance(value)) { return getInstant((String) value); } else { return null; } } private static Instant getInstant(final String timestamp) { if (timestamp == null) { return null; } else { try { return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(timestamp, OffsetDateTime::from).toInstant(); } catch (DateTimeParseException e) { return null; } } } /** * Creates an otherwise empty secret for a <em>not-before</em> and * a <em>not-after</em> instant. * * @param notBefore The point in time from which on the credentials are valid * or {@code null} if there is no such constraint. * @param notAfter The point in time until the credentials are valid * or {@code null} if there is no such constraint. * @return The secret. * @throws IllegalArgumentException if not-before is not before not-after. */ public static JsonObject emptySecret(final Instant notBefore, final Instant notAfter) { if (notBefore != null && notAfter != null && !notBefore.isBefore(notAfter)) { throw new IllegalArgumentException("not before must be before not after"); } else { final JsonObject secret = new JsonObject(); if (notBefore != null) { secret.put(CredentialsConstants.FIELD_SECRETS_NOT_BEFORE, DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(notBefore.atOffset(ZoneOffset.UTC))); } if (notAfter != null) { secret.put(CredentialsConstants.FIELD_SECRETS_NOT_AFTER, DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(notAfter.atOffset(ZoneOffset.UTC))); } return secret; } } /** * Creates a salted hash for a password. * <p> * Gets the password's UTF-8 bytes, prepends them with the salt (if not {@code null} * and returns the output of the hash function applied to the byte array. * * @param hashFunction The hash function to use. * @param salt The salt to prepend the password bytes with. * @param password The password to hash. * @return The hashed password. * @throws NoSuchAlgorithmException if the given hash function is not supported on * the JVM. */ public static byte[] getHashedPassword(final String hashFunction, final byte[] salt, final String password) throws NoSuchAlgorithmException { final MessageDigest digest = MessageDigest.getInstance(hashFunction); if (salt != null) { digest.update(salt); } digest.update(password.getBytes(StandardCharsets.UTF_8)); return digest.digest(); } /** * Creates a credentials object for a device based on a username * and password. * <p> * The credentials created are of type <em>hashed-password</em>. * The {@linkplain #setAuthId(String) authentication identifier} will be set to * the given username. * * @param deviceId The device identifier. * @param username The username. * @param password The password. * @param hashAlgorithm The algorithm to use for creating the password hash. * @param notBefore The point in time from which on the credentials are valid. * @param notAfter The point in time until the credentials are valid. * @param salt The salt to use for creating the password hash. * @return The credentials. * @throws NullPointerException if any of device ID, authentication ID, password * or hash algorithm is {@code null}. * @throws IllegalArgumentException if the <em>not-before</em> instant does not lie * before the <em>not after</em> instant or if the * algorithm is not supported. */ public static CredentialsObject fromHashedPassword(final String deviceId, final String username, final String password, final String hashAlgorithm, final Instant notBefore, final Instant notAfter, final byte[] salt) { Objects.requireNonNull(password); final CredentialsObject result = new CredentialsObject(deviceId, username, CredentialsConstants.SECRETS_TYPE_HASHED_PASSWORD); result.addSecret(hashedPasswordSecret(password, hashAlgorithm, notBefore, notAfter, salt)); return result; } /** * Creates a hashed-password secret. * * @param password The password. * @param hashAlgorithm The algorithm to use for creating the password hash. * @param notBefore The point in time from which on the secret is valid. * @param notAfter The point in time until the secret is valid. * @param salt The salt to use for creating the password hash. * @return The secret. * @throws NullPointerException if any of password or hash algorithm is {@code null}. * @throws IllegalArgumentException if the <em>not-before</em> instant does not lie * before the <em>not after</em> instant or if the * algorithm is not supported. */ public static JsonObject hashedPasswordSecret(final String password, final String hashAlgorithm, final Instant notBefore, final Instant notAfter, final byte[] salt) { Objects.requireNonNull(password); Objects.requireNonNull(hashAlgorithm); try { final JsonObject secret = emptySecret(notBefore, notAfter); secret.put(CredentialsConstants.FIELD_SECRETS_HASH_FUNCTION, hashAlgorithm); if (salt != null) { secret.put(CredentialsConstants.FIELD_SECRETS_SALT, Base64.getEncoder().encodeToString(salt)); } secret.put(CredentialsConstants.FIELD_SECRETS_PWD_HASH, Base64.getEncoder().encodeToString(getHashedPassword(hashAlgorithm, salt, password))); return secret; } catch (final NoSuchAlgorithmException e) { throw new IllegalArgumentException("unsupported hash algorithm"); } } /** * Creates a credentials object for a device and auth ID. * <p> * The credentials created are of type <em>psk</em>. * * @param deviceId The device identifier. * @param authId The authentication identifier. * @param key The shared key. * @param notBefore The point in time from which on the credentials are valid. * @param notAfter The point in time until the credentials are valid. * @return The credentials. * @throws NullPointerException if any of device ID, authentication ID or password * is {@code null}. * @throws IllegalArgumentException if the <em>not-before</em> instant does not lie * before the <em>not after</em> instant. */ public static CredentialsObject fromPresharedKey(final String deviceId, final String authId, final byte[] key, final Instant notBefore, final Instant notAfter) { Objects.requireNonNull(key); final CredentialsObject result = new CredentialsObject(deviceId, authId, CredentialsConstants.SECRETS_TYPE_PRESHARED_KEY); final JsonObject secret = emptySecret(notBefore, notAfter); secret.put(CredentialsConstants.FIELD_SECRETS_KEY, Base64.getEncoder().encodeToString(key)); result.addSecret(secret); return result; } /** * Creates a credentials object for a device based on a client certificate. * <p> * The credentials created are of type <em>x509-cert</em>. The * {@linkplain #setAuthId(String) authentication identifier} will be set to * the certificate's subject DN using the serialization format defined * by <a href="https://tools.ietf.org/html/rfc2253#section-2">RFC 2253, Section 2</a>. * * @param deviceId The device identifier. * @param certificate The device's client certificate. * @param notBefore The point in time from which on the credentials are valid. * @param notAfter The point in time until the credentials are valid. * @return The credentials. * @throws NullPointerException if device ID or certificate are {@code null}. * @throws IllegalArgumentException if the <em>not-before</em> instant does not lie * before the <em>not after</em> instant. */ public static CredentialsObject fromClientCertificate(final String deviceId, final X509Certificate certificate, final Instant notBefore, final Instant notAfter) { Objects.requireNonNull(certificate); return fromSubjectDn(deviceId, certificate.getSubjectX500Principal(), notBefore, notAfter); } /** * Creates a credentials object for a device based on a subject DN. * <p> * The credentials created are of type <em>x509-cert</em>. The * {@linkplain #setAuthId(String) authentication identifier} will be set to * the subject DN using the serialization format defined by * <a href="https://tools.ietf.org/html/rfc2253#section-2">RFC 2253, Section 2</a>. * * @param deviceId The device identifier. * @param subjectDn The subject DN. * @param notBefore The point in time from which on the credentials are valid. * @param notAfter The point in time until the credentials are valid. * @return The credentials. * @throws NullPointerException if device ID or subject DN are {@code null}. * @throws IllegalArgumentException if the <em>not-before</em> instant does not lie * before the <em>not after</em> instant. */ public static CredentialsObject fromSubjectDn(final String deviceId, final X500Principal subjectDn, final Instant notBefore, final Instant notAfter) { Objects.requireNonNull(subjectDn); final String authId = subjectDn.getName(X500Principal.RFC2253); final CredentialsObject result = new CredentialsObject(deviceId, authId, CredentialsConstants.SECRETS_TYPE_X509_CERT); final JsonObject secret = emptySecret(notBefore, notAfter); result.addSecret(secret); return result; } }