#include "stdafx.h" #include "Dimensions.h" #include #include "cellos/sys_usbd.h" LOG_CHANNEL(dimensions_log, "dimensions"); dimensions_toypad g_dimensionstoypad; static constexpr std::array COMMAND_KEY = {0x55, 0xFE, 0xF6, 0xB0, 0x62, 0xBF, 0x0B, 0x41, 0xC9, 0xB3, 0x7C, 0xB4, 0x97, 0x3E, 0x29, 0x7B}; static constexpr std::array CHAR_CONSTANT = {0xB7, 0xD5, 0xD7, 0xE6, 0xE7, 0xBA, 0x3C, 0xA8, 0xD8, 0x75, 0x47, 0x68, 0xCF, 0x23, 0xE9, 0xFE, 0xAA}; static constexpr std::array PWD_CONSTANT = {0x28, 0x63, 0x29, 0x20, 0x43, 0x6F, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x4C, 0x45, 0x47, 0x4F, 0x20, 0x32, 0x30, 0x31, 0x34, 0xAA, 0xAA}; void dimensions_figure::save() { if (!dim_file) { dimensions_log.error("Tried to save infinity figure to file but no infinity figure is active!"); return; } dim_file.seek(0, fs::seek_set); dim_file.write(data.data(), 0x2D * 0x04); } u8 dimensions_toypad::generate_checksum(const std::array& data, u32 num_of_bytes) { int checksum = 0; ensure(num_of_bytes <= data.size()); for (u8 i = 0; i < num_of_bytes; i++) { checksum += data[i]; } return (checksum & 0xFF); } void dimensions_toypad::get_blank_response(u8 type, u8 sequence, std::array& reply_buf) { reply_buf[0] = 0x55; reply_buf[1] = type; reply_buf[2] = sequence; reply_buf[3] = generate_checksum(reply_buf, 3); } void dimensions_toypad::generate_random_number(const u8* buf, u8 sequence, std::array& reply_buf) { // Decrypt payload into an 8 byte array const std::array value = decrypt(buf, std::nullopt); // Seed is the first 4 bytes (little endian) of the decrypted payload const u32 seed = read_from_ptr>(value); // Confirmation is the second 4 bytes (big endian) of the decrypted payload const u32 conf = read_from_ptr>(value, 4); // Initialize rng using the seed from decrypted payload initialize_rng(seed); std::array value_to_encrypt = {}; // Encrypt 8 bytes, first 4 bytes is the decrypted confirmation from payload, 2nd 4 bytes are blank write_to_ptr>(value_to_encrypt, conf); const std::array encrypted = encrypt(value_to_encrypt.data(), std::nullopt); reply_buf[0] = 0x55; reply_buf[1] = 0x09; reply_buf[2] = sequence; // Copy encrypted value to response data std::memcpy(&reply_buf[3], encrypted.data(), encrypted.size()); reply_buf[11] = generate_checksum(reply_buf, 11); } void dimensions_toypad::initialize_rng(u32 seed) { m_random_a = 0xF1EA5EED; m_random_b = seed; m_random_c = seed; m_random_d = seed; for (int i = 0; i < 42; i++) { get_next(); } } u32 dimensions_toypad::get_next() { const u32 e = m_random_a - std::rotl(m_random_b, 21); m_random_a = m_random_b ^ std::rotl(m_random_c, 19); m_random_b = m_random_c + std::rotl(m_random_d, 6); m_random_c = m_random_d + e; m_random_d = e + m_random_a; return m_random_d; } std::array dimensions_toypad::decrypt(const u8* buf, std::optional> key) { // Value to decrypt is separated in to two little endian 32 bit unsigned integers u32 data_one = read_from_ptr>(buf); u32 data_two = read_from_ptr>(buf, 4); // Use the key as 4 32 bit little endian unsigned integers u32 key_one; u32 key_two; u32 key_three; u32 key_four; if (key) { key_one = read_from_ptr>(key.value()); key_two = read_from_ptr>(key.value(), 4); key_three = read_from_ptr>(key.value(), 8); key_four = read_from_ptr>(key.value(), 12); } else { key_one = read_from_ptr>(COMMAND_KEY); key_two = read_from_ptr>(COMMAND_KEY, 4); key_three = read_from_ptr>(COMMAND_KEY, 8); key_four = read_from_ptr>(COMMAND_KEY, 12); } u32 sum = 0xC6EF3720; constexpr u32 delta = 0x9E3779B9; for (int i = 0; i < 32; i++) { data_two -= (((data_one << 4) + key_three) ^ (data_one + sum) ^ ((data_one >> 5) + key_four)); data_one -= (((data_two << 4) + key_one) ^ (data_two + sum) ^ ((data_two >> 5) + key_two)); sum -= delta; } ensure(sum == 0, "Decryption failed, sum inequal to 0"); std::array decrypted = {u8(data_one & 0xFF), u8((data_one >> 8) & 0xFF), u8((data_one >> 16) & 0xFF), u8((data_one >> 24) & 0xFF), u8(data_two & 0xFF), u8((data_two >> 8) & 0xFF), u8((data_two >> 16) & 0xFF), u8((data_two >> 24) & 0xFF)}; return decrypted; } std::array dimensions_toypad::encrypt(const u8* buf, std::optional> key) { // Value to encrypt is separated in to two little endian 32 bit unsigned integers u32 data_one = read_from_ptr>(buf); u32 data_two = read_from_ptr>(buf, 4); // Use the key as 4 32 bit little endian unsigned integers u32 key_one; u32 key_two; u32 key_three; u32 key_four; if (key) { key_one = read_from_ptr>(key.value()); key_two = read_from_ptr>(key.value(), 4); key_three = read_from_ptr>(key.value(), 8); key_four = read_from_ptr>(key.value(), 12); } else { key_one = read_from_ptr>(COMMAND_KEY); key_two = read_from_ptr>(COMMAND_KEY, 4); key_three = read_from_ptr>(COMMAND_KEY, 8); key_four = read_from_ptr>(COMMAND_KEY, 12); } u32 sum = 0; constexpr u32 delta = 0x9E3779B9; for (int i = 0; i < 32; i++) { sum += delta; data_one += (((data_two << 4) + key_one) ^ (data_two + sum) ^ ((data_two >> 5) + key_two)); data_two += (((data_one << 4) + key_three) ^ (data_one + sum) ^ ((data_one >> 5) + key_four)); } ensure(sum == 0xC6EF3720, "Encryption failed, sum inequal to 0xC6EF3720"); std::array encrypted = {u8(data_one & 0xFF), u8((data_one >> 8) & 0xFF), u8((data_one >> 16) & 0xFF), u8((data_one >> 24) & 0xFF), u8(data_two & 0xFF), u8((data_two >> 8) & 0xFF), u8((data_two >> 16) & 0xFF), u8((data_two >> 24) & 0xFF)}; return encrypted; } std::array dimensions_toypad::generate_figure_key(const std::array& buf) { std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; std::array figure_key = {}; write_to_ptr>(figure_key, scramble(uid, 3)); write_to_ptr>(figure_key, 4, scramble(uid, 4)); write_to_ptr>(figure_key, 8, scramble(uid, 5)); write_to_ptr>(figure_key, 12, scramble(uid, 6)); return figure_key; } u32 dimensions_toypad::scramble(const std::array& uid, u8 count) { std::vector to_scramble; to_scramble.reserve(uid.size() + CHAR_CONSTANT.size()); for (u8 x : uid) { to_scramble.push_back(x); } for (u8 c : CHAR_CONSTANT) { to_scramble.push_back(c); } ::at32(to_scramble, count * 4 - 1) = 0xaa; return read_from_ptr>(dimensions_randomize(to_scramble, count).data()); } std::array dimensions_toypad::dimensions_randomize(const std::vector& key, u8 count) { u32 scrambled = 0; for (u8 i = 0; i < count; i++) { const u32 v4 = std::rotr(scrambled, 25); const u32 v5 = std::rotr(scrambled, 10); const u32 b = read_from_ptr>(key, i * 4); scrambled = b + v4 + v5 - scrambled; } return {u8(scrambled & 0xFF), u8(scrambled >> 8 & 0xFF), u8(scrambled >> 16 & 0xFF), u8(scrambled >> 24 & 0xFF)}; } u32 dimensions_toypad::get_figure_id(const std::array& buf) { const std::array figure_key = generate_figure_key(buf); const std::array decrypted = decrypt(&buf[36 * 4], figure_key); const u32 fig_num = read_from_ptr>(decrypted); // Characters have their model number encrypted in page 36 if (fig_num < 1000) { return fig_num; } // Vehicles/Gadgets have their model number written as little endian in page 36 return read_from_ptr>(buf, 36 * 4); } dimensions_figure& dimensions_toypad::get_figure_by_index(u8 index) { return ::at32(m_figures, index); } void dimensions_toypad::random_uid(u8* uid_buffer) { uid_buffer[0] = 0x04; uid_buffer[7] = 0x80; for (u8 i = 1; i < 7; i++) { u8 random = rand() % 255; uid_buffer[i] = random; } } void dimensions_toypad::get_challenge_response(const u8* buf, u8 sequence, std::array& reply_buf) { // Decrypt payload into an 8 byte array const std::array value = decrypt(buf, std::nullopt); // Confirmation is the first 4 bytes of the decrypted payload const u32 conf = read_from_ptr>(value); // Generate next random number based on RNG const u32 next_random = get_next(); std::array value_to_encrypt = {}; // Encrypt an 8 byte array, first 4 bytes are the next random number (little endian) // followed by the confirmation from the decrypted payload write_to_ptr>(value_to_encrypt, next_random); write_to_ptr>(value_to_encrypt, 4, conf); const std::array encrypted = encrypt(value_to_encrypt.data(), std::nullopt); reply_buf[0] = 0x55; reply_buf[1] = 0x09; reply_buf[2] = sequence; // Copy encrypted value to response data std::memcpy(&reply_buf[3], encrypted.data(), encrypted.size()); reply_buf[11] = generate_checksum(reply_buf, 11); } void dimensions_toypad::query_block(u8 index, u8 page, std::array& reply_buf, u8 sequence) { std::lock_guard lock(m_dimensions_mutex); reply_buf[0] = 0x55; reply_buf[1] = 0x12; reply_buf[2] = sequence; reply_buf[3] = 0x00; // Index from game begins at 1 rather than 0, so minus 1 here if (const u8 figure_index = index - 1; figure_index < dimensions_figure_count) { const dimensions_figure& figure = get_figure_by_index(figure_index); // Query 4 pages of 4 bytes from the figure, copy this to the response if (figure.index != 255 && (4 * page) < ((0x2D * 4) - 16)) { std::memcpy(&reply_buf[4], figure.data.data() + (4 * page), 16); } } reply_buf[20] = generate_checksum(reply_buf, 20); } void dimensions_toypad::write_block(u8 index, u8 page, const u8* to_write_buf, std::array& reply_buf, u8 sequence) { std::lock_guard lock(m_dimensions_mutex); reply_buf[0] = 0x55; reply_buf[1] = 0x02; reply_buf[2] = sequence; reply_buf[3] = 0x00; // Index from game begins at 1 rather than 0, so minus 1 here if (const u8 figure_index = index - 1; figure_index < dimensions_figure_count) { dimensions_figure& figure = get_figure_by_index(figure_index); // Copy 4 bytes to the page on the figure requested by the game if (figure.index != 255 && page < 0x2D) { // Id is written to page 36 if (page == 36) { figure.id = read_from_ptr>(to_write_buf); } std::memcpy(figure.data.data() + (page * 4), to_write_buf, 4); figure.save(); } } reply_buf[4] = generate_checksum(reply_buf, 4); } void dimensions_toypad::get_model(const u8* buf, u8 sequence, std::array& reply_buf) { // Decrypt payload to 8 byte array, byte 1 is the index, 4-7 are the confirmation const std::array value = decrypt(buf, std::nullopt); const u8 index = value[0]; const u32 conf = read_from_ptr>(value, 4); std::array value_to_encrypt = {}; // Response is the figure's id (little endian) followed by the confirmation from payload // Index from game begins at 1 rather than 0, so minus 1 here if (const u8 figure_index = index - 1; figure_index < dimensions_figure_count) { const dimensions_figure& figure = get_figure_by_index(figure_index); write_to_ptr>(value_to_encrypt, figure.id); } write_to_ptr>(value_to_encrypt, 4, conf); const std::array encrypted = encrypt(value_to_encrypt.data(), std::nullopt); reply_buf[0] = 0x55; reply_buf[1] = 0x0a; reply_buf[2] = sequence; reply_buf[3] = 0x00; // Copy encrypted message to response std::memcpy(&reply_buf[4], encrypted.data(), encrypted.size()); reply_buf[12] = generate_checksum(reply_buf, 12); } u32 dimensions_toypad::load_figure(const std::array& buf, fs::file in_file, u8 pad, u8 index, bool lock) { if (lock) { m_dimensions_mutex.lock(); } const u32 id = get_figure_id(buf); dimensions_figure& figure = get_figure_by_index(index); figure.dim_file = std::move(in_file); figure.id = id; figure.pad = pad; figure.index = index + 1; std::memcpy(figure.data.data(), buf.data(), buf.size()); // When a figure is added to the toypad, respond to the game with the pad they were added to, their index, // the direction (0x00 in byte 6 for added) and their UID std::array figure_change_response = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x00, buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; figure_change_response[13] = generate_checksum(figure_change_response, 13); m_figure_added_removed_responses.push(std::move(figure_change_response)); if (lock) { m_dimensions_mutex.unlock(); } return id; } bool dimensions_toypad::remove_figure(u8 pad, u8 index, bool full_remove, bool lock) { dimensions_figure& figure = get_figure_by_index(index); if (figure.index == 255) { return false; } if (lock) { m_dimensions_mutex.lock(); } // When a figure is removed from the toypad, respond to the game with the pad they were removed from, their index, // the direction (0x01 in byte 6 for removed) and their UID if (full_remove) { std::array figure_change_response = {0x56, 0x0b, pad, 0x00, figure.index, 0x01, figure.data[0], figure.data[1], figure.data[2], figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; figure_change_response[13] = generate_checksum(figure_change_response, 13); m_figure_added_removed_responses.push(std::move(figure_change_response)); figure.save(); figure.dim_file.close(); } figure.index = 255; figure.pad = 255; figure.id = 0; if (lock) { m_dimensions_mutex.unlock(); } return true; } bool dimensions_toypad::temp_remove(u8 index) { std::lock_guard lock(m_dimensions_mutex); const dimensions_figure& figure = get_figure_by_index(index); if (figure.index == 255) return false; // Send a response to the game that the figure has been "Picked up" from existing slot, // until either the movement is cancelled, or user chooses a space to move to std::array figure_change_response = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x01, figure.data[0], figure.data[1], figure.data[2], figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; figure_change_response[13] = generate_checksum(figure_change_response, 13); m_figure_added_removed_responses.push(std::move(figure_change_response)); return true; } bool dimensions_toypad::cancel_remove(u8 index) { std::lock_guard lock(m_dimensions_mutex); dimensions_figure& figure = get_figure_by_index(index); if (figure.index == 255) return false; // Cancel the previous movement of the figure std::array figure_change_response = {0x56, 0x0b, figure.pad, 0x00, figure.index, 0x00, figure.data[0], figure.data[1], figure.data[2], figure.data[4], figure.data[5], figure.data[6], figure.data[7]}; figure_change_response[13] = generate_checksum(figure_change_response, 13); m_figure_added_removed_responses.push(std::move(figure_change_response)); return true; } bool dimensions_toypad::move_figure(u8 pad, u8 index, u8 old_pad, u8 old_index) { if (old_index == index) { // Don't bother removing and loading again, just send response to the game cancel_remove(index); return true; } std::lock_guard lock(m_dimensions_mutex); // When moving figures between spaces on the toypad, remove any figure from the space they are moving to, // then remove them from their current space, then load them to the space they are moving to remove_figure(pad, index, true, false); dimensions_figure& figure = get_figure_by_index(old_index); const std::array data = figure.data; fs::file in_file = std::move(figure.dim_file); remove_figure(old_pad, old_index, false, false); load_figure(data, std::move(in_file), pad, index, false); return true; } bool dimensions_toypad::create_blank_character(std::array& buf, u16 id) { random_uid(buf.data()); buf[3] = id & 0xFF; // Only characters are created with their ID encrypted and stored in pages 36 and 37, // as well as a password stored in page 43. Blank tags have their information populated // by the game when it calls the write_block command. if (id != 0) { // LEGO Dimensions figures use NTAG213 tag types, and the UID for these is stored in // bytes 0, 1, 2, 4, 5, 6 and 7 (out of 180 bytes) std::array uid = {buf[0], buf[1], buf[2], buf[4], buf[5], buf[6], buf[7]}; const std::array figure_key = generate_figure_key(buf); std::array value_to_encrypt = {}; write_to_ptr>(value_to_encrypt, id); write_to_ptr>(value_to_encrypt, 4, id); std::array encrypted = encrypt(value_to_encrypt.data(), figure_key); std::memcpy(&buf[36 * 4], &encrypted[0], 4); std::memcpy(&buf[37 * 4], &encrypted[4], 4); std::memcpy(&buf[43 * 4], pwd_generate(uid).data(), 4); } else { // Page 38 is used as verification for blank tags write_to_ptr>(buf.data(), 38 * 4, 1); } return true; } std::array dimensions_toypad::pwd_generate(const std::array& uid) { std::vector pwd_calc = {PWD_CONSTANT.begin(), PWD_CONSTANT.end() - 1}; for (u8 i = 0; i < uid.size(); i++) { pwd_calc.insert(pwd_calc.begin() + i, uid[i]); } return dimensions_randomize(pwd_calc, 8); } std::optional> dimensions_toypad::pop_added_removed_response() { std::lock_guard lock(m_dimensions_mutex); if (m_figure_added_removed_responses.empty()) { return std::nullopt; } std::array response = m_figure_added_removed_responses.front(); m_figure_added_removed_responses.pop(); return response; } usb_device_dimensions::usb_device_dimensions(const std::array& location) : usb_device_emulated(location) { device = UsbDescriptorNode(USB_DESCRIPTOR_DEVICE, UsbDeviceDescriptor{0x200, 0x0, 0x0, 0x0, 0x20, 0x0E6F, 0x0241, 0x200, 0x1, 0x2, 0x3, 0x1}); auto& config0 = device.add_node(UsbDescriptorNode(USB_DESCRIPTOR_CONFIG, UsbDeviceConfiguration{0x29, 0x1, 0x1, 0x0, 0x80, 0xFA})); config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_INTERFACE, UsbDeviceInterface{0x0, 0x0, 0x2, 0x3, 0x0, 0x0, 0x0})); config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_HID, UsbDeviceHID{0x0111, 0x00, 0x01, 0x22, 0x001d})); config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_ENDPOINT, UsbDeviceEndpoint{0x81, 0x03, 0x20, 0x1})); config0.add_node(UsbDescriptorNode(USB_DESCRIPTOR_ENDPOINT, UsbDeviceEndpoint{0x01, 0x03, 0x20, 0x1})); } usb_device_dimensions::~usb_device_dimensions() { } std::shared_ptr usb_device_dimensions::make_instance(u32, const std::array& location) { return std::make_shared(location); } u16 usb_device_dimensions::get_num_emu_devices() { return 1; } void usb_device_dimensions::control_transfer(u8 bmRequestType, u8 bRequest, u16 wValue, u16 wIndex, u16 wLength, u32 buf_size, u8* buf, UsbTransfer* transfer) { usb_device_emulated::control_transfer(bmRequestType, bRequest, wValue, wIndex, wLength, buf_size, buf, transfer); } void usb_device_dimensions::interrupt_transfer(u32 buf_size, u8* buf, u32 endpoint, UsbTransfer* transfer) { ensure(buf_size == 0x20); transfer->fake = true; transfer->expected_count = buf_size; transfer->expected_result = HC_CC_NOERR; switch (endpoint) { case 0x81: { // Read Endpoint, if a request has not been sent via the write endpoint, set expected result as // EHCI_CC_HALTED so the game doesn't report the Toypad as being disconnected. std::lock_guard lock(m_query_mutex); std::optional> response = g_dimensionstoypad.pop_added_removed_response(); if (response) { std::memcpy(buf, response.value().data(), 0x20); } else if (!m_queries.empty()) { std::memcpy(buf, m_queries.front().data(), 0x20); m_queries.pop(); } else { transfer->expected_count = 0; transfer->expected_result = EHCI_CC_HALTED; } break; } case 0x01: { // Write endpoint, similar structure of request to the Infinity Base with a command for byte 3, // sequence for byte 4, the payload after that, then a checksum for the final byte. const u8 command = buf[2]; const u8 sequence = buf[3]; transfer->expected_time = get_timestamp() + 100; std::array q_result{}; switch (command) { case 0xB0: // Wake { // Consistent device response to the wake command q_result = {0x55, 0x0e, 0x01, 0x28, 0x63, 0x29, 0x20, 0x4c, 0x45, 0x47, 0x4f, 0x20, 0x32, 0x30, 0x31, 0x34, 0x46}; break; } case 0xB1: // Seed { // Initialise a random number generator using the seed provided g_dimensionstoypad.generate_random_number(&buf[4], sequence, q_result); break; } case 0xB3: // Challenge { // Get the next number in the sequence based on the RNG from 0xB1 command g_dimensionstoypad.get_challenge_response(&buf[4], sequence, q_result); break; } case 0xC0: // Color case 0xC1: // Get Pad Color case 0xC2: // Fade case 0xC3: // Flash case 0xC4: // Fade Random case 0xC6: // Fade All case 0xC7: // Flash All case 0xC8: // Color All { // Send a blank response to acknowledge color has been sent to toypad g_dimensionstoypad.get_blank_response(0x01, sequence, q_result); break; } case 0xD2: // Read { // Read 4 pages from the figure at index (buf[4]), starting with page buf[5] g_dimensionstoypad.query_block(buf[4], buf[5], q_result, sequence); break; } case 0xD3: // Write { // Write 4 bytes to page buf[5] to the figure at index buf[4] g_dimensionstoypad.write_block(buf[4], buf[5], &buf[6], q_result, sequence); break; } case 0xD4: // Model { // Get the model id of the figure at index buf[4] g_dimensionstoypad.get_model(&buf[4], sequence, q_result); break; } case 0xD0: // Tag List case 0xE1: // PWD case 0xE5: // Active case 0xFF: // LEDS Query { // Further investigation required dimensions_log.error("Unimplemented LD Function: 0x%x", command); dimensions_log.error("Request: %s", fmt::buf_to_hexstring(buf, buf_size)); break; } default: { dimensions_log.error("Unknown LD Function: 0x%x", command); dimensions_log.error("Request: %s", fmt::buf_to_hexstring(buf, buf_size)); break; } } std::lock_guard lock(m_query_mutex); m_queries.push(q_result); break; } default: break; } } void usb_device_dimensions::isochronous_transfer(UsbTransfer* transfer) { usb_device_emulated::isochronous_transfer(transfer); }