Skip to content

Commit 922965c

Browse files
authored
feat: add system metadata assertion (#630)
This pull request introduces a new feature to include system metadata assertions during the encryption process in the `tdf3` library. The changes add support for automatically generating and embedding metadata about the system environment into the manifest of encrypted files. This functionality is configurable and has been thoroughly tested with new unit tests. ### Feature: System Metadata Assertions #### Core Implementation: * **`lib/tdf3/src/assertions.ts`**: Added the `SystemMetadata` type to define the structure of system metadata, and implemented the `getSystemMetadataAssertionConfig` function to generate default metadata assertions. This includes details such as TDF specification version, SDK version, operating system, and browser user agent. * **`lib/tdf3/src/tdf.ts`**: Modified the `writeStream` function to include system metadata assertions in the manifest if the `systemMetadataAssertion` flag is enabled in the encryption configuration. #### Configuration Enhancements: * **`lib/tdf3/src/client/builders.ts`**: Added the `systemMetadataAssertion` property to `EncryptParams` and a corresponding `withSystemMetadataAssertion` method in `EncryptParamsBuilder` for enabling this feature during encryption. [[1]](diffhunk://#diff-fa045a0a354fbc54f8447bbc5aeb39723e58b98150e278dc5584ee9309fdce6bR53) [[2]](diffhunk://#diff-fa045a0a354fbc54f8447bbc5aeb39723e58b98150e278dc5584ee9309fdce6bR503-R515) * **`lib/tdf3/src/tdf.ts`**: Updated the `EncryptConfiguration` type to include the `systemMetadataAssertion` flag. #### Client Integration: * **`lib/tdf3/src/client/index.ts`**: Passed the `systemMetadataAssertion` flag to the encryption configuration within the `Client` class. ### Testing: Unit Tests for System Metadata Assertions * **`lib/tests/mocha/encrypt-decrypt.spec.ts`**: Added a new test case to verify the inclusion and correctness of system metadata assertions in the manifest during encryption and decryption processes. This ensures that the metadata is correctly generated, embedded, and validated. These changes enhance the encryption process by providing additional metadata for debugging, auditing, or compliance purposes, while maintaining flexibility through optional configuration.
1 parent b763278 commit 922965c

File tree

5 files changed

+208
-2
lines changed

5 files changed

+208
-2
lines changed

lib/tdf3/src/assertions.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { canonicalizeEx } from 'json-canonicalize';
22
import { SignJWT, jwtVerify } from 'jose';
33
import { base64, hex } from '../../src/encodings/index.js';
44
import { ConfigurationError, IntegrityError, InvalidFileError } from '../../src/errors.js';
5+
import { tdfSpecVersion, version as sdkVersion } from '../../src/version.js';
56

67
export type AssertionKeyAlg = 'ES256' | 'RS256' | 'HS256';
78
export type AssertionType = 'handling' | 'other';
@@ -229,6 +230,54 @@ export type AssertionVerificationKeys = {
229230
Keys: Record<string, AssertionKey>;
230231
};
231232

233+
/**
234+
* Metadata structure for system information.
235+
*/
236+
type SystemMetadata = {
237+
tdf_spec_version: string;
238+
creation_date: string;
239+
sdk_version: string;
240+
browser_user_agent?: string;
241+
// platform is often the same as os in browser, but kept for consistency with original Go struct
242+
platform?: string;
243+
};
244+
245+
/**
246+
* Returns a default assertion configuration populated with system metadata.
247+
*/
248+
export function getSystemMetadataAssertionConfig(): AssertionConfig {
249+
let platformIdentifier = 'unknown';
250+
if (typeof navigator !== 'undefined') {
251+
if (typeof navigator.userAgent === 'string') {
252+
platformIdentifier = navigator.userAgent;
253+
} else if (typeof navigator.platform === 'string') {
254+
platformIdentifier = navigator.platform; // Deprecated, but used as a fallback
255+
}
256+
}
257+
258+
const metadata: SystemMetadata = {
259+
tdf_spec_version: tdfSpecVersion,
260+
creation_date: new Date().toISOString(),
261+
sdk_version: `JS-${sdkVersion}`, // Prefixed to distinguish from Go SDK version
262+
browser_user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
263+
platform: platformIdentifier,
264+
};
265+
266+
const metadataJSON = JSON.stringify(metadata);
267+
268+
return {
269+
id: 'system-metadata', // Consistent ID for this type of assertion
270+
type: 'other', // General type for metadata assertions
271+
scope: 'tdo', // Metadata typically applies to the TDF Data Object as a whole
272+
appliesToState: 'unencrypted', // Metadata itself is not encrypted by this assertion's scope
273+
statement: {
274+
format: 'json',
275+
schema: 'system-metadata-v1', // A schema name for this metadata
276+
value: metadataJSON,
277+
},
278+
};
279+
}
280+
232281
function concatenateUint8Arrays(array1: Uint8Array, array2: Uint8Array): Uint8Array {
233282
const combinedLength = array1.length + array2.length;
234283
const combinedArray = new Uint8Array(combinedLength);

lib/tdf3/src/client/builders.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type EncryptParams = {
5151
splitPlan?: SplitStep[];
5252
streamMiddleware?: EncryptStreamMiddleware;
5353
assertionConfigs?: AssertionConfig[];
54+
systemMetadataAssertion?: boolean;
5455
defaultKASEndpoint?: string;
5556

5657
// Preferred wrapping key algorithm. Used when KID resolution is not available.
@@ -500,6 +501,19 @@ class EncryptParamsBuilder {
500501
this._params.assertionConfigs = assertionConfigs;
501502
return this;
502503
}
504+
505+
/**
506+
* Specifies whether a default system metadata assertion should be automatically
507+
* included during the encryption process.
508+
*
509+
* @param {boolean} systemMetadataAssertion - True to include the system metadata assertion, false otherwise.
510+
* @returns {EncryptParamsBuilder} The current instance of the EncryptParamsBuilder for method chaining.
511+
* @see {@link getSystemMetadataAssertionConfig}
512+
*/
513+
withSystemMetadataAssertion(systemMetadataAssertion: boolean): EncryptParamsBuilder {
514+
this._params.systemMetadataAssertion = systemMetadataAssertion;
515+
return this;
516+
}
503517
}
504518

505519
export type DecryptKeyMiddleware = (key: Binary) => Promise<Binary>;

lib/tdf3/src/client/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,7 @@ export class Client {
721721
keyForEncryption,
722722
keyForManifest,
723723
assertionConfigs: opts.assertionConfigs,
724+
systemMetadataAssertion: opts.systemMetadataAssertion,
724725
tdfSpecVersion,
725726
};
726727

lib/tdf3/src/tdf.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export type EncryptConfiguration = {
147147
keyForEncryption: KeyInfo;
148148
keyForManifest: KeyInfo;
149149
assertionConfigs?: AssertionConfig[];
150+
systemMetadataAssertion?: boolean;
150151
tdfSpecVersion?: string;
151152
};
152153

@@ -534,8 +535,24 @@ export async function writeStream(cfg: EncryptConfiguration): Promise<DecoratedR
534535
manifest.encryptionInformation.integrityInformation.segments = segmentInfos;
535536

536537
manifest.encryptionInformation.method.isStreamable = true;
537-
538538
const signedAssertions: assertions.Assertion[] = [];
539+
if (cfg.systemMetadataAssertion) {
540+
const systemMetadataConfigBase = assertions.getSystemMetadataAssertionConfig();
541+
const signingKeyForSystemMetadata: AssertionKey = {
542+
alg: 'HS256', // Default algorithm, can be configured if needed
543+
key: new Uint8Array(cfg.keyForEncryption.unwrappedKeyBinary.asArrayBuffer()),
544+
};
545+
signedAssertions.push(
546+
await assertions.CreateAssertion(
547+
aggregateHash,
548+
{
549+
...systemMetadataConfigBase, // Spread the properties from the base config
550+
signingKey: signingKeyForSystemMetadata, // Add the signing key
551+
},
552+
cfg.tdfSpecVersion // Pass the TDF spec version
553+
)
554+
);
555+
}
539556
if (cfg.assertionConfigs && cfg.assertionConfigs.length > 0) {
540557
await Promise.all(
541558
cfg.assertionConfigs.map(async (assertionConfig) => {

lib/tests/mocha/encrypt-decrypt.spec.ts

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { KasPublicKeyAlgorithm } from '../../src/access.js';
66
import { AuthProvider, HttpRequest } from '../../src/auth/auth.js';
77
import { AesGcmCipher, KeyInfo, SplitKey, WebCryptoService } from '../../tdf3/index.js';
88
import { Client } from '../../tdf3/src/index.js';
9-
import { AssertionConfig, AssertionVerificationKeys } from '../../tdf3/src/assertions.js';
9+
import {
10+
AssertionConfig,
11+
AssertionVerificationKeys,
12+
getSystemMetadataAssertionConfig,
13+
Assertion,
14+
} from '../../tdf3/src/assertions.js';
1015
import { Scope } from '../../tdf3/src/client/builders.js';
1116
import { NetworkError } from '../../src/errors.js';
1217

@@ -364,4 +369,124 @@ describe('encrypt decrypt test', async function () {
364369
});
365370
}
366371
}
372+
373+
it('encrypt-decrypt with system metadata assertion', async function () {
374+
const cipher = new AesGcmCipher(WebCryptoService);
375+
const encryptionInformation = new SplitKey(cipher);
376+
const key1 = await encryptionInformation.generateKey();
377+
const keyMiddleware = async () => ({ keyForEncryption: key1, keyForManifest: key1 });
378+
379+
const client = new Client.Client({
380+
kasEndpoint: kasUrl,
381+
platformUrl: kasUrl,
382+
dpopKeys: Mocks.entityKeyPair(),
383+
clientId: 'id',
384+
authProvider,
385+
});
386+
387+
const scope: Scope = {
388+
dissem: ['[email protected]'],
389+
attributes: [],
390+
};
391+
392+
const encryptedStream = await client.encrypt({
393+
metadata: Mocks.getMetadataObject(),
394+
wrappingKeyAlgorithm: 'rsa:2048',
395+
offline: true,
396+
scope,
397+
keyMiddleware,
398+
source: new ReadableStream({
399+
start(controller) {
400+
controller.enqueue(new TextEncoder().encode(expectedVal));
401+
controller.close();
402+
},
403+
}),
404+
systemMetadataAssertion: true, // Enable the system metadata assertion
405+
});
406+
407+
// Consume the stream into a buffer. This also ensures manifest population is complete.
408+
const encryptedTdfBuffer = await encryptedStream.toBuffer();
409+
410+
// Verify the manifest for the system metadata assertion
411+
const manifest = encryptedStream.manifest;
412+
assert.isArray(manifest.assertions, 'Manifest assertions should be an array');
413+
assert.lengthOf(manifest.assertions, 1, 'Should have one assertion for system metadata');
414+
415+
const systemAssertion = manifest.assertions.find(
416+
(assertion: Assertion) => assertion.id === 'system-metadata'
417+
);
418+
assert.isDefined(systemAssertion, 'System metadata assertion should be found');
419+
if (systemAssertion) {
420+
assert.equal(systemAssertion.type, 'other', 'Assertion type should be "other"');
421+
assert.equal(systemAssertion.scope, 'tdo', 'Assertion scope should be "tdo"');
422+
assert.equal(systemAssertion.statement.format, 'json', 'Statement format should be "json"');
423+
assert.equal(
424+
systemAssertion.statement.schema,
425+
'system-metadata-v1',
426+
'Statement schema should be "system-metadata-v1"'
427+
);
428+
429+
const metadataValue = JSON.parse(systemAssertion.statement.value);
430+
assert.property(metadataValue, 'tdf_spec_version', 'Metadata should have tdfSpecVersion');
431+
assert.property(metadataValue, 'creation_date', 'Metadata should have creationDate');
432+
assert.property(metadataValue, 'sdk_version', 'Metadata should have sdkVersion');
433+
assert.property(metadataValue, 'browser_user_agent', 'Metadata should have browserUserAgent');
434+
assert.property(metadataValue, 'platform', 'Metadata should have platform');
435+
436+
// Compare Values
437+
const systemMetadata = getSystemMetadataAssertionConfig();
438+
assert.equal(systemMetadata.id, systemAssertion.id, 'ID should match');
439+
assert.equal(systemMetadata.type, systemAssertion.type, 'Type should match');
440+
assert.equal(systemMetadata.scope, systemAssertion.scope, 'Scope should match');
441+
assert.equal(
442+
systemMetadata.statement.format,
443+
systemAssertion.statement.format,
444+
'Statement format should match'
445+
);
446+
assert.equal(
447+
systemMetadata.statement.schema,
448+
systemAssertion.statement.schema,
449+
'Statement schema should match'
450+
);
451+
assert.equal(
452+
systemMetadata.appliesToState,
453+
systemAssertion.appliesToState,
454+
'AppliesToState should match'
455+
);
456+
457+
// Parse statement.value and compare individual fields, ignoring creationDate for direct equality
458+
const expectedMetadataValue = JSON.parse(systemMetadata.statement.value);
459+
const actualMetadataValue = JSON.parse(systemAssertion.statement.value);
460+
461+
assert.isString(actualMetadataValue.creation_date, 'creation_date should be a string');
462+
assert.isNotEmpty(actualMetadataValue.creation_date, 'creation_date should not be empty');
463+
assert.equal(
464+
actualMetadataValue.tdf_spec_version,
465+
expectedMetadataValue.tdf_spec_version,
466+
'tdf_spec_version should match'
467+
);
468+
assert.equal(
469+
actualMetadataValue.sdk_version,
470+
expectedMetadataValue.sdk_version,
471+
'sdk_version should match'
472+
);
473+
assert.equal(
474+
actualMetadataValue.browser_user_agent,
475+
expectedMetadataValue.browser_user_agent,
476+
'browser_user_agent should match'
477+
);
478+
assert.equal(
479+
actualMetadataValue.platform,
480+
expectedMetadataValue.platform,
481+
'platform should match'
482+
);
483+
}
484+
485+
const decryptStream = await client.decrypt({
486+
source: { type: 'buffer', location: encryptedTdfBuffer },
487+
});
488+
489+
const { value: decryptedText } = await decryptStream.stream.getReader().read();
490+
assert.equal(new TextDecoder().decode(decryptedText), expectedVal);
491+
});
367492
});

0 commit comments

Comments
 (0)