Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
50 changes: 50 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 '../../../lib/src/version.js';

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

/**
* Metadata structure for system information.
*/
type SystemMetadata = {
tdfSpecVersion: string;
creationDate: string;
os?: string;
sdkVersion: string;
browserUserAgent?: string; // Equivalent for GoVersion/runtime information
platform?: string; // Equivalent for Architecture/runtime information
};

/**
* 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 = {
tdfSpecVersion: tdfSpecVersion,
creationDate: new Date().toISOString(),
os: platformIdentifier,
sdkVersion: `JS-${sdkVersion}`, // Prefixed to distinguish from Go SDK version
browserUserAgent: 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 @@ -50,6 +50,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 @@ -499,6 +500,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 @@ -517,6 +517,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 @@ -527,8 +528,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
73 changes: 72 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 @@ -365,4 +370,70 @@ 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
});

// Verify the manifest for the system metadata assertion
const manifest = encryptedStream.manifest;
expect(Array.isArray(manifest.assertions)).toBe(true);
expect(manifest.assertions).toHaveLength(1); // Assuming only system metadata assertion for this test

const systemAssertion = manifest.assertions.find(
(assertion: Assertion) => assertion.id === 'system-metadata'
);
expect(systemAssertion).not.toBeUndefined();
if (systemAssertion) {
expect(systemAssertion.type).toBe('other');
expect(systemAssertion.scope).toBe('tdo');
expect(systemAssertion.statement.format).toBe('json');
expect(systemAssertion.statement.schema).toBe('system-metadata-v1');

const metadataValue = JSON.parse(systemAssertion.statement.value);
expect(metadataValue).toHaveProperty('tdfSpecVersion');
expect(metadataValue).toHaveProperty('creationDate');
expect(metadataValue).toHaveProperty('os');
expect(metadataValue).toHaveProperty('sdkVersion');
expect(metadataValue).toHaveProperty('browserUserAgent');
expect(metadataValue).toHaveProperty('platform');
}

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

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