mirror of
https://github.com/whowechina/diva_pico.git
synced 2026-10-01 11:18:12 +03:00
PS4key import
This commit is contained in:
+18
-11
@@ -67,7 +67,16 @@ static void disp_hid()
|
||||
{
|
||||
const char *joy_map[] = {"Switch", "Steam", "Arcade", "PS4"};
|
||||
printf("[HID]\n");
|
||||
printf(" Keymap: %s\n", joy_map[diva_cfg->hid.joy_map % 4]);
|
||||
|
||||
int joy = diva_cfg->hid.joy_map % 4;
|
||||
|
||||
const char *key_info = "\0";
|
||||
if (joy == 3) {
|
||||
const ps4key_t *key = (const ps4key_t *)savedata_get_global();
|
||||
key_info = ps4key_key_valid(key) ? " (Key loaded)" : " (No key, disconnected every 8 minutes)";
|
||||
}
|
||||
|
||||
printf(" Keymap: %s%s\n", joy_map[joy], key_info);
|
||||
}
|
||||
|
||||
void handle_display(int argc, char *argv[])
|
||||
@@ -431,7 +440,7 @@ static void handle_factory_reset()
|
||||
|
||||
static void handle_ps4key(int argc, char *argv[])
|
||||
{
|
||||
static ps4key_data_t ps4key_data;
|
||||
static ps4key_t key;
|
||||
const char *usage = "Usage: ps4key <serialized_key|clear>\n";
|
||||
|
||||
if (argc != 1) {
|
||||
@@ -440,23 +449,21 @@ static void handle_ps4key(int argc, char *argv[])
|
||||
}
|
||||
|
||||
if (strncasecmp(argv[0], "clear", strlen(argv[0])) == 0) {
|
||||
ps4key_clear_data(&ps4key_data);
|
||||
memset(&key, 0, sizeof(key));
|
||||
savedata_clear_global();
|
||||
printf("PS4 key data cleared.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
const char *error = NULL;
|
||||
if (!ps4key_parse_text(argv[0], &ps4key_data, &error)) {
|
||||
printf("PS4 key import failed: %s\n", error ? error : "Unknown error.");
|
||||
const char *error = "Unknown";
|
||||
if (!ps4key_parse_text(argv[0], &key, &error)) {
|
||||
printf("PS4 key import failed: %s\n", error);
|
||||
return;
|
||||
}
|
||||
|
||||
savedata_write_global(&ps4key_data, sizeof(ps4key_data));
|
||||
printf("PS4 key stored: serial=%s pem=%u bytes sig=%u bytes.\n",
|
||||
ps4key_data.serial,
|
||||
(unsigned)ps4key_data.pem_len,
|
||||
(unsigned)ps4key_data.sig_len);
|
||||
savedata_write_global(&key, sizeof(key));
|
||||
printf("PS4 key stored: Serial: %s, PEM: %d bytes, sig: %d bytes\n",
|
||||
ps4key_get_serial(&key), key.pem_len - 1, key.sig_len);
|
||||
}
|
||||
|
||||
void commands_init()
|
||||
|
||||
+115
-95
@@ -1,16 +1,13 @@
|
||||
#include "ps4key.h"
|
||||
/*
|
||||
* PS4 Key import and de-serialization
|
||||
* WHowe <github.com/whowechina>
|
||||
*/
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t magic[4];
|
||||
uint8_t version;
|
||||
uint16_t serial_len;
|
||||
uint16_t pem_len;
|
||||
uint16_t sig_len;
|
||||
uint32_t crc32;
|
||||
} ps4key_wire_header_t;
|
||||
#include "ps4key.h"
|
||||
|
||||
static uint32_t crc32_calc(const uint8_t *data, size_t size)
|
||||
{
|
||||
@@ -109,133 +106,156 @@ static bool base64_decode(const char *input, uint8_t *output,
|
||||
|
||||
static bool serial_is_valid(const uint8_t *serial, size_t size)
|
||||
{
|
||||
if (size != PS4KEY_SERIAL_LENGTH) {
|
||||
if (size != (PS4KEY_SERIAL_LENGTH + 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
for (size_t i = 0; i < PS4KEY_SERIAL_LENGTH; i++) {
|
||||
if (!isdigit(serial[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (serial[PS4KEY_SERIAL_LENGTH] != '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool pem_is_valid(const char *pem)
|
||||
{
|
||||
return strstr(pem, "-----BEGIN RSA PRIVATE KEY-----") != NULL &&
|
||||
strstr(pem, "-----END RSA PRIVATE KEY-----") != NULL;
|
||||
return strstr(pem, "-----BEGIN RSA PRIVATE KEY-----") &&
|
||||
strstr(pem, "-----END RSA PRIVATE KEY-----");
|
||||
}
|
||||
|
||||
void ps4key_clear_data(ps4key_data_t *data)
|
||||
static void set_error(const char **error, const char *message)
|
||||
{
|
||||
if (data == NULL) {
|
||||
return;
|
||||
if (error) {
|
||||
*error = message;
|
||||
}
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
bool ps4key_parse_text(const char *text, ps4key_data_t *data, const char **error)
|
||||
static bool ps4key_validate(const ps4key_t *key, const char **error)
|
||||
{
|
||||
static uint8_t decoded[sizeof(ps4key_wire_header_t) +
|
||||
PS4KEY_SERIAL_LENGTH +
|
||||
PS4KEY_PEM_MAX_LENGTH +
|
||||
PS4KEY_SIG_MAX_LENGTH];
|
||||
|
||||
if ((text == NULL) || (data == NULL)) {
|
||||
if (error) {
|
||||
*error = "Missing input.";
|
||||
}
|
||||
if (key == NULL) {
|
||||
set_error(error, "Missing input.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strncmp(text, PS4KEY_TEXT_PREFIX, strlen(PS4KEY_TEXT_PREFIX)) != 0) {
|
||||
if (error) {
|
||||
*error = "Missing PS4K1 prefix.";
|
||||
}
|
||||
if ((key->magic != PS4KEY_STORAGE_MAGIC) ||
|
||||
(key->version != PS4KEY_STORAGE_VERSION)) {
|
||||
set_error(error, "Unexpected serialization header.");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t decoded_len = 0;
|
||||
if (!base64_decode(text + strlen(PS4KEY_TEXT_PREFIX), decoded,
|
||||
sizeof(decoded), &decoded_len)) {
|
||||
if (error) {
|
||||
*error = "Base64 decode failed.";
|
||||
}
|
||||
if ((key->serial_len != (PS4KEY_SERIAL_LENGTH + 1)) ||
|
||||
(key->pem_len <= 1) || (key->pem_len > (PS4KEY_PEM_MAX_LENGTH + 1)) ||
|
||||
(key->sig_len == 0) || (key->sig_len > PS4KEY_SIG_MAX_LENGTH)) {
|
||||
set_error(error, "Serialized part lengths are invalid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (decoded_len < sizeof(ps4key_wire_header_t)) {
|
||||
if (error) {
|
||||
*error = "Serialized data is too short.";
|
||||
}
|
||||
size_t payload_len = key->serial_len + key->pem_len + key->sig_len;
|
||||
if (payload_len > sizeof(key->payload)) {
|
||||
set_error(error, "Serialized length mismatch.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const ps4key_wire_header_t *header = (const ps4key_wire_header_t *)decoded;
|
||||
if ((memcmp(header->magic, "PS4K", 4) != 0) ||
|
||||
(header->version != PS4KEY_STORAGE_VERSION)) {
|
||||
if (error) {
|
||||
*error = "Unexpected serialization header.";
|
||||
}
|
||||
if (crc32_calc(key->payload, payload_len) != key->crc32) {
|
||||
set_error(error, "CRC32 mismatch.");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t payload_len = (size_t)header->serial_len + header->pem_len + header->sig_len;
|
||||
if (decoded_len != sizeof(*header) + payload_len) {
|
||||
if (error) {
|
||||
*error = "Serialized length mismatch.";
|
||||
}
|
||||
if (!serial_is_valid(key->payload, key->serial_len)) {
|
||||
set_error(error, "Invalid serial.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((header->serial_len != PS4KEY_SERIAL_LENGTH) ||
|
||||
(header->pem_len == 0) || (header->pem_len > PS4KEY_PEM_MAX_LENGTH) ||
|
||||
(header->sig_len == 0) || (header->sig_len > PS4KEY_SIG_MAX_LENGTH)) {
|
||||
if (error) {
|
||||
*error = "Serialized part lengths are invalid.";
|
||||
}
|
||||
const uint8_t *pem_ptr = key->payload + key->serial_len;
|
||||
if (pem_ptr[key->pem_len - 1] != '\0') {
|
||||
set_error(error, "PEM data is not null terminated.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t *payload = decoded + sizeof(*header);
|
||||
if (crc32_calc(payload, payload_len) != header->crc32) {
|
||||
if (error) {
|
||||
*error = "CRC32 mismatch.";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!serial_is_valid(payload, header->serial_len)) {
|
||||
if (error) {
|
||||
*error = "Serial must be exactly 16 decimal digits.";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *pem = (const char *)(payload + header->serial_len);
|
||||
const uint8_t *sig = payload + header->serial_len + header->pem_len;
|
||||
|
||||
ps4key_clear_data(data);
|
||||
data->magic = PS4KEY_STORAGE_MAGIC;
|
||||
data->version = PS4KEY_STORAGE_VERSION;
|
||||
data->serial_len = header->serial_len;
|
||||
data->pem_len = header->pem_len;
|
||||
data->sig_len = header->sig_len;
|
||||
data->crc32 = header->crc32;
|
||||
memcpy(data->serial, payload, header->serial_len);
|
||||
memcpy(data->pem, pem, header->pem_len);
|
||||
memcpy(data->sig, sig, header->sig_len);
|
||||
data->serial[header->serial_len] = '\0';
|
||||
data->pem[header->pem_len] = '\0';
|
||||
|
||||
if (!pem_is_valid(data->pem)) {
|
||||
if (error) {
|
||||
*error = "PEM content does not look like an RSA private key.";
|
||||
}
|
||||
ps4key_clear_data(data);
|
||||
if (!pem_is_valid((const char *)pem_ptr)) {
|
||||
set_error(error, "PEM content does not look like an RSA private key.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ps4key_parse_text(const char *text, ps4key_t *key, const char **error)
|
||||
{
|
||||
static uint8_t decoded[sizeof(ps4key_t)];
|
||||
|
||||
if ((text == NULL) || (key == NULL)) {
|
||||
set_error(error, "Missing input.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strncmp(text, PS4KEY_TEXT_PREFIX, strlen(PS4KEY_TEXT_PREFIX)) != 0) {
|
||||
set_error(error, "Missing PS4K1 prefix.");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t decoded_len = 0;
|
||||
if (!base64_decode(text + strlen(PS4KEY_TEXT_PREFIX), decoded, sizeof(decoded), &decoded_len)) {
|
||||
set_error(error, "Base64 decode failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t header_len = offsetof(ps4key_t, payload);
|
||||
if (decoded_len < header_len) {
|
||||
set_error(error, "Serialized data is too short.");
|
||||
return false;
|
||||
}
|
||||
const ps4key_t *src = (const ps4key_t *)decoded;
|
||||
size_t payload_len = src->serial_len + src->pem_len + src->sig_len;
|
||||
if (decoded_len != header_len + payload_len) {
|
||||
set_error(error, "Serialized length mismatch.");
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(key, 0, sizeof(*key));
|
||||
memcpy(key, decoded, decoded_len);
|
||||
|
||||
return ps4key_validate(key, error);
|
||||
}
|
||||
|
||||
bool ps4key_key_valid(const ps4key_t *key)
|
||||
{
|
||||
return ps4key_validate(key, NULL);
|
||||
}
|
||||
|
||||
const char *ps4key_get_serial(const ps4key_t *key)
|
||||
{
|
||||
if (key == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
return (const char *)key->payload;
|
||||
}
|
||||
|
||||
const char *ps4key_get_pem(const ps4key_t *key)
|
||||
{
|
||||
if (key == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
return (const char *)(key->payload + key->serial_len);
|
||||
}
|
||||
|
||||
const uint8_t *ps4key_get_sig(const ps4key_t *key)
|
||||
{
|
||||
if (key == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
return key->payload + key->serial_len + key->pem_len;
|
||||
}
|
||||
|
||||
size_t ps4key_get_sig_len(const ps4key_t *key)
|
||||
{
|
||||
if (key == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return key->sig_len;
|
||||
}
|
||||
|
||||
+14
-8
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* PS4 key serialization support
|
||||
* PS4 Key import and de-serialization
|
||||
* WHowe <github.com/whowechina>
|
||||
*/
|
||||
|
||||
#ifndef PS4KEY_H
|
||||
@@ -15,20 +16,25 @@
|
||||
#define PS4KEY_SERIAL_LENGTH 16
|
||||
#define PS4KEY_PEM_MAX_LENGTH 2048
|
||||
#define PS4KEY_SIG_MAX_LENGTH 512
|
||||
#define PS4KEY_PAYLOAD_MAX_LENGTH (PS4KEY_SERIAL_LENGTH + 1 + PS4KEY_PEM_MAX_LENGTH + 1 + PS4KEY_SIG_MAX_LENGTH)
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint8_t version;
|
||||
uint8_t reserved;
|
||||
uint16_t serial_len;
|
||||
uint16_t pem_len;
|
||||
uint16_t sig_len;
|
||||
uint32_t crc32;
|
||||
char serial[PS4KEY_SERIAL_LENGTH + 1];
|
||||
char pem[PS4KEY_PEM_MAX_LENGTH + 1];
|
||||
uint8_t sig[PS4KEY_SIG_MAX_LENGTH];
|
||||
} ps4key_data_t;
|
||||
uint8_t payload[PS4KEY_PAYLOAD_MAX_LENGTH];
|
||||
} ps4key_t;
|
||||
|
||||
void ps4key_clear_data(ps4key_data_t *data);
|
||||
bool ps4key_parse_text(const char *text, ps4key_data_t *data, const char **error);
|
||||
bool ps4key_parse_text(const char *text, ps4key_t *key, const char **error);
|
||||
bool ps4key_key_valid(const ps4key_t *key);
|
||||
|
||||
const char *ps4key_get_serial(const ps4key_t *key);
|
||||
const char *ps4key_get_pem(const ps4key_t *key);
|
||||
const uint8_t *ps4key_get_sig(const ps4key_t *key);
|
||||
size_t ps4key_get_sig_len(const ps4key_t *key);
|
||||
|
||||
#endif
|
||||
+4
-11
@@ -32,7 +32,7 @@ static uint32_t my_magic = 0xcafecafe;
|
||||
#define SAVE_TIMEOUT_US 5000000
|
||||
|
||||
#define SAVE_SECTOR_OFFSET (PICO_FLASH_SIZE_BYTES - FLASH_SECTOR_SIZE)
|
||||
#define GLOBAL_SECTOR_NUM 2
|
||||
#define GLOBAL_SECTOR_NUM 1
|
||||
#define GLOBAL_SECTOR_SIZE (GLOBAL_SECTOR_NUM * FLASH_SECTOR_SIZE)
|
||||
#define GLOBAL_SECTOR_OFFSET (SAVE_SECTOR_OFFSET - GLOBAL_SECTOR_SIZE)
|
||||
|
||||
@@ -164,16 +164,9 @@ void savedata_request(bool immediately)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void savedata_read_global(size_t offset, void *data, size_t size)
|
||||
void *savedata_get_global()
|
||||
{
|
||||
if ((data == NULL) || (size == 0) || (offset >= GLOBAL_SECTOR_SIZE)) {
|
||||
return;
|
||||
}
|
||||
if (size > GLOBAL_SECTOR_SIZE - offset) {
|
||||
size = GLOBAL_SECTOR_SIZE - offset;
|
||||
}
|
||||
memcpy(data, (void *)(XIP_BASE + GLOBAL_SECTOR_OFFSET + offset), size);
|
||||
return (void *)(XIP_BASE + GLOBAL_SECTOR_OFFSET);
|
||||
}
|
||||
|
||||
static void do_write_global(void *param)
|
||||
@@ -211,7 +204,7 @@ void savedata_write_global(const void *data, size_t size)
|
||||
param[0] = (uintptr_t)global_write_buffer;
|
||||
param[1] = padded;
|
||||
|
||||
printf("Program Global %8x ", GLOBAL_SECTOR_OFFSET);
|
||||
printf("Program Global %08x ", GLOBAL_SECTOR_OFFSET);
|
||||
if (flash_safe_execute(do_write_global, param, 1000) != PICO_OK) {
|
||||
printf("Failed!\n");
|
||||
} else {
|
||||
|
||||
@@ -20,7 +20,7 @@ void savedata_loop();
|
||||
void *savedata_alloc(size_t size, void *def, void (*after_load)());
|
||||
void savedata_request(bool immediately);
|
||||
|
||||
void savedata_read_global(size_t offset, void *data, size_t size);
|
||||
void *savedata_get_global();
|
||||
void savedata_write_global(const void *data, size_t size);
|
||||
void savedata_clear_global();
|
||||
size_t savedata_global_size();
|
||||
|
||||
+98
-97
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PS4 Key Serialization for Diva Pico</title>
|
||||
<title>PS4 Key Packer for Diva Pico</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4efe6;
|
||||
@@ -47,8 +47,8 @@
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 4vw, 3.8rem);
|
||||
line-height: 0.95;
|
||||
font-size: clamp(1.5rem, 3vw, 2.4rem);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
@@ -69,13 +69,19 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grid {
|
||||
.panel-main {
|
||||
margin-top: 12px;
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
.panel-top {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 24px;
|
||||
padding: 24px 24px 2px;
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
|
||||
@@ -83,6 +89,29 @@
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.button-section {
|
||||
padding: 2px 24px 24px;
|
||||
border-right: 1px solid var(--line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-section:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.button-section.left,
|
||||
.button-section.right {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1rem;
|
||||
@@ -130,43 +159,28 @@
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.output-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 14px 22px;
|
||||
padding: 14px 88px;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 24px rgba(15, 118, 110, 0.10);
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, opacity 180ms ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
opacity: 0.96;
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: #f7fffe;
|
||||
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%);
|
||||
box-shadow: 0 14px 30px rgba(15, 118, 110, 0.24);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--ink);
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1px solid var(--line);
|
||||
.primay {
|
||||
color: var(--accent-strong);
|
||||
background: linear-gradient(135deg, #e6f7f5 0%, #d2f1ed 100%);
|
||||
border: 1px solid #b6e2de;
|
||||
box-shadow: 0 8px 24px rgba(15, 118, 110, 0.10);
|
||||
padding: 14px 88px;
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +192,7 @@
|
||||
.status.output-status {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
text-align: right;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status.warn {
|
||||
@@ -215,60 +229,48 @@
|
||||
body {
|
||||
padding: 20px 14px 32px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section class="hero">
|
||||
<h1>PS4 Key Serialization for Diva Pico</h1>
|
||||
<p class="subtitle">Pack <strong>signature.bin</strong>, <strong>serial.txt</strong>, and <strong>key.pem</strong> into one copy-friendly plain text string for the new <strong>ps4key</strong> CLI command. The format is <strong>PS4K1 + Base64(binary payload)</strong> with a CRC32 over the raw payload bytes.</p>
|
||||
<h1>PS4 Key Packer for Diva Pico</h1>
|
||||
</section>
|
||||
|
||||
<section class="panel grid">
|
||||
<div class="section">
|
||||
<h2>Input</h2>
|
||||
<p>Provide the binary signature, the 16-digit serial, and the RSA private key text. Validation is intentionally strict to catch common mistakes before flashing.</p>
|
||||
<section class="panel panel-main">
|
||||
<div class="panel-top">
|
||||
<div class="section">
|
||||
<h2>Input</h2>
|
||||
<p>Provide the binary signature, the serial, and the RSA private key files.</p>
|
||||
|
||||
<label for="sigFile">signature.bin file</label>
|
||||
<input id="sigFile" type="file" accept=".bin,application/octet-stream">
|
||||
|
||||
<label for="sigFile">signature.bin 文件</label>
|
||||
<input id="sigFile" type="file" accept=".bin,application/octet-stream">
|
||||
<label for="serialFile">serial.txt file</label>
|
||||
<input id="serialFile" type="file" accept=".txt,text/plain">
|
||||
|
||||
<label for="serialFile">serial.txt 文件</label>
|
||||
<input id="serialFile" type="file" accept=".txt,text/plain">
|
||||
<label for="pemFile">key.pem file</label>
|
||||
<input id="pemFile" type="file" accept=".pem,text/plain">
|
||||
|
||||
<label for="pemFile">key.pem 文件</label>
|
||||
<input id="pemFile" type="file" accept=".pem,text/plain">
|
||||
|
||||
<div class="actions">
|
||||
<button id="packButton" class="primary" type="button">Pack</button>
|
||||
<div id="status" class="status" aria-live="polite"></div>
|
||||
<div id="meta" class="meta"></div>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status" aria-live="polite"></div>
|
||||
<div id="meta" class="meta"></div>
|
||||
<div class="section">
|
||||
<h2>Output</h2>
|
||||
<textarea id="output" class="output" readonly wrap="soft" placeholder="Packed text will appear here."></textarea>
|
||||
<div id="outputStatus" class="status output-status" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Output</h2>
|
||||
<p>Single-line plain text, suitable for direct paste into the CLI as <strong>ps4key <serialized_key></strong>.</p>
|
||||
<textarea id="output" class="output" readonly wrap="soft" placeholder="Packed text will appear here."></textarea>
|
||||
<div class="output-actions">
|
||||
<button id="copyButton" class="secondary" type="button">Copy Output</button>
|
||||
<div class="button-row">
|
||||
<div class="button-section left">
|
||||
<button id="packButton" class="primay" type="button">Pack the Files</button>
|
||||
</div>
|
||||
<div class="button-section right">
|
||||
<button id="copyButton" class="primay" type="button">Copy to Clipboard</button>
|
||||
</div>
|
||||
<div id="outputStatus" class="status output-status" aria-live="polite"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -316,20 +318,20 @@
|
||||
|
||||
function assertInputs(serial, pem, sigBytes) {
|
||||
if (!sigBytes || sigBytes.length === 0) {
|
||||
throw new Error("请上传 signature.bin 二进制文件。");
|
||||
throw new Error("Please upload the signature.bin file.");
|
||||
}
|
||||
if (sigBytes.length > SIG_MAX_LENGTH) {
|
||||
throw new Error(`signature.bin 二进制文件过大,最大 ${SIG_MAX_LENGTH} 字节。`);
|
||||
throw new Error(`signature.bin is too large, max ${SIG_MAX_LENGTH} bytes.`);
|
||||
}
|
||||
if (!/^\d{16}$/.test(serial)) {
|
||||
throw new Error("serial.txt 必须是 16 位十进制字符串。");
|
||||
throw new Error("serial.txt must be a 16-digit decimal string.");
|
||||
}
|
||||
if (!pem.includes("-----BEGIN RSA PRIVATE KEY-----") ||
|
||||
!pem.includes("-----END RSA PRIVATE KEY-----")) {
|
||||
throw new Error("key.pem 必须包含 BEGIN/END RSA PRIVATE KEY 标记。");
|
||||
throw new Error("key.pem must contain BEGIN/END RSA PRIVATE KEY markers.");
|
||||
}
|
||||
if (pem.length === 0 || pem.length > PEM_MAX_LENGTH) {
|
||||
throw new Error(`key.pem 长度必须在 1-${PEM_MAX_LENGTH} 字符之间。`);
|
||||
throw new Error(`key.pem length must be 1-${PEM_MAX_LENGTH} characters.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,20 +359,25 @@
|
||||
function packPayload(serial, pem, sigBytes) {
|
||||
const serialBytes = encoder.encode(serial);
|
||||
const pemBytes = encoder.encode(pem);
|
||||
const payloadLength = serialBytes.length + pemBytes.length + sigBytes.length;
|
||||
const serialLen = serialBytes.length + 1;
|
||||
const pemLen = pemBytes.length + 1;
|
||||
const payloadLength = serialLen + pemLen + sigBytes.length;
|
||||
const payload = new Uint8Array(payloadLength);
|
||||
payload.set(serialBytes, 0);
|
||||
payload.set(pemBytes, serialBytes.length);
|
||||
payload.set(sigBytes, serialBytes.length + pemBytes.length);
|
||||
payload[serialBytes.length] = 0;
|
||||
payload.set(pemBytes, serialLen);
|
||||
payload[serialLen + pemBytes.length] = 0;
|
||||
payload.set(sigBytes, serialLen + pemLen);
|
||||
|
||||
const header = new Uint8Array(15);
|
||||
const header = new Uint8Array(16);
|
||||
header.set([0x50, 0x53, 0x34, 0x4b], 0);
|
||||
header[4] = 1;
|
||||
header[5] = 0;
|
||||
const view = new DataView(header.buffer);
|
||||
view.setUint16(5, serialBytes.length, true);
|
||||
view.setUint16(7, pemBytes.length, true);
|
||||
view.setUint16(9, sigBytes.length, true);
|
||||
view.setUint32(11, crc32(payload), true);
|
||||
view.setUint16(6, serialLen, true);
|
||||
view.setUint16(8, pemLen, true);
|
||||
view.setUint16(10, sigBytes.length, true);
|
||||
view.setUint32(12, crc32(payload), true);
|
||||
|
||||
const packed = new Uint8Array(header.length + payload.length);
|
||||
packed.set(header, 0);
|
||||
@@ -393,17 +400,18 @@
|
||||
|
||||
async function doPack() {
|
||||
try {
|
||||
// 前置文件选择校验
|
||||
|
||||
// Pre-file selection validation
|
||||
if (!sigFileInput.files || !sigFileInput.files[0]) {
|
||||
setStatus("error", "请先选择 sig.bin 文件。");
|
||||
setStatus("error", "Please select sig.bin file.");
|
||||
return;
|
||||
}
|
||||
if (!serialFileInput.files || !serialFileInput.files[0]) {
|
||||
setStatus("error", "请先选择 serial.txt 文件。");
|
||||
setStatus("error", "Please select serial.txt file.");
|
||||
return;
|
||||
}
|
||||
if (!pemFileInput.files || !pemFileInput.files[0]) {
|
||||
setStatus("error", "请先选择 key.pem 文件。");
|
||||
setStatus("error", "Please select key.pem file.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,16 +419,17 @@
|
||||
const serialText = await readFileAsText(serialFileInput);
|
||||
const pemText = await readFileAsText(pemFileInput);
|
||||
|
||||
|
||||
if (!sigBytes || sigBytes.length === 0) {
|
||||
setStatus("error", "sig.bin 文件读取失败或为空。");
|
||||
setStatus("error", "sig.bin file read failed or empty.");
|
||||
return;
|
||||
}
|
||||
if (!serialText || serialText.trim().length === 0) {
|
||||
setStatus("error", "serial.txt 文件读取失败或为空。");
|
||||
setStatus("error", "serial.txt file read failed or empty.");
|
||||
return;
|
||||
}
|
||||
if (!pemText || pemText.trim().length === 0) {
|
||||
setStatus("error", "key.pem 文件读取失败或为空。");
|
||||
setStatus("error", "key.pem file read failed or empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -431,13 +440,7 @@
|
||||
|
||||
const packed = packPayload(serial, pem, sigBytes);
|
||||
output.value = packed;
|
||||
setMeta([
|
||||
`serial: ${serial.length} bytes`,
|
||||
`pem: ${pem.length} bytes`,
|
||||
`sig: ${sigBytes.length} bytes`,
|
||||
`output: ${packed.length} chars`
|
||||
]);
|
||||
setStatus("ok", "打包成功。");
|
||||
setStatus("ok", `Packed successfully, total ${packed.length} chars.`);
|
||||
} catch (error) {
|
||||
output.value = "";
|
||||
setMeta([]);
|
||||
@@ -447,7 +450,6 @@
|
||||
|
||||
async function copyOutput() {
|
||||
if (!output.value) {
|
||||
setOutputStatus("warn", "Nothing to copy.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -458,7 +460,6 @@
|
||||
} catch (error) {
|
||||
output.focus();
|
||||
output.select();
|
||||
setOutputStatus("warn", "Clipboard API is unavailable. The output has been selected.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user