2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.playstation.internal;
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;
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;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
40 * The {@link PS4Crypto} is responsible for encryption and decryption of
41 * packets to / from the PS4.
43 * @author Fredrik Ahlström - Initial contribution
46 public class PS4Crypto {
48 private final Logger logger = LoggerFactory.getLogger(PS4Crypto.class);
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-----";
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;
67 ps4Cipher = getRsaCipher(PUBLIC_KEY);
71 aesEncryptCipher = null;
72 aesDecryptCipher = null;
76 new SecureRandom().nextBytes(randomSeed);
77 SecretKeySpec keySpec = new SecretKeySpec(randomSeed, "AES");
78 IvParameterSpec ivSpec = new IvParameterSpec(remoteSeed);
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);
93 int parseHelloResponsePacket(ByteBuffer rBuffer) {
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);
102 int cmdValue = rBuffer.getInt();
103 int statusValue = rBuffer.getInt();
104 PS4Command command = PS4Command.valueOfTag(cmdValue);
105 byte[] respBuff = new byte[size];
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);
116 logger.debug("Unknown resp-cmd, size:{}, command:{}, status:{}, data:{}.", size, cmdValue, statusValue,
122 ByteBuffer makeHandshakePacket() {
124 Cipher hsCipher = ps4Cipher;
125 if (hsCipher != null) {
127 msg = hsCipher.doFinal(randomSeed);
128 } catch (IllegalBlockSizeException | BadPaddingException e) {
129 logger.debug("Cipher exception: {}", e.getMessage());
132 if (msg == null || msg.length != 256) {
133 return ByteBuffer.allocate(0);
135 ByteBuffer packet = PS4PacketHandler.newPacketOfSize(8 + 256 + 16, PS4Command.HANDSHAKE_REQ);
137 packet.put(remoteSeed); // Seed = 16 bytes
142 ByteBuffer encryptPacket(ByteBuffer packet) {
143 Cipher encCipher = aesEncryptCipher;
144 if (encCipher != null) {
145 return ByteBuffer.wrap(encCipher.update(packet.array()));
147 logger.debug("Not encrypting packet.");
148 return ByteBuffer.allocate(0);
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);
159 return ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
162 logger.debug("Not decrypting response.");
163 return ByteBuffer.allocate(0);
166 private @Nullable Cipher getRsaCipher(String key) {
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");
177 } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException e) {
178 logger.warn("Exception enabling RSA cipher.", e);