Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions libs/book/ContentProtection/include/ByteSource.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

// FreeInk — random-access byte source.
//
// Mirrors FreeInkBook's BookSource shape so adapting an SD file (CrossPoint
// HalFile, FreeInkBook BookSource, host stdio) is a few lines. The content
// module never touches a filesystem directly.
//
// Freestanding C++17.

#include <stdint.h>

namespace freeink {
namespace content {

class ByteSource {
public:
virtual ~ByteSource() = default;
// Returns bytes actually read (may be short at EOF), or negative on error.
virtual int32_t readAt(uint64_t offset, void* dst, uint32_t len) = 0;
virtual uint64_t size() const = 0;
};

} // namespace content
} // namespace freeink
64 changes: 64 additions & 0 deletions libs/book/ContentProtection/include/ContentMinizConfig.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#pragma once

/* This module needs miniz's streaming inflate for on-read content access. Include this
* header instead of <miniz.h> so every translation unit sees the same config.
*
* Two modes, selected by the embedding project:
* - CONTENT_EXTERNAL_MINIZ (device build): bind to the host application's
* already-linked miniz instead of compiling a second copy (two copies in one
* image corrupt inflate). CrossPoint's lib/miniz/src/MinizConfig.h prefixes
* only tinfl_decompress* and mz_crc32/adler32/free with crosspoint_ and
* exports the mz_inflate* zlib API under plain names; we mirror that rename
* set EXACTLY so the linker binds to its definitions.
* - standalone (host tests): compile our own vendored copy under a content_
* prefix so the tinfl symbols never bind to a ROM/host miniz copy.
*/

#define MINIZ_NO_STDIO
#define MINIZ_NO_TIME
#define MINIZ_NO_ARCHIVE_APIS
#define MINIZ_NO_ARCHIVE_WRITING_APIS
#define MINIZ_NO_DEFLATE_APIS
#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES

#if defined(CONTENT_EXTERNAL_MINIZ)

// Match the host application's MinizConfig.h rename set exactly.
#define tinfl_decompress crosspoint_tinfl_decompress
#define tinfl_decompress_mem_to_heap crosspoint_tinfl_decompress_mem_to_heap
#define tinfl_decompress_mem_to_mem crosspoint_tinfl_decompress_mem_to_mem
#define tinfl_decompress_mem_to_callback crosspoint_tinfl_decompress_mem_to_callback
#define mz_crc32 crosspoint_mz_crc32
#define mz_adler32 crosspoint_mz_adler32
#define mz_free crosspoint_mz_free
// mz_inflate*, tinfl_decompressor_alloc/free, mz_uncompress*, mz_error,
// mz_version, miniz_def_*: the host exports these under plain names — leave
// them unprefixed so we bind to its definitions.

#else // standalone: our own copy, content_-prefixed.

#define tinfl_decompress content_tinfl_decompress
#define tinfl_decompress_mem_to_heap content_tinfl_decompress_mem_to_heap
#define tinfl_decompress_mem_to_mem content_tinfl_decompress_mem_to_mem
#define tinfl_decompress_mem_to_callback content_tinfl_decompress_mem_to_callback
#define tinfl_decompressor_alloc content_tinfl_decompressor_alloc
#define tinfl_decompressor_free content_tinfl_decompressor_free
#define mz_crc32 content_mz_crc32
#define mz_adler32 content_mz_adler32
#define mz_free content_mz_free
#define mz_error content_mz_error
#define mz_version content_mz_version
#define mz_inflate content_mz_inflate
#define mz_inflateInit content_mz_inflateInit
#define mz_inflateInit2 content_mz_inflateInit2
#define mz_inflateReset content_mz_inflateReset
#define mz_inflateEnd content_mz_inflateEnd
#define mz_uncompress content_mz_uncompress
#define mz_uncompress2 content_mz_uncompress2
#define miniz_def_alloc_func content_miniz_def_alloc_func
#define miniz_def_realloc_func content_miniz_def_realloc_func
#define miniz_def_free_func content_miniz_def_free_func

#endif

#include "../third_party/miniz/miniz.h"
44 changes: 44 additions & 0 deletions libs/book/ContentProtection/include/ContentProtection.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

// Reader-facing interface for encrypted entries.
//
// Freestanding C++17 core; storage/crypto injected. Nothing here can write the
// decoded bytes to storage; access is on-read, into memory only.

#include <cstdint>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>

namespace freeink {
namespace content {

using ContentChunkSink = bool (*)(void* context, const uint8_t* data, size_t size);

// Provides streamed access to protected items of an opened container.
class ContentDecryptor {
public:
virtual ~ContentDecryptor() = default;
// Is this container-relative item stored encrypted?
virtual bool isEncrypted(const std::string& itemPath) const = 0;
virtual size_t decryptedSize(const std::string& itemPath) const = 0;
// Read one protected item as chunks through the sink; the caller owns all
// buffering (no whole-entry allocation happens on this side).
virtual bool decryptToSink(const std::string& itemPath, ContentChunkSink sink,
void* context) = 0;
};

// Opens the protected read path for `epubPath` when the content is protected
// and this device holds the credential to read it. Returns:
// non-null -> route protected item reads through it
// null, err empty -> plain content (not protected) — read normally
// null, err non-empty -> protected but unreadable (no credential / expired)
//
// This is the SD-backed convenience entry; the integrating firmware provides
// the implementation (it binds the ByteSource to device storage). The portable
// lib itself stays storage-agnostic — host code opens ProtectedBook directly.
std::unique_ptr<ContentDecryptor> openProtectedBook(const std::string& epubPath, std::string& err);

} // namespace content
} // namespace freeink
44 changes: 44 additions & 0 deletions libs/book/ContentProtection/include/Credential.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

// FreeInk — access credential.
//
// The credential bundle is produced off-device and stored on the SD card so
// it is portable across firmware installs —
// CrossPoint users switch firmware builds and the credential must survive.
// FAT32 has no permissions; the private key inside is sensitive — treat the
// file accordingly.
//
// Line-oriented `key: value` format (FREEINK-CONTENT-KEY 1) so parsing
// needs no JSON library. Unknown lines are ignored (forward-compatible).
//
// Freestanding C++17.

#include <stdint.h>

#include <string>

#include "ByteSource.h"

namespace freeink {
namespace content {

struct Credential {
std::string username;
std::string userUuid;
std::string deviceUuid;
std::string serial;
std::string fingerprint;
std::string privateLicenseKey; // base64 PKCS#8 DER (clear; sensitive)
std::string deviceSalt; // base64 (optional for read-only use)
bool complete() const {
return !userUuid.empty() && !deviceUuid.empty() && !privateLicenseKey.empty();
}
};

// Parses an identity bundle. Returns false when the file can't be read or
// the mandatory fields are missing.
bool parseCredential(ByteSource& source, Credential* out);
bool parseCredential(const std::string& text, Credential* out);

} // namespace content
} // namespace freeink
73 changes: 73 additions & 0 deletions libs/book/ContentProtection/include/Crypto.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#pragma once

// FreeInk — crypto backend interface.
//
// On device: wolfSSL. Host tests: OpenSSL. Kept behind an interface so the
// freestanding core carries no crypto dependency and stays unit-testable.
//
// Freestanding C++17.

#include <stddef.h>
#include <stdint.h>

#include <string>
#include <vector>

namespace freeink {
namespace content {

struct RsaKeyPairDer {
std::vector<uint8_t> spki; // SubjectPublicKeyInfo DER (wire format)
std::vector<uint8_t> pkcs8; // PKCS#8 DER (private key storage format)
};

class Crypto {
public:
virtual ~Crypto() = default;

// --- read path ------------------------------------------------------------

// Raw RSA private-key operation (no padding) with a PKCS#8 DER key.
// `in` is one modulus-sized block; `out` must have room for the same.
// Returns output length (== modulus size) or a negative value on error.
virtual int32_t rsaPrivateRaw(const uint8_t* pkcs8Der, size_t pkcs8Len, const uint8_t* in,
size_t inLen, uint8_t* out, size_t outCap) = 0;

// AES-128-CBC decrypt, no padding handling (caller unpads). `len` must be
// a multiple of 16. In-place operation is allowed (out == in).
virtual bool aes128CbcDecrypt(const uint8_t key[16], const uint8_t iv[16], const uint8_t* in,
size_t len, uint8_t* out) = 0;

virtual void sha1(const uint8_t* data, size_t len, uint8_t out[20]) = 0;
virtual void sha256(const uint8_t* data, size_t len, uint8_t out[32]) = 0;

// --- client (off-device credential setup) -----------------------------------

// RSA-1024 keypair (e=65537), exported as SPKI + PKCS#8 DER.
virtual bool rsaGenerate(RsaKeyPairDer* out) = 0;

// RSAES-PKCS1-v1_5 with an X.509 certificate's public key (DER).
virtual bool rsaPublicEncrypt(const uint8_t* certDer, size_t certLen, const uint8_t* in,
size_t inLen, uint8_t* out, size_t outCap, size_t* outLen) = 0;

// request signature: PKCS#1 v1.5 type-1 padding + raw RSA private op
// over the 20-byte canonicalization hash. `out` is one modulus (128 bytes).
virtual bool rsaPrivateSignRaw(const uint8_t* pkcs8Der, size_t pkcs8Len, const uint8_t hash[20],
uint8_t out[128]) = 0;

// AES-128-CBC encrypt WITH PKCS#7 padding (used for device-key encryption).
// `out` must hold len rounded up to 16 + 16.
virtual bool aes128CbcEncrypt(const uint8_t key[16], const uint8_t iv[16], const uint8_t* in,
size_t len, uint8_t* out) = 0;

// Extracts the private key (as PKCS#8 DER) and certificate (DER) from a
// PKCS#12 bundle. note: the signing bundle's passphrase is
// base64(device key).
virtual bool pkcs12Extract(const uint8_t* p12, size_t len, const std::string& password,
std::vector<uint8_t>* keyPkcs8, std::vector<uint8_t>* certDer) = 0;

virtual void randomBytes(uint8_t* out, size_t len) = 0;
};

} // namespace content
} // namespace freeink
94 changes: 94 additions & 0 deletions libs/book/ContentProtection/include/ProtectedBook.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#pragma once

// FreeInk — encrypted-entry read path.
//
// Access-only, by design:
// - content stays encrypted at rest (items are accessed on read, into memory)
// - the content key is unwrapped with this device's access credential
// - access expiry from rights.xml is enforced by the caller (isExpired)
// There is deliberately no API that writes the content out in the clear.
//
// Freestanding C++17. Crypto and storage are injected.

#include <stdint.h>

#include <string>
#include <vector>

#include "ByteSource.h"
#include "ContentProtection.h"
#include "Crypto.h"
#include "Credential.h"
#include "Rights.h"
#include "Zip.h"

namespace freeink {
namespace content {

class ProtectedBook {
public:
// Opens a (possibly protected) EPUB: scans the container, and when
// META-INF/encryption.xml is present, parses rights/encryption metadata and
// unwraps the content key with the credential's private key.
//
// rightsXmlOverride: the access-grant rights document (wrapped content key),
// supplied out-of-band. the source delivers rights.xml separately from the EPUB, so
// the preferred flow keeps it in a sidecar and passes it here — the EPUB on
// disk stays byte-identical to what was delivered. When empty, falls back to
// reading META-INF/rights.xml from inside the zip (legacy in-container form).
bool open(ByteSource& source, Crypto& crypto, const Credential& identity,
const std::string& rightsXmlOverride = "");

// Completes open using an already-scanned ZIP index. This lets an embedding
// application classify a plain EPUB before initializing crypto or loading
// credentials, then transfer ownership of that same index instead of
// scanning the central directory a second time.
bool openFromScan(ByteSource& source, Crypto& crypto, const Credential& identity,
ZipScan&& scan, const std::string& rightsXmlOverride = "");

bool isProtected() const { return protected_; }
const Rights& rights() const { return rights_; }
const std::string& lastError() const { return lastError_; }

// Loan expiry (0 = none found). Caller enforces: refuse to open past due.
int64_t expiresAt() const { return rights_.expiresAt; }
bool isExpired(int64_t nowEpoch) const { return rights_.expiresAt != 0 && nowEpoch > rights_.expiresAt; }

bool isEncrypted(const std::string& name) const;
size_t decryptedSize(const std::string& name) const;

// Access + inflate one protected entry, streamed through the sink. The only
// read path: callers own their buffering, so no whole-entry allocation can
// hide in here.
bool decryptEntryToSink(ByteSource& source, Crypto& crypto, const std::string& name,
ContentChunkSink sink, void* context);

// Read a non-encrypted entry fully, inflating when deflated. Capped and
// OOM-safe: fails (rather than aborting) when the entry is oversized or
// memory is unavailable.
bool readEntryInflated(ByteSource& source, const std::string& name, std::string* out);

// Inflates raw-deflate data (windowBits -15) into a caller-owned buffer of
// exactly the expected size.
bool inflateTo(const uint8_t* in, size_t inLen, uint8_t* out, size_t outLen);

private:
bool finishOpen(ByteSource& source, Crypto& crypto, const Credential& identity,
const std::string& rightsXmlOverride);
bool unwrapBookKey(Crypto& crypto, const Credential& identity, uint8_t out[16]);
// Stream-parses encryption.xml out of the zip in chunks, keeping only path
// hashes. The manifest scales with the container's file count, so it is
// never materialized whole.
bool scanEncryptionXml(ByteSource& source, const ZipEntryInfo& entry);

ZipScan zip_;
Rights rights_;
// Sorted FNV-1a hashes of the aes128-cbc encrypted entry paths.
std::vector<uint64_t> encryptedUriHashes_;
uint8_t bookKey_[16] = {0};
bool protected_ = false;
std::string lastError_;
};

} // namespace content
} // namespace freeink
28 changes: 28 additions & 0 deletions libs/book/ContentProtection/include/Rights.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

// FreeInk — rights.xml and encryption.xml parsing.

#include <stdint.h>

#include <string>
#include <vector>

namespace freeink {
namespace content {

struct Rights {
std::string encryptedKey; // base64, RSA-wrapped content key
std::string keyType; // optional: first-pass obfuscation variant
std::string user; // account UUID this grant belongs to
std::string device; // device UUID
std::string fulfillment; // access-grant UUID
std::string voucher; // voucher id
int64_t expiresAt = 0; // epoch seconds, 0 = no expiry found
};

// Parses META-INF/rights.xml. Returns false on malformed input or a missing
// encryptedKey. Expiry comes from permissions/display (until or duration).
bool parseRightsXml(const std::string& xml, Rights* out);

} // namespace content
} // namespace freeink
Loading