]> git.basschouten.com Git - openhab-addons.git/blob
7592cd879cab71d8be7f29b018a304c73dcbf5ef
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.playstation.internal;
14
15 import java.nio.ByteBuffer;
16 import java.nio.ByteOrder;
17 import java.security.InvalidAlgorithmParameterException;
18 import java.security.InvalidKeyException;
19 import java.security.KeyFactory;
20 import java.security.NoSuchAlgorithmException;
21 import java.security.PublicKey;
22 import java.security.SecureRandom;
23 import java.security.spec.InvalidKeySpecException;
24 import java.security.spec.X509EncodedKeySpec;
25 import java.util.Base64;
26
27 import javax.crypto.BadPaddingException;
28 import javax.crypto.Cipher;
29 import javax.crypto.IllegalBlockSizeException;
30 import javax.crypto.NoSuchPaddingException;
31 import javax.crypto.spec.IvParameterSpec;
32 import javax.crypto.spec.SecretKeySpec;
33
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * The {@link PS4Crypto} is responsible for encryption and decryption of
41  * packets to / from the PS4.
42  *
43  * @author Fredrik Ahlström - Initial contribution
44  */
45 @NonNullByDefault
46 public class PS4Crypto {
47
48     private final Logger logger = LoggerFactory.getLogger(PS4Crypto.class);
49
50     // Public key is from ps4-waker (https://github.com/dhleong/ps4-waker)
51     private static final String PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----"
52             + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxfAO/MDk5ovZpp7xlG9J"
53             + "JKc4Sg4ztAz+BbOt6Gbhub02tF9bryklpTIyzM0v817pwQ3TCoigpxEcWdTykhDL"
54             + "cGhAbcp6E7Xh8aHEsqgtQ/c+wY1zIl3fU//uddlB1XuipXthDv6emXsyyU/tJWqc"
55             + "zy9HCJncLJeYo7MJvf2TE9nnlVm1x4flmD0k1zrvb3MONqoZbKb/TQVuVhBv7SM+"
56             + "U5PSi3diXIx1Nnj4vQ8clRNUJ5X1tT9XfVmKQS1J513XNZ0uYHYRDzQYujpLWucu"
57             + "ob7v50wCpUm3iKP1fYCixMP6xFm0jPYz1YQaMV35VkYwc40qgk3av0PDS+1G0dCm" + "swIDAQAB"
58             + "-----END PUBLIC KEY-----";
59
60     private final byte[] remoteSeed = new byte[16];
61     private final byte[] randomSeed = new byte[16];
62     private @Nullable Cipher ps4Cipher;
63     private @Nullable Cipher aesEncryptCipher;
64     private @Nullable Cipher aesDecryptCipher;
65
66     PS4Crypto() {
67         ps4Cipher = getRsaCipher(PUBLIC_KEY);
68     }
69
70     void clearCiphers() {
71         aesEncryptCipher = null;
72         aesDecryptCipher = null;
73     }
74
75     void initCiphers() {
76         new SecureRandom().nextBytes(randomSeed);
77         SecretKeySpec keySpec = new SecretKeySpec(randomSeed, "AES");
78         IvParameterSpec ivSpec = new IvParameterSpec(remoteSeed);
79         try {
80             Cipher encCipher = Cipher.getInstance("AES/CBC/NoPadding");
81             encCipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
82             Cipher decCipher = Cipher.getInstance("AES/CBC/NoPadding");
83             decCipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
84             logger.debug("Ciphers initialized.");
85             aesEncryptCipher = encCipher;
86             aesDecryptCipher = decCipher;
87         } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
88                 | InvalidAlgorithmParameterException e) {
89             logger.warn("Can not initialize ciphers.", e);
90         }
91     }
92
93     int parseHelloResponsePacket(ByteBuffer rBuffer) {
94         int result = -1;
95         rBuffer.rewind();
96         final int buffSize = rBuffer.remaining();
97         final int size = rBuffer.getInt();
98         if (size > buffSize || size < 12) {
99             logger.warn("Response size ({}) not good, buffer size ({}).", size, buffSize);
100             return result;
101         }
102         int cmdValue = rBuffer.getInt();
103         int statusValue = rBuffer.getInt();
104         PS4Command command = PS4Command.valueOfTag(cmdValue);
105         byte[] respBuff = new byte[size];
106         rBuffer.rewind();
107         rBuffer.get(respBuff);
108         if (command == PS4Command.HELLO_REQ) {
109             if (statusValue == PS4PacketHandler.REQ_VERSION) {
110                 rBuffer.position(20);
111                 rBuffer.get(remoteSeed, 0, 16);
112                 initCiphers();
113                 result = 0;
114             }
115         } else {
116             logger.debug("Unknown resp-cmd, size:{}, command:{}, status:{}, data:{}.", size, cmdValue, statusValue,
117                     respBuff);
118         }
119         return result;
120     }
121
122     ByteBuffer makeHandshakePacket() {
123         byte[] msg = null;
124         Cipher hsCipher = ps4Cipher;
125         if (hsCipher != null) {
126             try {
127                 msg = hsCipher.doFinal(randomSeed);
128             } catch (IllegalBlockSizeException | BadPaddingException e) {
129                 logger.debug("Cipher exception: {}", e.getMessage());
130             }
131         }
132         if (msg == null || msg.length != 256) {
133             return ByteBuffer.allocate(0);
134         }
135         ByteBuffer packet = PS4PacketHandler.newPacketOfSize(8 + 256 + 16, PS4Command.HANDSHAKE_REQ);
136         packet.put(msg);
137         packet.put(remoteSeed); // Seed = 16 bytes
138         packet.rewind();
139         return packet;
140     }
141
142     ByteBuffer encryptPacket(ByteBuffer packet) {
143         Cipher encCipher = aesEncryptCipher;
144         if (encCipher != null) {
145             return ByteBuffer.wrap(encCipher.update(packet.array()));
146         }
147         logger.debug("Not encrypting packet.");
148         return ByteBuffer.allocate(0);
149     }
150
151     ByteBuffer decryptPacket(ByteBuffer encBuffer) {
152         Cipher decCipher = aesDecryptCipher;
153         if (decCipher != null) {
154             byte[] respBuff = new byte[encBuffer.position()];
155             encBuffer.position(0);
156             encBuffer.get(respBuff, 0, respBuff.length);
157             byte[] data = decCipher.update(respBuff);
158             if (data != null) {
159                 return ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
160             }
161         }
162         logger.debug("Not decrypting response.");
163         return ByteBuffer.allocate(0);
164     }
165
166     private @Nullable Cipher getRsaCipher(String key) {
167         try {
168             String keyString = key.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");
169             byte[] keyData = Base64.getDecoder().decode(keyString);
170             KeyFactory keyFactory = KeyFactory.getInstance("RSA");
171             X509EncodedKeySpec x509keySpec = new X509EncodedKeySpec(keyData);
172             PublicKey publicKey = keyFactory.generatePublic(x509keySpec);
173             Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
174             cipher.init(Cipher.ENCRYPT_MODE, publicKey);
175             logger.debug("Initialized RSA public key cipher");
176             return cipher;
177         } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException e) {
178             logger.warn("Exception enabling RSA cipher.", e);
179             return null;
180         }
181     }
182 }