Skip to content
Merged
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
49 changes: 49 additions & 0 deletions lib/tdf3/src/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { canonicalizeEx } from 'json-canonicalize';
import { SignJWT, jwtVerify } from 'jose';
import { base64, hex } from '../../src/encodings/index.js';
import { ConfigurationError, IntegrityError, InvalidFileError } from '../../src/errors.js';
import { tdfSpecVersion, version as sdkVersion } from '../../src/version.js';

export type AssertionKeyAlg = 'ES256' | 'RS256' | 'HS256';
export type AssertionType = 'handling' | 'other';
Expand Down Expand Up @@ -229,6 +230,54 @@ export type AssertionVerificationKeys = {
Keys: Record<string, AssertionKey>;
};

/**
* Metadata structure for system information.
*/
type SystemMetadata = {
tdf_spec_version: string;
creation_date: string;
sdk_version: string;
browser_user_agent?: string;
// platform is often the same as os in browser, but kept for consistency with original Go struct
platform?: string;
};

/**
* Returns a default assertion configuration populated with system metadata.
*/
export function getSystemMetadataAssertionConfig(): AssertionConfig {
let platformIdentifier = 'unknown';
if (typeof navigator !== 'undefined') {
if (typeof navigator.userAgent === 'string') {
platformIdentifier = navigator.userAgent;
} else if (typeof navigator.platform === 'string') {
platformIdentifier = navigator.platform; // Deprecated, but used as a fallback
}
}

const metadata: SystemMetadata = {
tdf_spec_version: tdfSpecVersion,
creation_date: new Date().toISOString(),
sdk_version: `JS-${sdkVersion}`, // Prefixed to distinguish from Go SDK version
browser_user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
platform: platformIdentifier,
};

const metadataJSON = JSON.stringify(metadata);

return {
id: 'system-metadata', // Consistent ID for this type of assertion
type: 'other', // General type for metadata assertions
scope: 'tdo', // Metadata typically applies to the TDF Data Object as a whole
appliesToState: 'unencrypted', // Metadata itself is not encrypted by this assertion's scope
statement: {
format: 'json',
schema: 'system-metadata-v1', // A schema name for this metadata
value: metadataJSON,
},
};
}

function concatenateUint8Arrays(array1: Uint8Array, array2: Uint8Array): Uint8Array {
const combinedLength = array1.length + array2.length;
const combinedArray = new Uint8Array(combinedLength);
Expand Down
14 changes: 14 additions & 0 deletions lib/tdf3/src/client/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type EncryptParams = {
splitPlan?: SplitStep[];
streamMiddleware?: EncryptStreamMiddleware;
assertionConfigs?: AssertionConfig[];
systemMetadataAssertion?: boolean;
defaultKASEndpoint?: string;

// Preferred wrapping key algorithm. Used when KID resolution is not available.
Expand Down Expand Up @@ -500,6 +501,19 @@ class EncryptParamsBuilder {
this._params.assertionConfigs = assertionConfigs;
return this;
}

/**
* Specifies whether a default system metadata assertion should be automatically
* included during the encryption process.
*
* @param {boolean} systemMetadataAssertion - True to include the system metadata assertion, false otherwise.
* @returns {EncryptParamsBuilder} The current instance of the EncryptParamsBuilder for method chaining.
* @see {@link getSystemMetadataAssertionConfig}
*/
withSystemMetadataAssertion(systemMetadataAssertion: boolean): EncryptParamsBuilder {
this._params.systemMetadataAssertion = systemMetadataAssertion;
return this;
}
}

export type DecryptKeyMiddleware = (key: Binary) => Promise<Binary>;
Expand Down
1 change: 1 addition & 0 deletions lib/tdf3/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ export class Client {
keyForEncryption,
keyForManifest,
assertionConfigs: opts.assertionConfigs,
systemMetadataAssertion: opts.systemMetadataAssertion,
tdfSpecVersion,
};

Expand Down
19 changes: 18 additions & 1 deletion lib/tdf3/src/tdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export type EncryptConfiguration = {
keyForEncryption: KeyInfo;
keyForManifest: KeyInfo;
assertionConfigs?: AssertionConfig[];
systemMetadataAssertion?: boolean;
tdfSpecVersion?: string;
};

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

manifest.encryptionInformation.method.isStreamable = true;

const signedAssertions: assertions.Assertion[] = [];
if (cfg.systemMetadataAssertion) {
const systemMetadataConfigBase = assertions.getSystemMetadataAssertionConfig();
const signingKeyForSystemMetadata: AssertionKey = {
alg: 'HS256', // Default algorithm, can be configured if needed
key: new Uint8Array(cfg.keyForEncryption.unwrappedKeyBinary.asArrayBuffer()),
};
signedAssertions.push(
await assertions.CreateAssertion(
aggregateHash,
{
...systemMetadataConfigBase, // Spread the properties from the base config
signingKey: signingKeyForSystemMetadata, // Add the signing key
},
cfg.tdfSpecVersion // Pass the TDF spec version
)
);
}
if (cfg.assertionConfigs && cfg.assertionConfigs.length > 0) {
await Promise.all(
cfg.assertionConfigs.map(async (assertionConfig) => {
Expand Down
127 changes: 126 additions & 1 deletion lib/tests/mocha/encrypt-decrypt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { KasPublicKeyAlgorithm } from '../../src/access.js';
import { AuthProvider, HttpRequest } from '../../src/auth/auth.js';
import { AesGcmCipher, KeyInfo, SplitKey, WebCryptoService } from '../../tdf3/index.js';
import { Client } from '../../tdf3/src/index.js';
import { AssertionConfig, AssertionVerificationKeys } from '../../tdf3/src/assertions.js';
import {
AssertionConfig,
AssertionVerificationKeys,
getSystemMetadataAssertionConfig,
Assertion,
} from '../../tdf3/src/assertions.js';
import { Scope } from '../../tdf3/src/client/builders.js';
import { NetworkError } from '../../src/errors.js';

Expand Down Expand Up @@ -364,4 +369,124 @@ describe('encrypt decrypt test', async function () {
});
}
}

it('encrypt-decrypt with system metadata assertion', async function () {
const cipher = new AesGcmCipher(WebCryptoService);
const encryptionInformation = new SplitKey(cipher);
const key1 = await encryptionInformation.generateKey();
const keyMiddleware = async () => ({ keyForEncryption: key1, keyForManifest: key1 });

const client = new Client.Client({
kasEndpoint: kasUrl,
platformUrl: kasUrl,
dpopKeys: Mocks.entityKeyPair(),
clientId: 'id',
authProvider,
});

const scope: Scope = {
dissem: ['[email protected]'],
attributes: [],
};

const encryptedStream = await client.encrypt({
metadata: Mocks.getMetadataObject(),
wrappingKeyAlgorithm: 'rsa:2048',
offline: true,
scope,
keyMiddleware,
source: new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(expectedVal));
controller.close();
},
}),
systemMetadataAssertion: true, // Enable the system metadata assertion
});

// Consume the stream into a buffer. This also ensures manifest population is complete.
const encryptedTdfBuffer = await encryptedStream.toBuffer();

// Verify the manifest for the system metadata assertion
const manifest = encryptedStream.manifest;
assert.isArray(manifest.assertions, 'Manifest assertions should be an array');
assert.lengthOf(manifest.assertions, 1, 'Should have one assertion for system metadata');

const systemAssertion = manifest.assertions.find(
(assertion: Assertion) => assertion.id === 'system-metadata'
);
assert.isDefined(systemAssertion, 'System metadata assertion should be found');
if (systemAssertion) {
assert.equal(systemAssertion.type, 'other', 'Assertion type should be "other"');
assert.equal(systemAssertion.scope, 'tdo', 'Assertion scope should be "tdo"');
assert.equal(systemAssertion.statement.format, 'json', 'Statement format should be "json"');
assert.equal(
systemAssertion.statement.schema,
'system-metadata-v1',
'Statement schema should be "system-metadata-v1"'
);

const metadataValue = JSON.parse(systemAssertion.statement.value);
assert.property(metadataValue, 'tdf_spec_version', 'Metadata should have tdfSpecVersion');
assert.property(metadataValue, 'creation_date', 'Metadata should have creationDate');
assert.property(metadataValue, 'sdk_version', 'Metadata should have sdkVersion');
assert.property(metadataValue, 'browser_user_agent', 'Metadata should have browserUserAgent');
assert.property(metadataValue, 'platform', 'Metadata should have platform');

// Compare Values
const systemMetadata = getSystemMetadataAssertionConfig();
assert.equal(systemMetadata.id, systemAssertion.id, 'ID should match');
assert.equal(systemMetadata.type, systemAssertion.type, 'Type should match');
assert.equal(systemMetadata.scope, systemAssertion.scope, 'Scope should match');
assert.equal(
systemMetadata.statement.format,
systemAssertion.statement.format,
'Statement format should match'
);
assert.equal(
systemMetadata.statement.schema,
systemAssertion.statement.schema,
'Statement schema should match'
);
assert.equal(
systemMetadata.appliesToState,
systemAssertion.appliesToState,
'AppliesToState should match'
);

// Parse statement.value and compare individual fields, ignoring creationDate for direct equality
const expectedMetadataValue = JSON.parse(systemMetadata.statement.value);
const actualMetadataValue = JSON.parse(systemAssertion.statement.value);

assert.isString(actualMetadataValue.creation_date, 'creation_date should be a string');
assert.isNotEmpty(actualMetadataValue.creation_date, 'creation_date should not be empty');
assert.equal(
actualMetadataValue.tdf_spec_version,
expectedMetadataValue.tdf_spec_version,
'tdf_spec_version should match'
);
assert.equal(
actualMetadataValue.sdk_version,
expectedMetadataValue.sdk_version,
'sdk_version should match'
);
assert.equal(
actualMetadataValue.browser_user_agent,
expectedMetadataValue.browser_user_agent,
'browser_user_agent should match'
);
assert.equal(
actualMetadataValue.platform,
expectedMetadataValue.platform,
'platform should match'
);
}

const decryptStream = await client.decrypt({
source: { type: 'buffer', location: encryptedTdfBuffer },
});

const { value: decryptedText } = await decryptStream.stream.getReader().read();
assert.equal(new TextDecoder().decode(decryptedText), expectedVal);
});
});
Loading