2 * Copyright (c) 2010-2023 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 = """
52 -----BEGIN PUBLIC KEY-----\
53 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxfAO/MDk5ovZpp7xlG9J\
54 JKc4Sg4ztAz+BbOt6Gbhub02tF9bryklpTIyzM0v817pwQ3TCoigpxEcWdTykhDL\
55 cGhAbcp6E7Xh8aHEsqgtQ/c+wY1zIl3fU//uddlB1XuipXthDv6emXsyyU/tJWqc\
56 zy9HCJncLJeYo7MJvf2TE9nnlVm1x4flmD0k1zrvb3MONqoZbKb/TQVuVhBv7SM+\
57 U5PSi3diXIx1Nnj4vQ8clRNUJ5X1tT9XfVmKQS1J513XNZ0uYHYRDzQYujpLWucu\
58 ob7v50wCpUm3iKP1fYCixMP6xFm0jPYz1YQaMV35VkYwc40qgk3av0PDS+1G0dCm\
60 -----END PUBLIC KEY-----\
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;
70 ps4Cipher = getRsaCipher(PUBLIC_KEY);
74 aesEncryptCipher = null;
75 aesDecryptCipher = null;
79 new SecureRandom().nextBytes(randomSeed);
80 SecretKeySpec keySpec = new SecretKeySpec(randomSeed, "AES");
81 IvParameterSpec ivSpec = new IvParameterSpec(remoteSeed);
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);
96 int parseHelloResponsePacket(ByteBuffer rBuffer) {
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);
105 int cmdValue = rBuffer.getInt();
106 int statusValue = rBuffer.getInt();
107 PS4Command command = PS4Command.valueOfTag(cmdValue);
108 byte[] respBuff = new byte[size];
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);
119 logger.debug("Unknown resp-cmd, size:{}, command:{}, status:{}, data:{}.", size, cmdValue, statusValue,
125 ByteBuffer makeHandshakePacket() {
127 Cipher hsCipher = ps4Cipher;
128 if (hsCipher != null) {
130 msg = hsCipher.doFinal(randomSeed);
131 } catch (IllegalBlockSizeException | BadPaddingException e) {
132 logger.debug("Cipher exception: {}", e.getMessage());
135 if (msg == null || msg.length != 256) {
136 return ByteBuffer.allocate(0);
138 ByteBuffer packet = PS4PacketHandler.newPacketOfSize(8 + 256 + 16, PS4Command.HANDSHAKE_REQ);
140 packet.put(remoteSeed); // Seed = 16 bytes
145 ByteBuffer encryptPacket(ByteBuffer packet) {
146 Cipher encCipher = aesEncryptCipher;
147 if (encCipher != null) {
148 return ByteBuffer.wrap(encCipher.update(packet.array()));
150 logger.debug("Not encrypting packet.");
151 return ByteBuffer.allocate(0);
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);
162 return ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
165 logger.debug("Not decrypting response.");
166 return ByteBuffer.allocate(0);
169 private @Nullable Cipher getRsaCipher(String key) {
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");
180 } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException e) {
181 logger.warn("Exception enabling RSA cipher.", e);