/** * EncryptedHttpSource — ECDH + AES-GCM + HMAC protected playback * * Wire protocol: * 1. Client generates an ephemeral P-256 ECDH keypair. The private key is * re-imported as non-extractable so `crypto.subtle.exportKey()` and * `wrapKey()` both refuse — a DevTools-only attacker can't read it. * 2. On /api/token the client sends its public key. The server generates * its own ephemeral keypair, wraps its private key with the server * secret, embeds that + the client's pubkey inside a signed token, * and returns its ephemeral public key. No raw session key bytes * transit the wire in either direction. * 3. Both peers independently ECDH-derive the shared secret and * HKDF-expand it into two sub-keys: * - masterKey: AES-GCM 256, decrypt-only, non-extractable * - hmacKey: HMAC-SHA256, sign-only, non-extractable * 4. Each read() issues a signed GET to /api/video (HMAC covers * token:nonce:timestamp:offset:length). The worker encrypts the * upstream byte range with masterKey and returns * [12-byte IV || ciphertext || 16-byte tag]. The client peels the * IV, decrypts with the same non-extractable masterKey, and hands * plaintext to the demuxer. * * Why inherit from HttpSource? Purely for a shared SourceAdapter shape * + the convenience of resolveSize() and getKey() defaults. The * streaming machinery HttpSource provides is NOT used here — encrypted * mode has to read one full range at a time (AES-GCM is authenticated * per message, so you can't decrypt arbitrary byte offsets inside a * ciphertext). */ import { HttpSource } from "./HttpSource"; export interface EncryptedSourceConfig { videoUrl: string; tokenUrl: string; /** The upstream source URL. MoviElement passes this through as `videoId`. */ videoId: string; fingerprint: string; sessionToken: string; headers?: Record; onAuthFailed?: (reason: string) => void; } export declare class EncryptedHttpSource extends HttpSource { private readonly _encConfig; private _cryptoReady; private _clientPrivKey; private _clientPubB64; private _masterKey; private _hmacKey; private _token; private _expiresAt; private _knownFileSize; private _encContentDispositionFilename; private _tokenRefresh; private readonly _usedNonces; private readonly _abortCtrl; private _authFailed; private static readonly BLOCK_SIZE; private static readonly MAX_CACHED_BLOCKS; private static readonly READAHEAD_BLOCKS; private static readonly PREFETCH_LOW_WATER; private static readonly PREFETCH_HIGH_WATER; private static readonly MAX_CONCURRENT_STREAMS; private _maxCachedBlocks; private _prefetchLowWater; private _prefetchHighWater; private _lastReadEnd; private readonly _blockCache; private readonly _blockInflight; private readonly _activeStreams; constructor(config: EncryptedSourceConfig); private initCrypto; protected resolveSize(): Promise; read(offset: number, length: number): Promise; /** * Keep a continuous window of cached/inflight blocks ahead of the * latest read. When the contiguous run drops below PREFETCH_LOW_WATER, * fetchBlock() at the gap kicks off a new stream (which itself grabs * READAHEAD_BLOCKS), refilling up to PREFETCH_HIGH_WATER. */ private maybePrefetch; /** * Report the furthest byte offset currently cached as a contiguous * run from the START of the file. HttpSource exposes the same method * so the player UI's buffer bar can reflect both modes uniformly. * * Only count blocks reachable linearly from block 0 without a gap. * Blocks cached by a tail-of-file probe (MKV Cues / MP4 moov at end) * are intentionally ignored here — if we counted them, a demuxer * metadata pass would show "100% buffered" while 95% of the file is * actually not cached and every seek would round-trip. The player * UI's seek-is-instant hint relies on this being honest. */ getBufferedEnd(): number; /** * Override prefetch depth and cache cap at runtime. `megabytes` is the * target "buffer ahead of playback" window the caller wants the * player to maintain — encrypted mode translates that to an * equivalent number of 2 MB blocks for HIGH_WATER, sets LOW_WATER to * roughly half (so refill kicks in before we burn through what's * buffered), and grows the cache cap to comfortably hold the target * plus a bit of eviction headroom. * * Ignored if the requested size is non-positive. Takes effect on the * NEXT maybePrefetch() call (typically the next read), no re-init * needed. MoviElement wires this up through its `buffersize` * attribute so a consumer can tune deployment-specific memory / * responsiveness trade-offs without forking. */ setMaxBufferSize(megabytes: number): void; /** * Fetch one BLOCK_SIZE-aligned plaintext window. If it isn't already * cached or inflight, open a *streaming* fetch that spans this block * plus several look-ahead blocks — the worker emits one AES-GCM frame * per block back-to-back, and the reader below decrypts each frame as * it arrives, filling the cache progressively. This mirrors the way * HttpSource holds one long range GET open instead of starting a new * request per read. */ private fetchBlock; /** * Open one token-signed /api/video GET spanning the full block range, * read the framed AES-GCM response body, decrypt each frame into the * cache, and resolve the per-block inflight promises as they land. */ private streamBlocks; getKey(): string; /** * Expose the upstream Content-Disposition filename the worker * captured during token issuance. HttpSource's version reads from its * own HEAD-populated field which we never fill in encrypted mode, so * we override to return the token-issued value. */ getContentDispositionFilename(): string | null; close(): void; private ensureValidToken; private refreshToken; private generateNonce; } //# sourceMappingURL=EncryptedHttpSource.d.ts.map