]> git.basschouten.com Git - openhab-addons.git/blob
95c9789226a83f65870aa7e7194db47227834d77
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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 = """
52             -----BEGIN PUBLIC KEY-----\
53             MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxfAO/MDk5ovZpp7xlG9J\
54             JKc4Sg4ztAz+BbOt6Gbhub02tF9bryklpTIyzM0v817pwQ3TCoigpxEcWdTykhDL\
55             cGhAbcp6E7Xh8aHEsqgtQ/c+wY1zIl3fU//uddlB1XuipXthDv6emXsyyU/tJWqc\
56             zy9HCJncLJeYo7MJvf2TE9nnlVm1x4flmD0k1zrvb3MONqoZbKb/TQVuVhBv7SM+\
57             U5PSi3diXIx1Nnj4vQ8clRNUJ5X1tT9XfVmKQS1J513XNZ0uYHYRDzQYujpLWucu\
58             ob7v50wCpUm3iKP1fYCixMP6xFm0jPYz1YQaMV35VkYwc40qgk3av0PDS+1G0dCm\
59             swIDAQAB\
60             -----END PUBLIC KEY-----\
61             """;
62
63     private final byte[] remoteSeed = new byte[16];
64     private final byte[] randomSeed = new byte[16];
65     private @Nullable Cipher ps4Cipher;
66     private @Nullable Cipher aesEncryptCipher;
67     private @Nullable Cipher aesDecryptCipher;
68
69     PS4Crypto() {
70         ps4Cipher = getRsaCipher(PUBLIC_KEY);
71     }
72
73     void clearCiphers() {
74         aesEncryptCipher = null;
75         aesDecryptCipher = null;
76     }
77
78     void initCiphers() {
79         new SecureRandom().nextBytes(randomSeed);
80         SecretKeySpec keySpec = new SecretKeySpec(randomSeed, "AES");
81         IvParameterSpec ivSpec = new IvParameterSpec(remoteSeed);
82         try {
83             Cipher encCipher = Cipher.getInstance("AES/CBC/NoPadding");
84             encCipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
85             Cipher decCipher = Cipher.getInstance("AES/CBC/NoPadding");
86             decCipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
87             logger.debug("Ciphers initialized.");
88             aesEncryptCipher = encCipher;
89             aesDecryptCipher = decCipher;
90         } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
91                 | InvalidAlgorithmParameterException e) {
92             logger.warn("Can not initialize ciphers.", e);
93         }
94     }
95
96     int parseHelloResponsePacket(ByteBuffer rBuffer) {
97         int result = -1;
98         rBuffer.rewind();
99         final int buffSize = rBuffer.remaining();
100         final int size = rBuffer.getInt();
101         if (size > buffSize || size < 12) {
102             logger.warn("Response size ({}) not good, buffer size ({}).", size, buffSize);
103             return result;
104         }
105         int cmdValue = rBuffer.getInt();
106         int statusValue = rBuffer.getInt();
107         PS4Command command = PS4Command.valueOfTag(cmdValue);
108         byte[] respBuff = new byte[size];
109         rBuffer.rewind();
110         rBuffer.get(respBuff);
111         if (command == PS4Command.HELLO_REQ) {
112             if (statusValue == PS4PacketHandler.REQ_VERSION) {
113                 rBuffer.position(20);
114                 rBuffer.get(remoteSeed, 0, 16);
115                 initCiphers();
116                 result = 0;
117             }
118         } else {
119             logger.debug("Unknown resp-cmd, size:{}, command:{}, status:{}, data:{}.", size, cmdValue, statusValue,
120                     respBuff);
121         }
122         return result;
123     }
124
125     ByteBuffer makeHandshakePacket() {
126         byte[] msg = null;
127         Cipher hsCipher = ps4Cipher;
128         if (hsCipher != null) {
129             try {
130                 msg = hsCipher.doFinal(randomSeed);
131             } catch (IllegalBlockSizeException | BadPaddingException e) {
132                 logger.debug("Cipher exception: {}", e.getMessage());
133             }
134         }
135         if (msg == null || msg.length != 256) {
136             return ByteBuffer.allocate(0);
137         }
138         ByteBuffer packet = PS4PacketHandler.newPacketOfSize(8 + 256 + 16, PS4Command.HANDSHAKE_REQ);
139         packet.put(msg);
140         packet.put(remoteSeed); // Seed = 16 bytes
141         packet.rewind();
142         return packet;
143     }
144
145     ByteBuffer encryptPacket(ByteBuffer packet) {
146         Cipher encCipher = aesEncryptCipher;
147         if (encCipher != null) {
148             return ByteBuffer.wrap(encCipher.update(packet.array()));
149         }
150         logger.debug("Not encrypting packet.");
151         return ByteBuffer.allocate(0);
152     }
153
154     ByteBuffer decryptPacket(ByteBuffer encBuffer) {
155         Cipher decCipher = aesDecryptCipher;
156         if (decCipher != null) {
157             byte[] respBuff = new byte[encBuffer.position()];
158             encBuffer.position(0);
159             encBuffer.get(respBuff, 0, respBuff.length);
160             byte[] data = decCipher.update(respBuff);
161             if (data != null) {
162                 return ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
163             }
164         }
165         logger.debug("Not decrypting response.");
166         return ByteBuffer.allocate(0);
167     }
168
169     private @Nullable Cipher getRsaCipher(String key) {
170         try {
171             String keyString = key.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "");
172             byte[] keyData = Base64.getDecoder().decode(keyString);
173             KeyFactory keyFactory = KeyFactory.getInstance("RSA");
174             X509EncodedKeySpec x509keySpec = new X509EncodedKeySpec(keyData);
175             PublicKey publicKey = keyFactory.generatePublic(x509keySpec);
176             Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
177             cipher.init(Cipher.ENCRYPT_MODE, publicKey);
178             logger.debug("Initialized RSA public key cipher");
179             return cipher;
180         } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException e) {
181             logger.warn("Exception enabling RSA cipher.", e);
182             return null;
183         }
184     }
185 }