package com.amazonaws.mobileconnectors.iot; import com.amazonaws.AmazonClientException; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSSessionCredentials; import com.amazonaws.auth.AnonymousAWSCredentials; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.BasicSessionCredentials; import com.amazonaws.auth.SigningAlgorithm; import com.amazonaws.regions.Region; import com.amazonaws.util.BinaryUtils; import com.amazonaws.util.DateUtils; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.MessageDigest; import java.util.Date; import static com.amazonaws.util.StringUtils.UTF8; /** * The AWSIotWebSocketUrlSigner class creates the SigV4 signature and builds a connection * URL to be used with the Paho MQTT client. */ class AWSIotWebSocketUrlSigner { /** Constant defining the algorithm specifier in SigV4 parameters. */ private static final String ALGORITHM = "AWS4-HMAC-SHA256"; /** Constant defining the key prefix string in SigV4 parameters. */ private static final String KEY_PREFIX = "AWS4"; /** Constant defining the terminator string in SigV4 parameters. */ private static final String TERMINATOR = "aws4_request"; /** Short date format pattern used in SigV4 parameters. */ private static final String DATE_PATTERN = "yyyyMMdd"; /** ISO 8601 date + time format pattern used in SigV4 signature parameters. */ private static final String TIME_PATTERN = "yyyyMMdd'T'HHmmss'Z'"; /** Constant defining the HTTP method for initiating a WebSocket connection. */ private static final String METHOD = "GET"; /** URI for WebSocket endpoint when doing initial HTTP operation. */ private static final String CANONICAL_URI = "/mqtt"; /** * Service name used when constructing the endpoint and singing the URL. */ private String signerServiceName; /** Date override for unit testing only. */ private Date overriddenDate = null; /** * Create a new AWSIotWebSocketUrlSigner. * @param serviceName AWS IoT service name used in SigV4 algorithm. */ public AWSIotWebSocketUrlSigner(String serviceName) { signerServiceName = serviceName; } /** * Given the region and service name provided to the client, the endpoint and the current time * return a signed connection URL to be used when connecting via WebSocket to AWS IoT. * @param endpoint service endpoint with or without customer specific URL prefix. * @param awsCredentials credential set to be used in SigV4 signature algorithm. * @param currentTimeInMillis time value to be used in SigV4 calculations. In milliseconds. * @return a URL with SigV4 signature formatted to be used with AWS IoT. */ public String getSignedUrl(String endpoint, AWSCredentials awsCredentials, long currentTimeInMillis) { // anonymous credentials, don't sign if (awsCredentials instanceof AnonymousAWSCredentials) { throw new IllegalArgumentException("Credentials cannot be Anonymous"); } Region signerRegion = AwsIotEndpointUtility.getRegionFromIotEndpoint(endpoint); String signerRegionName = signerRegion.getName(); AWSCredentials sanitizedCredentials = sanitizeCredentials(awsCredentials); // Create a canonical request for signature version 4 // SigV4 canonical string uses time in two formats (date and full date/time) String amzDate = getAmzDate(currentTimeInMillis); // full date/time String dateStamp = getDateStamp(currentTimeInMillis); // date // Credential scoped to date and region String credentialScope = dateStamp + "/" + signerRegionName + "/" + signerServiceName + "/aws4_request"; // Now build the canonical string StringBuilder canonicalQueryStringBuilder = new StringBuilder(); canonicalQueryStringBuilder.append("X-Amz-Algorithm=").append(ALGORITHM); canonicalQueryStringBuilder.append("&X-Amz-Credential="); try { canonicalQueryStringBuilder.append(URLEncoder.encode(sanitizedCredentials.getAWSAccessKeyId() + "/" + credentialScope, UTF8.name())); } catch (UnsupportedEncodingException e) { throw new AmazonClientException("Error encoding URL when building WebSocket URL", e); } canonicalQueryStringBuilder.append("&X-Amz-Date=").append(amzDate); canonicalQueryStringBuilder.append("&X-Amz-SignedHeaders=host"); // headers and payload for the signing request // not used in an WebSocket URL, but encoded into the signature string String canonicalHeaders = "host:" + endpoint + "\n"; String payloadHash = BinaryUtils.toHex(hash("")); // The request to sign includes the HTTP method, path, query string, headers and payload String canonicalRequest = METHOD + "\n" + CANONICAL_URI + "\n" + canonicalQueryStringBuilder.toString() + "\n" + canonicalHeaders + "\nhost\n" + payloadHash; // Create a string to sign, generate a signing key... String stringToSign = ALGORITHM + "\n" + amzDate + "\n" + credentialScope + "\n" + BinaryUtils.toHex(hash(canonicalRequest)); byte[] signingKey = getSigningKey(dateStamp, signerRegionName, signerServiceName, sanitizedCredentials); // ...and sign the string. byte[] signatureBytes = sign(stringToSign.getBytes(), signingKey, SigningAlgorithm.HmacSHA256); String signature = BinaryUtils.toHex(signatureBytes); // Add the signature to the query string. canonicalQueryStringBuilder.append("&X-Amz-Signature="); canonicalQueryStringBuilder.append(signature); // Now build the URL. String requestUrl = "wss://" + endpoint + CANONICAL_URI + "?" + canonicalQueryStringBuilder.toString(); // If there are session credentials (from an STS server, AssumeRole, or Amazon Cognito), // append the session token to the end of the URL string after signing. if (awsCredentials instanceof AWSSessionCredentials) { String sessionToken = null; try { sessionToken = URLEncoder.encode(((AWSSessionCredentials) awsCredentials).getSessionToken(), UTF8.name()); } catch (UnsupportedEncodingException e) { throw new AmazonClientException("Error encoding URL when appending session token to URL", e); } requestUrl += "&X-Amz-Security-Token=" + sessionToken; } return requestUrl; } /** * The SigV4 signing key is made up by consecutively hashing a number of unique pieces of data. * @param dateStamp the current date in short date format. * @param regionName AWS region name. * @param serviceName service name for IoT service. * @param credentials AWS credential set to be used in signing. * @return byte array containing the SigV4 signing key. */ private byte[] getSigningKey(String dateStamp, String regionName, String serviceName, AWSCredentials credentials) { // AWS4 uses a series of derived keys, formed by hashing different pieces of data byte[] signingSecret = (KEY_PREFIX + credentials.getAWSSecretKey()).getBytes(); byte[] signingDate = sign(dateStamp, signingSecret, SigningAlgorithm.HmacSHA256); byte[] signingRegion = sign(regionName, signingDate, SigningAlgorithm.HmacSHA256); byte[] signingService = sign(serviceName, signingRegion, SigningAlgorithm.HmacSHA256); return sign(TERMINATOR, signingService, SigningAlgorithm.HmacSHA256); } /** * Given the input epoch time returns a String of the proper format for the * ISO 8601 date + time in SigV4 parameters. * @param dateMilli desired date in milliseconds since epoch, UTC. * @return date formatted string in ISO 8601 date + time format. */ private String getAmzDate(long dateMilli) { return DateUtils.format(TIME_PATTERN, new Date(dateMilli)); } /** * Given the input epoch time returns a String of the proper format for the * short date in SigV4 parameters. * @param dateMilli desired date in milliseconds since epoch, UTC. * @return date formatted string in short date format. */ private String getDateStamp(long dateMilli) { return DateUtils.format(DATE_PATTERN, new Date(dateMilli)); } /** * Loads the individual access key ID and secret key from the specified * credentials and trimming any extra whitespace from the credentials. * * @param credentials AWSCredentials to be sanitized. * @return A new credentials object with the sanitized credentials. */ AWSCredentials sanitizeCredentials(AWSCredentials credentials) { String accessKeyId = null; String secretKey = null; String token = null; accessKeyId = credentials.getAWSAccessKeyId(); secretKey = credentials.getAWSSecretKey(); if (credentials instanceof AWSSessionCredentials) { token = ((AWSSessionCredentials) credentials).getSessionToken(); } if (secretKey != null) { secretKey = secretKey.trim(); } if (accessKeyId != null) { accessKeyId = accessKeyId.trim(); } if (token != null) { token = token.trim(); } if (credentials instanceof AWSSessionCredentials) { return new BasicSessionCredentials(accessKeyId, secretKey, token); } return new BasicAWSCredentials(accessKeyId, secretKey); } /** * Hashes the string contents (assumed to be UTF-8) using the SHA-256 * algorithm. * * @param text The string to hash. * @return The hashed bytes from the specified string. * @throws AmazonClientException If the hash cannot be computed. */ byte[] hash(String text) throws AmazonClientException { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(text.getBytes(UTF8)); return md.digest(); } catch (Exception e) { throw new AmazonClientException("Unable to compute hash while signing request: " + e.getMessage(), e); } } /** * Sign the given string with the key provide using the specified agorithm. * * @param stringData String to be signed. * @param key the key for signing. * @param algorithm the signature algorithm. * @return a byte array containing the signed string. * @throws AmazonClientException in the case of a signature error. */ byte[] sign(String stringData, byte[] key, SigningAlgorithm algorithm) throws AmazonClientException { try { byte[] data = stringData.getBytes(UTF8); return sign(data, key, algorithm); } catch (Exception e) { throw new AmazonClientException("Unable to calculate a request signature: " + e.getMessage(), e); } } /** * Sign the given data with the key provide using the specified agorithm. * * @param data byte buffer of data to be signed. * @param key the key for signing. * @param algorithm the signature algorithm. * @return a byte array containing the signed string. * @throws AmazonClientException in the case of a signature error. */ byte[] sign(byte[] data, byte[] key, SigningAlgorithm algorithm) throws AmazonClientException { try { Mac mac = Mac.getInstance(algorithm.toString()); mac.init(new SecretKeySpec(key, algorithm.toString())); return mac.doFinal(data); } catch (Exception e) { throw new AmazonClientException("Unable to calculate a request signature: " + e.getMessage(), e); } } }