Files
2026-10-29 03:55:40 +00:00

163 lines
6.8 KiB
C++

#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
namespace sppm {
constexpr std::uint16_t kWidth = 640;
constexpr std::uint16_t kHeight = 360;
constexpr std::size_t kPixelsPerLayer = static_cast<std::size_t>(kWidth) * kHeight;
constexpr std::size_t kLayerCount = 4;
constexpr std::size_t kPaletteSize = 256;
constexpr std::size_t kMaxFrames = 9999;
constexpr std::size_t kMaxAudioTracks = 4;
struct Color {
std::uint8_t r = 0;
std::uint8_t g = 0;
std::uint8_t b = 0;
std::uint8_t a = 255;
};
using Palette = std::array<Color, kPaletteSize>;
// Palette index 0 is transparent. Other entries can be changed by the editor.
Palette defaultPalette();
struct Frame {
// A, B, C, D from front to back; one palette index per 640x360 pixel.
std::array<std::vector<std::uint8_t>, kLayerCount> layers;
Frame();
};
struct AudioTrack {
std::string name; // e.g. BGM, SE1, SE2, SE3
std::uint32_t sampleRate = 0; // 8000..48000 Hz
std::uint8_t channels = 1; // 1 or 2
std::uint8_t referenceFps = 0; // BGM's native animation speed; 0 means use the note's speed
std::uint8_t referenceSpeed = 0; // SPPM v3 BGM speed index (1..8); 0 means legacy fps
std::vector<std::uint16_t> frameTriggers; // SE cues; BGM always starts at frame 0
std::vector<std::int16_t> pcm; // interleaved signed 16-bit samples
};
// A public identity is safe to put in an .sppm file. It contains an Ed25519
// verification key and a one-way, app-specific device tag; neither field
// exposes a raw console identifier.
struct PublicIdentity {
std::array<std::uint8_t, 32> key{};
std::array<std::uint8_t, 16> deviceTag{};
};
struct SigningIdentity {
PublicIdentity publicIdentity;
std::array<std::uint8_t, 64> secretKey{};
// Present only to keep validating pre-v3 console-HMAC files after migration.
std::array<std::uint8_t, 32> legacyHmacKey{};
};
struct Metadata {
std::string title;
std::string description;
std::string authorName;
std::string originalAuthorName;
std::string lastEditorName;
// Creator identities, not file IDs. Exactly 16 uppercase hexadecimal chars.
std::string authorFsid;
std::string originalAuthorFsid;
std::string lastEditorFsid;
// Present for v3 public-key-signed files. Zero values indicate a legacy
// author record which cannot be retrospectively cryptographically bound.
PublicIdentity authorIdentity;
PublicIdentity originalAuthorIdentity;
PublicIdentity lastEditorIdentity;
// File lineage IDs. parentId is empty only for an original Switchnote.
std::string currentId;
std::string rootId;
std::string parentId;
std::uint64_t createdUnixSeconds = 0;
std::uint64_t modifiedUnixSeconds = 0;
std::uint32_t coverFrame = 0;
std::uint8_t fps = 12;
std::uint8_t speed = 0; // SPPM v3 DSi speed index (1..8); 0 means legacy fps
bool locked = false;
bool allowSpinoffs = true;
bool bgmFollowsSpeed = true;
bool loopPlayback = true;
};
struct Document {
Metadata metadata;
Palette palette = defaultPalette();
std::vector<Frame> frames;
std::vector<AudioTrack> audioTracks;
};
bool isValidFsid(std::string_view fsid);
bool hasPublicIdentity(const PublicIdentity& identity);
std::string deriveFsid(const PublicIdentity& identity);
std::string identityFingerprint(const PublicIdentity& identity);
std::array<std::uint8_t, 16> deriveDeviceTag(const std::vector<std::uint8_t>& deviceMaterial);
std::array<std::uint8_t, 32> deriveDeviceWrapKey(const std::vector<std::uint8_t>& deviceMaterial,
const std::array<std::uint8_t, 16>& salt);
bool createSigningIdentity(const std::array<std::uint8_t, 16>& deviceTag,
SigningIdentity& identity, std::string& error);
bool sealSigningSecret(const SigningIdentity& identity,
const std::array<std::uint8_t, 32>& wrapKey,
const std::array<std::uint8_t, 24>& nonce,
std::array<std::uint8_t, 128>& sealed, std::string& error);
bool unsealSigningSecret(const std::array<std::uint8_t, 128>& sealed,
const std::array<std::uint8_t, 32>& wrapKey,
const std::array<std::uint8_t, 24>& nonce,
SigningIdentity& identity, std::string& error);
// Domain-separated 64-bit ID helper used for non-identity note and lineage IDs.
// Creator FSIDs are instead derived from PublicIdentity.
std::string deriveFsid(const std::array<std::uint8_t, 32>& secret,
const std::array<std::uint8_t, 16>& nonce,
std::string_view domain);
bool validate(const Document& document, std::string& error);
double frameRate(const Metadata& metadata);
bool serialize(const Document& document, std::vector<std::uint8_t>& bytes,
std::string& error);
bool deserialize(const std::vector<std::uint8_t>& bytes, Document& document,
std::string& error);
bool loadFile(const std::string& path, Document& document, std::string& error);
bool saveFile(const std::string& path, const Document& document, std::string& error);
// Console-local authenticated .sppm envelopes. Legacy v1 notes remain readable;
// new saves include a 256-bit HMAC-SHA-256 footer verified with the local key.
bool deserializeSigned(const std::vector<std::uint8_t>& bytes, Document& document,
const std::array<std::uint8_t, 32>& signingKey,
std::string& error);
bool loadSignedFile(const std::string& path, Document& document,
const std::array<std::uint8_t, 32>& signingKey,
std::string& error);
bool saveSignedFile(const std::string& path, const Document& document,
const std::array<std::uint8_t, 32>& signingKey,
std::string& error);
// v3 is publicly verifiable: no console-local secret is needed to validate a
// received file. It signs a domain-separated SHA-256 digest with Ed25519.
bool savePublicSignedFile(const std::string& path, const Document& document,
const SigningIdentity& identity, std::string& error);
bool signMessage(const SigningIdentity& identity, const std::uint8_t* message,
std::size_t messageSize, std::array<std::uint8_t, 64>& signature,
std::string& error);
// Preserves the original author and root ID, while giving the fork a new
// current ID, parent ID, author, creation time and editable state.
bool createSpinoff(const Document& parent, std::string_view newAuthorName,
std::string_view newAuthorFsid, std::string_view newCurrentId,
std::uint64_t nowUnixSeconds, Document& child,
std::string& error);
} // namespace sppm