diff --git a/src/main/java/com/backend/glowhouse/config/AwsIotUtil.java b/src/main/java/com/backend/glowhouse/config/AwsIotUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..3209f3fc073c7295a9f21507437b745c05525967 --- /dev/null +++ b/src/main/java/com/backend/glowhouse/config/AwsIotUtil.java @@ -0,0 +1,134 @@ +package com.backend.glowhouse.config; + +import java.io.BufferedInputStream; + import java.io.DataInputStream; + import java.io.File; + import java.io.FileInputStream; + import java.io.IOException; + import java.io.InputStream; + import java.math.BigInteger; + import java.net.URL; + import java.security.GeneralSecurityException; + import java.security.KeyStore; + import java.security.KeyStoreException; + import java.security.NoSuchAlgorithmException; + import java.security.PrivateKey; + import java.security.SecureRandom; + import java.security.cert.Certificate; + import java.security.cert.CertificateException; + import java.security.cert.CertificateFactory; + import java.util.List; + import java.util.Properties; + +/** + * This is a helper class to facilitate reading of the configurations and + * certificate from the resource files. + */ +public class AwsIotUtil { + private static final String PropertyFile = "application.properties"; + + public static class KeyStorePasswordPair { + public KeyStore keyStore; + public String keyPassword; + + public KeyStorePasswordPair(KeyStore keyStore, String keyPassword) { + this.keyStore = keyStore; + this.keyPassword = keyPassword; + } + } + + public static String getConfig(String name) { + Properties prop = new Properties(); + URL resource = AwsIotUtil.class.getResource(PropertyFile); + if (resource == null) { + return null; + } + try (InputStream stream = resource.openStream()) { + prop.load(stream); + } catch (IOException e) { + return null; + } + String value = prop.getProperty(name); + if (value == null || value.trim().length() == 0) { + return null; + } else { + return value; + } + } + + public static KeyStorePasswordPair getKeyStorePasswordPair(final String certificateFile, final String privateKeyFile) { + return getKeyStorePasswordPair(certificateFile, privateKeyFile, null); + } + + public static KeyStorePasswordPair getKeyStorePasswordPair(final String certificateFile, final String privateKeyFile, + String keyAlgorithm) { + if (certificateFile == null || privateKeyFile == null) { + System.out.println("Certificate or private key file missing"); + return null; + } + System.out.println("Cert file:" + certificateFile + " Private key: " + privateKeyFile); + + final PrivateKey privateKey = loadPrivateKeyFromFile(privateKeyFile, keyAlgorithm); + + final List<Certificate> certChain = loadCertificatesFromFile(certificateFile); + + if (certChain == null || privateKey == null) return null; + + return getKeyStorePasswordPair(certChain, privateKey); + } + + public static KeyStorePasswordPair getKeyStorePasswordPair(final List<Certificate> certificates, final PrivateKey privateKey) { + KeyStore keyStore; + String keyPassword; + try { + keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + + // randomly generated key password for the key in the KeyStore + keyPassword = new BigInteger(128, new SecureRandom()).toString(32); + + Certificate[] certChain = new Certificate[certificates.size()]; + certChain = certificates.toArray(certChain); + keyStore.setKeyEntry("alias", privateKey, keyPassword.toCharArray(), certChain); + } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) { + System.out.println("Failed to create key store"); + return null; + } + + return new KeyStorePasswordPair(keyStore, keyPassword); + } + + private static List<Certificate> loadCertificatesFromFile(final String filename) { + File file = new File(filename); + if (!file.exists()) { + System.out.println("Certificate file: " + filename + " is not found."); + return null; + } + + try (BufferedInputStream stream = new BufferedInputStream(new FileInputStream(file))) { + final CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + return (List<Certificate>) certFactory.generateCertificates(stream); + } catch (IOException | CertificateException e) { + System.out.println("Failed to load certificate file " + filename); + } + return null; + } + + private static PrivateKey loadPrivateKeyFromFile(final String filename, final String algorithm) { + PrivateKey privateKey = null; + + File file = new File(filename); + if (!file.exists()) { + System.out.println("Private key file not found: " + filename); + return null; + } + try (DataInputStream stream = new DataInputStream(new FileInputStream(file))) { + privateKey = PrivateKeyReader.getPrivateKey(stream, algorithm); + } catch (IOException | GeneralSecurityException e) { + System.out.println("Failed to load private key from file " + filename); + } + + return privateKey; + } + +} \ No newline at end of file diff --git a/src/main/java/com/backend/glowhouse/config/PrivateKeyReader.java b/src/main/java/com/backend/glowhouse/config/PrivateKeyReader.java new file mode 100644 index 0000000000000000000000000000000000000000..b625e6215e40eb694e99e8c0537b85296ae791a3 --- /dev/null +++ b/src/main/java/com/backend/glowhouse/config/PrivateKeyReader.java @@ -0,0 +1,489 @@ +package com.backend.glowhouse.config; + +/**************************************************************************** + * Amazon Modifications: Copyright 2016 Amazon.com, Inc. or its affiliates. + * All Rights Reserved. + ***************************************************************************** + * Copyright (c) 1998-2010 AOL Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ****************************************************************************/ +// http://oauth.googlecode.com/svn/code/branches/jmeter/jmeter/src/main/java/org/apache/jmeter/protocol/oauth/sampler/PrivateKeyReader.java + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPrivateCrtKeySpec; + +import javax.xml.bind.DatatypeConverter; + +/** + * Class for reading RSA or ECC private key from PEM file. + * + * It can read PEM files with PKCS#8 or PKCS#1 encodings. It doesn't support + * encrypted PEM files. + */ +public class PrivateKeyReader { + + // Private key file using PKCS #1 encoding + public static final String P1_BEGIN_MARKER = "-----BEGIN RSA PRIVATE KEY"; //$NON-NLS-1$ + public static final String P1_END_MARKER = "-----END RSA PRIVATE KEY"; //$NON-NLS-1$ + + // Private key file using PKCS #8 encoding + public static final String P8_BEGIN_MARKER = "-----BEGIN PRIVATE KEY"; //$NON-NLS-1$ + public static final String P8_END_MARKER = "-----END PRIVATE KEY"; //$NON-NLS-1$ + + /** + * Get a RSA Private Key from InputStream. + * + * @param fileName + * file name + * @return Private key + * @throws IOException + * IOException resulted from invalid file IO + * @throws GeneralSecurityException + * GeneralSecurityException resulted from invalid key format + */ + public static PrivateKey getPrivateKey(String fileName) throws IOException, GeneralSecurityException { + try (InputStream stream = new FileInputStream(fileName)) { + return getPrivateKey(stream, null); + } + } + + /** + * Get a Private Key from InputStream. + * + * @param fileName + * file name + * @param algorithm + * the name of the key algorithm, for example "RSA" or "EC" + * @return Private key + * @throws IOException + * IOException resulted from invalid file IO + * @throws GeneralSecurityException + * GeneralSecurityException resulted from invalid key data + */ + public static PrivateKey getPrivateKey(String fileName, String algorithm) throws IOException, + GeneralSecurityException { + try (InputStream stream = new FileInputStream(fileName)) { + return getPrivateKey(stream, algorithm); + } + } + + /** + * Get a Private Key for the file. + * + * @param stream + * InputStream object + * @param algorithm + * the name of the key algorithm, for example "RSA" or "EC" + * @return Private key + * @throws IOException + * IOException resulted from invalid file IO + * @throws GeneralSecurityException + * GeneralSecurityException resulted from invalid key data + */ + public static PrivateKey getPrivateKey(InputStream stream, String algorithm) throws IOException, + GeneralSecurityException { + PrivateKey key = null; + boolean isRSAKey = false; + + BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8")); + StringBuilder builder = new StringBuilder(); + boolean inKey = false; + for (String line = br.readLine(); line != null; line = br.readLine()) { + if (!inKey) { + if (line.startsWith("-----BEGIN ") && line.endsWith(" PRIVATE KEY-----")) { + inKey = true; + isRSAKey = line.contains("RSA"); + } + continue; + } else { + if (line.startsWith("-----END ") && line.endsWith(" PRIVATE KEY-----")) { + inKey = false; + isRSAKey = line.contains("RSA"); + break; + } + builder.append(line); + } + } + KeySpec keySpec = null; + byte[] encoded = DatatypeConverter.parseBase64Binary(builder.toString()); + if (isRSAKey) { + keySpec = getRSAKeySpec(encoded); + } else { + keySpec = new PKCS8EncodedKeySpec(encoded); + } + KeyFactory kf = KeyFactory.getInstance((algorithm == null) ? "RSA" : algorithm); + key = kf.generatePrivate(keySpec); + + return key; + } + + /** + * Convert PKCS#1 encoded private key into RSAPrivateCrtKeySpec. + * + * <p/> + * The ASN.1 syntax for the private key with CRT is + * + * <pre> + * -- + * -- Representation of RSA private key with information for the CRT algorithm. + * -- + * RSAPrivateKey ::= SEQUENCE { + * version Version, + * modulus INTEGER, -- n + * publicExponent INTEGER, -- e + * privateExponent INTEGER, -- d + * prime1 INTEGER, -- p + * prime2 INTEGER, -- q + * exponent1 INTEGER, -- d mod (p-1) + * exponent2 INTEGER, -- d mod (q-1) + * coefficient INTEGER, -- (inverse of q) mod p + * otherPrimeInfos OtherPrimeInfos OPTIONAL + * } + * </pre> + * + * @param keyBytes + * PKCS#1 encoded key + * @return KeySpec + * @throws IOException + * IOException resulted from invalid file IO + */ + private static RSAPrivateCrtKeySpec getRSAKeySpec(byte[] keyBytes) throws IOException { + + DerParser parser = new DerParser(keyBytes); + + Asn1Object sequence = parser.read(); + if (sequence.getType() != DerParser.SEQUENCE) + throw new IOException("Invalid DER: not a sequence"); //$NON-NLS-1$ + + // Parse inside the sequence + parser = sequence.getParser(); + + parser.read(); // Skip version + BigInteger modulus = parser.read().getInteger(); + BigInteger publicExp = parser.read().getInteger(); + BigInteger privateExp = parser.read().getInteger(); + BigInteger prime1 = parser.read().getInteger(); + BigInteger prime2 = parser.read().getInteger(); + BigInteger exp1 = parser.read().getInteger(); + BigInteger exp2 = parser.read().getInteger(); + BigInteger crtCoef = parser.read().getInteger(); + + RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, + exp2, crtCoef); + + return keySpec; + } +} + +/** + * A bare-minimum ASN.1 DER decoder, just having enough functions to decode + * PKCS#1 private keys. Especially, it doesn't handle explicitly tagged types + * with an outer tag. + * + * <p/> + * This parser can only handle one layer. To parse nested constructs, get a new + * parser for each layer using <code>Asn1Object.getParser()</code>. + * + * <p/> + * There are many DER decoders in JRE but using them will tie this program to a + * specific JCE/JVM. + * + * @author zhang + * + */ +class DerParser { + + // Classes + public final static int UNIVERSAL = 0x00; + public final static int APPLICATION = 0x40; + public final static int CONTEXT = 0x80; + public final static int PRIVATE = 0xC0; + + // Constructed Flag + public final static int CONSTRUCTED = 0x20; + + // Tag and data types + public final static int ANY = 0x00; + public final static int BOOLEAN = 0x01; + public final static int INTEGER = 0x02; + public final static int BIT_STRING = 0x03; + public final static int OCTET_STRING = 0x04; + public final static int NULL = 0x05; + public final static int OBJECT_IDENTIFIER = 0x06; + public final static int REAL = 0x09; + public final static int ENUMERATED = 0x0a; + public final static int RELATIVE_OID = 0x0d; + + public final static int SEQUENCE = 0x10; + public final static int SET = 0x11; + + public final static int NUMERIC_STRING = 0x12; + public final static int PRINTABLE_STRING = 0x13; + public final static int T61_STRING = 0x14; + public final static int VIDEOTEX_STRING = 0x15; + public final static int IA5_STRING = 0x16; + public final static int GRAPHIC_STRING = 0x19; + public final static int ISO646_STRING = 0x1A; + public final static int GENERAL_STRING = 0x1B; + + public final static int UTF8_STRING = 0x0C; + public final static int UNIVERSAL_STRING = 0x1C; + public final static int BMP_STRING = 0x1E; + + public final static int UTC_TIME = 0x17; + public final static int GENERALIZED_TIME = 0x18; + + protected InputStream in; + + /** + * Create a new DER decoder from an input stream. + * + * @param in + * The DER encoded stream + */ + public DerParser(InputStream in) throws IOException { + this.in = in; + } + + /** + * Create a new DER decoder from a byte array. + * + * @throws IOException + * IOException resulted from invalid file IO + */ + public DerParser(byte[] bytes) throws IOException { + this(new ByteArrayInputStream(bytes)); + } + + /** + * Read next object. If it's constructed, the value holds encoded content + * and it should be parsed by a new parser from + * <code>Asn1Object.getParser</code>. + * + * @return A object + * @throws IOException + * IOException resulted from invalid file IO + */ + public Asn1Object read() throws IOException { + int tag = in.read(); + + if (tag == -1) + throw new IOException("Invalid DER: stream too short, missing tag"); //$NON-NLS-1$ + + int length = getLength(); + + byte[] value = new byte[length]; + int n = in.read(value); + if (n < length) + throw new IOException("Invalid DER: stream too short, missing value"); //$NON-NLS-1$ + + Asn1Object o = new Asn1Object(tag, length, value); + + return o; + } + + /** + * Decode the length of the field. Can only support length encoding up to 4 + * octets. + * + * <p/> + * In BER/DER encoding, length can be encoded in 2 forms, + * <ul> + * <li>Short form. One octet. Bit 8 has value "0" and bits 7-1 give the + * length. + * <li>Long form. Two to 127 octets (only 4 is supported here). Bit 8 of + * first octet has value "1" and bits 7-1 give the number of additional + * length octets. Second and following octets give the length, base 256, + * most significant digit first. + * </ul> + * + * @return The length as integer + * @throws IOException + * IOException resulted from invalid file IO + */ + private int getLength() throws IOException { + + int i = in.read(); + if (i == -1) + throw new IOException("Invalid DER: length missing"); //$NON-NLS-1$ + + // A single byte short length + if ((i & ~0x7F) == 0) + return i; + + int num = i & 0x7F; + + // We can't handle length longer than 4 bytes + if (i >= 0xFF || num > 4) + throw new IOException("Invalid DER: length field too big (" //$NON-NLS-1$ + + i + ")"); //$NON-NLS-1$ + + byte[] bytes = new byte[num]; + int n = in.read(bytes); + if (n < num) + throw new IOException("Invalid DER: length too short"); //$NON-NLS-1$ + + return new BigInteger(1, bytes).intValue(); + } + +} + +/** + * An ASN.1 TLV. The object is not parsed. It can only handle integers and + * strings. + * + * @author zhang + * + */ +class Asn1Object { + + protected final int type; + protected final int length; + protected final byte[] value; + protected final int tag; + + /** + * Construct a ASN.1 TLV. The TLV could be either a constructed or primitive + * entity. + * + * <p/> + * The first byte in DER encoding is made of following fields, + * + * <pre> + * ------------------------------------------------- + * |Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1| + * ------------------------------------------------- + * | Class | CF | + Type | + * ------------------------------------------------- + * </pre> + * + * <ul> + * <li>Class: Universal, Application, Context or Private + * <li>CF: Constructed flag. If 1, the field is constructed. + * <li>Type: This is actually called tag in ASN.1. It indicates data type + * (Integer, String) or a construct (sequence, choice, set). + * </ul> + * + * @param tag + * Tag or Identifier + * @param length + * Length of the field + * @param value + * Encoded octet string for the field. + */ + public Asn1Object(int tag, int length, byte[] value) { + this.tag = tag; + this.type = tag & 0x1F; + this.length = length; + this.value = value; + } + + public int getType() { + return type; + } + + public int getLength() { + return length; + } + + public byte[] getValue() { + return value; + } + + public boolean isConstructed() { + return (tag & DerParser.CONSTRUCTED) == DerParser.CONSTRUCTED; + } + + /** + * For constructed field, return a parser for its content. + * + * @return A parser for the construct. + * @throws IOException + * IOException resulted from invalid file IO + */ + public DerParser getParser() throws IOException { + if (!isConstructed()) + throw new IOException("Invalid DER: can't parse primitive entity"); //$NON-NLS-1$ + + return new DerParser(value); + } + + /** + * Get the value as integer + * + * @return BigInteger + * @throws IOException + * IOException resulted from invalid file IO + */ + public BigInteger getInteger() throws IOException { + if (type != DerParser.INTEGER) + throw new IOException("Invalid DER: object is not integer"); //$NON-NLS-1$ + + return new BigInteger(value); + } + + /** + * Get value as string. Most strings are treated as Latin-1. + * + * @return Java string + * @throws IOException + * IOException resulted from invalid file IO + */ + public String getString() throws IOException { + + String encoding; + + switch (type) { + + // Not all are Latin-1 but it's the closest thing + case DerParser.NUMERIC_STRING: + case DerParser.PRINTABLE_STRING: + case DerParser.VIDEOTEX_STRING: + case DerParser.IA5_STRING: + case DerParser.GRAPHIC_STRING: + case DerParser.ISO646_STRING: + case DerParser.GENERAL_STRING: + encoding = "ISO-8859-1"; //$NON-NLS-1$ + break; + + case DerParser.BMP_STRING: + encoding = "UTF-16BE"; //$NON-NLS-1$ + break; + + case DerParser.UTF8_STRING: + encoding = "UTF-8"; //$NON-NLS-1$ + break; + + case DerParser.UNIVERSAL_STRING: + throw new IOException("Invalid DER: can't handle UCS-4 string"); //$NON-NLS-1$ + + default: + throw new IOException("Invalid DER: object is not a string"); //$NON-NLS-1$ + } + + return new String(value, encoding); + } +} \ No newline at end of file diff --git a/src/main/java/com/backend/glowhouse/model/DeviceShadow.java b/src/main/java/com/backend/glowhouse/model/DeviceShadow.java new file mode 100644 index 0000000000000000000000000000000000000000..b0762d16216a591a99e2aec24e264de0031a6dde --- /dev/null +++ b/src/main/java/com/backend/glowhouse/model/DeviceShadow.java @@ -0,0 +1,32 @@ +package com.backend.glowhouse.model; + +import com.amazonaws.services.iot.client.AWSIotDevice; +import com.amazonaws.services.iot.client.AWSIotDeviceProperty; + +public class DeviceShadow extends AWSIotDevice{ + @AWSIotDeviceProperty + private boolean light; + + @AWSIotDeviceProperty + private boolean pump; + + public DeviceShadow(String thingName) { + super(thingName); + } + + public boolean isLight() { + return light; + } + + public void setLight(boolean light) { + this.light = light; + } + + public boolean isPump() { + return pump; + } + + public void setPump(boolean pump) { + this.pump = pump; + } +}