multistreaming: scenes/composition, installer build support, service updates
- multistreaming (new): RTMP ingest + multi-platform fan-out with pluggable providers (Twitch/YouTube/Kick/custom), zero-knowledge key vaults, Authelia OIDC auth, shared rooms with editor/streamer roles, single-use invites, per-account streaming grants, and scenes & composition (grid/PiP layouts, text/image overlays, per-output audio routing). - installer: support Dockerfile build in metadata (not just image) and RSA key generation for the Authelia OIDC JWKS. - authelia: add OIDC provider with portainer + multistreaming clients (public + PKCE). - services: remove allprox; add nginx-proxy-manager and portainer; update lldap; regenerate catalog.
This commit is contained in:
parent
bb754cdd8c
commit
187379de4e
106 changed files with 21391 additions and 286 deletions
81
services/multistreaming/test/vault.test.js
Normal file
81
services/multistreaming/test/vault.test.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const {
|
||||
enroll,
|
||||
silentUnlock,
|
||||
recover,
|
||||
encryptSecret,
|
||||
decryptSecret,
|
||||
randomBytes,
|
||||
} = require('../src/vault');
|
||||
|
||||
const PASSWORD = 'correct horse battery staple';
|
||||
const AAD = 'fp:abc123|sess:xyz789';
|
||||
|
||||
test('enroll → silent unlock works with correct AAD', async () => {
|
||||
const deviceKey = randomBytes(32);
|
||||
const { serverWrapped, deviceWrapped, salt } = await enroll({
|
||||
password: PASSWORD,
|
||||
deviceKey,
|
||||
deviceAad: AAD,
|
||||
});
|
||||
|
||||
const vk = await silentUnlock(deviceKey, deviceWrapped, AAD);
|
||||
assert.equal(vk.length, 32);
|
||||
assert.ok(serverWrapped.iv && serverWrapped.data);
|
||||
assert.ok(salt);
|
||||
});
|
||||
|
||||
test('silent unlock fails if session/fingerprint AAD changes', async () => {
|
||||
const deviceKey = randomBytes(32);
|
||||
const { deviceWrapped } = await enroll({ password: PASSWORD, deviceKey, deviceAad: AAD });
|
||||
|
||||
await assert.rejects(
|
||||
silentUnlock(deviceKey, deviceWrapped, 'fp:abc123|sess:DIFFERENT'),
|
||||
/decrypt|operation/i,
|
||||
);
|
||||
});
|
||||
|
||||
test('silent unlock fails with a different device key (copied blob)', async () => {
|
||||
const deviceKeyA = randomBytes(32);
|
||||
const deviceKeyB = randomBytes(32);
|
||||
const { deviceWrapped } = await enroll({ password: PASSWORD, deviceKey: deviceKeyA, deviceAad: AAD });
|
||||
|
||||
await assert.rejects(silentUnlock(deviceKeyB, deviceWrapped, AAD), /decrypt|operation/i);
|
||||
});
|
||||
|
||||
test('recover with correct password works (new device)', async () => {
|
||||
const { serverWrapped, salt } = await enroll({ password: PASSWORD });
|
||||
const vk = await recover(PASSWORD, salt, serverWrapped);
|
||||
assert.equal(vk.length, 32);
|
||||
});
|
||||
|
||||
test('recover with wrong password fails', async () => {
|
||||
const { serverWrapped, salt } = await enroll({ password: PASSWORD });
|
||||
await assert.rejects(recover('wrong password', salt, serverWrapped), /decrypt|operation/i);
|
||||
});
|
||||
|
||||
test('account secret round-trips, and fails with wrong account AAD', async () => {
|
||||
const { serverWrapped, salt } = await enroll({ password: PASSWORD });
|
||||
const vk = await recover(PASSWORD, salt, serverWrapped);
|
||||
|
||||
const wrapped = await encryptSecret(vk, 'sk_live_secret_key_123', 'acct:twitch-1');
|
||||
const secret = await decryptSecret(vk, wrapped, 'acct:twitch-1');
|
||||
assert.equal(secret, 'sk_live_secret_key_123');
|
||||
|
||||
// The same ciphertext can't be re-attributed to a different account.
|
||||
await assert.rejects(decryptSecret(vk, wrapped, 'acct:kick-2'), /decrypt|operation/i);
|
||||
});
|
||||
|
||||
test('server never sees plaintext: wrapped blobs contain no secret bytes', async () => {
|
||||
const deviceKey = randomBytes(32);
|
||||
const secret = 'sk_topsecret';
|
||||
const { serverWrapped } = await enroll({ password: PASSWORD, deviceKey, deviceAad: AAD });
|
||||
const vk = await silentUnlock(deviceKey, (await enroll({ password: PASSWORD, deviceKey, deviceAad: AAD })).deviceWrapped, AAD);
|
||||
const wrappedSecret = await encryptSecret(vk, secret, 'acct:1');
|
||||
|
||||
const allBlobs = JSON.stringify({ serverWrapped, wrappedSecret });
|
||||
assert.ok(!allBlobs.includes(secret), 'ciphertext must not leak the plaintext secret');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue