Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP multikas plans #261

Closed
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
11 changes: 11 additions & 0 deletions lib/src/kas.ts → lib/src/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,14 @@ export async function fetchWrappedKey(

return response.json();
}

export async function fetchECKasPubKey(kasEndpoint: string): Promise<string> {
const kasPubKeyResponse = await fetch(`${kasEndpoint}/kas_public_key?algorithm=ec:secp256r1`);
if (!kasPubKeyResponse.ok) {
throw new Error(
`Unable to validate KAS [${kasEndpoint}]. Received [${kasPubKeyResponse.status}:${kasPubKeyResponse.statusText}]`
);
}
return kasPubKeyResponse.json();
}

15 changes: 3 additions & 12 deletions lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,7 @@ import {
import { keyAgreement, extractPublicFromCertToCrypto } from './nanotdf-crypto/index.js';
import { TypedArray, createAttribute, Policy } from './tdf/index.js';
import { AuthProvider } from './auth/auth.js';

async function fetchKasPubKey(kasUrl: string): Promise<string> {
const kasPubKeyResponse = await fetch(`${kasUrl}/kas_public_key?algorithm=ec:secp256r1`);
if (!kasPubKeyResponse.ok) {
throw new Error(
`Unable to validate KAS [${kasUrl}]. Received [${kasPubKeyResponse.status}:${kasPubKeyResponse.statusText}]`
);
}
return kasPubKeyResponse.json();
}
import { fetchECKasPubKey } from './access.js';

/**
* NanoTDF SDK Client
Expand Down Expand Up @@ -129,7 +120,7 @@ export class NanoTDFClient extends Client {
delete this.iv;

if (!this.kasPubKey) {
this.kasPubKey = await fetchKasPubKey(this.kasUrl);
this.kasPubKey = await fetchECKasPubKey(this.kasUrl);
}

// Create a policy for the tdf
Expand Down Expand Up @@ -253,7 +244,7 @@ export class NanoTDFDatasetClient extends Client {
const ephemeralKeyPair = await this.generateEphemeralKeyPair();

if (!this.kasPubKey) {
this.kasPubKey = await fetchKasPubKey(this.kasUrl);
this.kasPubKey = await fetchECKasPubKey(this.kasUrl);
}

// Create a policy for the tdf
Expand Down
2 changes: 1 addition & 1 deletion lib/src/nanotdf/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '../nanotdf-crypto/index.js';
import getHkdfSalt from './helpers/getHkdfSalt.js';
import DefaultParams from './models/DefaultParams.js';
import { fetchWrappedKey } from '../kas.js';
import { fetchWrappedKey } from '../access.js';
import { AuthProvider, reqSignature } from '../auth/providers.js';
import { safeUrlCheck, validateSecureUrl } from '../utils.js';

Expand Down
6 changes: 3 additions & 3 deletions lib/src/tdf/AttributeObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ export function createAttribute(
kasUrl: string
): AttributeObject {
return {
attribute: attribute,
attribute,
isDefault: false,
displayName: '',
pubKey: pubKey,
kasUrl: kasUrl,
pubKey,
kasUrl,
schemaVersion: '1.1.0',
};
}
2 changes: 2 additions & 0 deletions lib/tdf3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type DecryptKeyMiddleware,
type DecryptStreamMiddleware,
EncryptParamsBuilder,
type SplitStep,
} from './src/client/builders.js';
import { type SessionKeys, type ClientConfig, createSessionKeys } from './src/client/index.js';
import {
Expand Down Expand Up @@ -49,6 +50,7 @@ export type {
EncryptStreamMiddleware,
DecryptKeyMiddleware,
DecryptStreamMiddleware,
SplitStep,
};

export {
Expand Down
6 changes: 6 additions & 0 deletions lib/tdf3/src/client/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export type EncryptStreamMiddleware = (
stream: DecoratedReadableStream
) => Promise<DecoratedReadableStream>;

export type SplitStep = {
kas: string;
split?: string;
};

export type EncryptParams = {
source: ReadableStream<Uint8Array>;
opts?: { keypair: PemKeyPair };
Expand All @@ -40,6 +45,7 @@ export type EncryptParams = {
eo?: EntityObject;
payloadKey?: Binary;
keyMiddleware?: EncryptKeyMiddleware;
splitPlan?: SplitStep[],
streamMiddleware?: EncryptStreamMiddleware;
};

Expand Down
47 changes: 30 additions & 17 deletions lib/tdf3/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
DecryptStreamMiddleware,
EncryptKeyMiddleware,
EncryptStreamMiddleware,
SplitStep,
} from './builders.js';
import { DecoratedReadableStream } from './DecoratedReadableStream.js';

Expand Down Expand Up @@ -215,7 +216,7 @@ export class Client {
*/
readonly allowedKases: string[];

readonly kasPublicKey: Promise<KasPublicKeyInfo>;
readonly kasKeys: Record<string, Promise<KasPublicKeyInfo>>;

readonly easEndpoint?: string;

Expand Down Expand Up @@ -326,14 +327,19 @@ export class Client {
keypair: clientConfig.keypair,
});
}
this.kasKeys = {};
if (clientConfig.kasPublicKey) {
this.kasPublicKey = Promise.resolve({
url: this.kasEndpoint,
algorithm: 'rsa:2048',
publicKey: clientConfig.kasPublicKey,
});
} else {
this.kasPublicKey = fetchKasPublicKey(this.kasEndpoint);
this.kasKeys[this.kasEndpoint] = Promise.resolve({
url: this.kasEndpoint,
algorithm: 'rsa:2048',
publicKey: clientConfig.kasPublicKey,
});
}
for (const kasEndpoint of this.allowedKases) {
if (kasEndpoint in this.kasKeys) {
continue;
}
this.kasKeys[kasEndpoint] = fetchKasPublicKey(this.kasEndpoint);
}
}

Expand Down Expand Up @@ -391,9 +397,10 @@ export class Client {
eo,
keyMiddleware = defaultKeyMiddleware,
streamMiddleware = async (stream: DecoratedReadableStream) => stream,
splitPlan,
}: EncryptParams): Promise<DecoratedReadableStream | void> {
const sessionKeys = await this.sessionKeys;
const kasPublicKey = await this.kasPublicKey;

const policyObject = asPolicy(scope);
validatePolicyObject(policyObject);

Expand All @@ -409,14 +416,20 @@ export class Client {
eo.attributes.forEach((attr) => s.addJwtAttribute(attr));
attributeSet = s;
}
encryptionInformation.keyAccess.push(
await buildKeyAccess({
attributeSet,
type: offline ? 'wrapped' : 'remote',
url: kasPublicKey.url,
kid: kasPublicKey.kid,
publicKey: kasPublicKey.publicKey,
metadata,

const splits: SplitStep[] = splitPlan || [{kas: this.kasEndpoint}]
encryptionInformation.keyAccess = await Promise.all(
splits.map(async ({kas, split}) => {
const kasPublicKey = await this.kasKeys[kas];
return buildKeyAccess({
attributeSet,
type: offline ? 'wrapped' : 'remote',
url: kasPublicKey.url,
kid: kasPublicKey.kid,
publicKey: kasPublicKey.publicKey,
metadata,
split,
});
})
);
const { keyForEncryption, keyForManifest } = await (keyMiddleware as EncryptKeyMiddleware)();
Expand Down
18 changes: 11 additions & 7 deletions lib/tdf3/src/models/encryption-information.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ export type Segment = {
readonly encryptedSegmentSize?: number;
};

export type SplitType = 'split' | 'flat';

export type EncryptionInformation = {
readonly type: string;
readonly type: SplitType;
readonly keyAccess: KeyAccessObject[];
readonly integrityInformation: {
readonly rootSignature: {
Expand Down Expand Up @@ -75,19 +77,21 @@ export class SplitKey {
}

async getKeyAccessObjects(policy: Policy, keyInfo: KeyInfo): Promise<KeyAccessObject[]> {
const splitIds = [...new Set(this.keyAccess.map(({ split }) => split))].sort();
const unwrappedKeySplitBuffers = await keySplit(
new Uint8Array(keyInfo.unwrappedKeyBinary.asByteArray()),
this.keyAccess.length,
splitIds.length,
this.cryptoService
);
const splitsByName = Object.fromEntries(splitIds.map((split, index) => [split, unwrappedKeySplitBuffers[index]]));

const keyAccessObjects = [];
for (let i = 0; i < this.keyAccess.length; i++) {
for (const item of this.keyAccess) {
// use the key split to encrypt metadata for each key access object
const unwrappedKeySplitBuffer = unwrappedKeySplitBuffers[i];
const unwrappedKeySplitBuffer = splitsByName[item.split];
const unwrappedKeySplitBinary = Binary.fromArrayBuffer(unwrappedKeySplitBuffer.buffer);

const metadata = this.keyAccess[i].metadata || '';
const metadata = item.metadata || '';
const metadataStr = (
typeof metadata === 'object'
? JSON.stringify(metadata)
Expand All @@ -112,7 +116,7 @@ export class SplitKey {
};

const encryptedMetadataStr = JSON.stringify(encryptedMetadataOb);
const keyAccessObject = await this.keyAccess[i].write(
const keyAccessObject = await item.write(
policy,
unwrappedKeySplitBuffer,
encryptedMetadataStr
Expand All @@ -139,7 +143,7 @@ export class SplitKey {
const policyForManifest = base64.encode(JSON.stringify(policy));

return {
type: 'split',
type: 'flat',
keyAccess: keyAccessObjects,
method: {
algorithm,
Expand Down
10 changes: 8 additions & 2 deletions lib/tdf3/src/models/key-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export class Wrapped {
public readonly url: string,
public readonly kid: string | undefined,
public readonly publicKey: string,
public readonly metadata: unknown
public readonly metadata: unknown,
public readonly split: string
) {}

async write(
Expand Down Expand Up @@ -48,6 +49,9 @@ export class Wrapped {
if (this.kid) {
this.keyAccessObject.kid = this.kid;
}
if (this.split?.length) {
this.keyAccessObject.split = this.split;
}

return this.keyAccessObject;
}
Expand All @@ -63,7 +67,8 @@ export class Remote {
public readonly url: string,
public readonly kid: string | undefined,
public readonly publicKey: string,
public readonly metadata: unknown
public readonly metadata: unknown,
public readonly split: string
) {}

async write(
Expand Down Expand Up @@ -103,6 +108,7 @@ export class Remote {
export type KeyAccess = Remote | Wrapped;

export type KeyAccessObject = {
split?: string;
type: KeyAccessType;
url: string;
kid?: string;
Expand Down
48 changes: 41 additions & 7 deletions lib/tdf3/src/tdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
UpsertResponse,
Wrapped as KeyAccessWrapped,
KeyAccess,
KeyAccessObject,
SplitType,
} from './models/index.js';
import { base64 } from '../../src/encodings/index.js';
import {
Expand Down Expand Up @@ -69,7 +71,7 @@ export type EncryptionOptions = {
/**
* Defaults to `split`, the currently only implmented key wrap algorithm.
*/
type?: string;
type?: SplitType;
// Defaults to AES-256-GCM for the encryption.
cipher?: string;
};
Expand All @@ -91,6 +93,7 @@ export type BuildKeyAccess = {
publicKey: string;
attributeUrl?: string;
metadata?: Metadata;
split?: string;
};

type Segment = {
Expand Down Expand Up @@ -341,6 +344,7 @@ export async function buildKeyAccess({
kid,
attributeUrl,
metadata,
split = '',
}: BuildKeyAccess): Promise<KeyAccess> {
/** Internal function to keep it DRY */
function createKeyAccess(
Expand All @@ -352,9 +356,9 @@ export async function buildKeyAccess({
) {
switch (type) {
case 'wrapped':
return new KeyAccessWrapped(kasUrl, kasKeyIdentifier, pubKey, metadata);
return new KeyAccessWrapped(kasUrl, kasKeyIdentifier, pubKey, metadata, split);
case 'remote':
return new KeyAccessRemote(kasUrl, kasKeyIdentifier, pubKey, metadata);
return new KeyAccessRemote(kasUrl, kasKeyIdentifier, pubKey, metadata, split);
default:
throw new KeyAccessError(`buildKeyAccess: Key access type ${type} is unknown`);
}
Expand Down Expand Up @@ -801,6 +805,29 @@ async function loadTDFStream(
return { manifest, zipReader, centralDirectory };
}

export function splitLookupTableFactory(keyAccess: KeyAccessObject[], allowedKases: string[]): Record<string, Record<string, KeyAccessObject>> {
const splitIds = new Set(keyAccess.map(({split}) => split || ''));
const accessibleSplits = new Set(keyAccess.filter(({url}) => allowedKases.includes(url)).map(({split}) => split));
if (splitIds.size > accessibleSplits.size) {
const disallowedKases = new Set(keyAccess.filter(({url}) => !allowedKases.includes(url)).map(({url}) => url));
throw new KasDecryptError(
`Unreconstructable key - disallowed KASes include: [${JSON.stringify(disallowedKases)}] from splitIds [${JSON.stringify(splitIds)}]`
);
}
const splitPotentials: Record<string, Record<string, KeyAccessObject>> = Object.fromEntries([...splitIds].map(s => [s, {}]));
for (const kao of keyAccess) {
const disjunction = splitPotentials[kao.split || ''];
if (kao.url in disjunction) {
throw new KasDecryptError(`TODO: Fallback to no split ids. Repetition found for [${kao.url}] on split [${kao.split}]`);
}
if (allowedKases.includes(kao.url)) {
disjunction[kao.url] = kao;
}
}
return splitPotentials;

}

async function unwrapKey({
manifest,
allowedKases,
Expand All @@ -819,20 +846,27 @@ async function unwrapKey({
cryptoService: CryptoService;
}) {
if (authProvider === undefined) {
throw new Error('Upsert can be done without auth provider');
throw new KasDecryptError('Upsert can be done without auth provider');
}
const { keyAccess } = manifest.encryptionInformation;
const splitPotentials = splitLookupTableFactory(keyAccess, allowedKases);

let responseMetadata;
const isAppIdProvider = authProvider && isAppIdProviderCheck(authProvider);
const pkKeyLike = await importPKCS8(privateKey, 'RS256');
// Get key access information to know the KAS URLS
// TODO: logic that runs on multiple KAS's

const rewrappedKeys = await Promise.all(
keyAccess.map(async (keySplitInfo) => {
if (!allowedKases.includes(keySplitInfo.url)) {
throw new KasUpsertError(`Unexpected KAS url: [${keySplitInfo.url}]`);
Object.entries(splitPotentials).map(async ([splitId, potentials]) => {
if (!potentials || !Object.keys(potentials).length) {
throw new KasDecryptError(
`Unreconstructable key - no valid KAS found for split ${JSON.stringify(splitId)}`
);
}
// TODO: If we have multiple ways of getting a value, try the 'best' way
// or maybe retry across all potential ways
const [keySplitInfo] = Object.values(potentials);
const url = `${keySplitInfo.url}/${isAppIdProvider ? '' : 'v2/'}rewrap`;

const requestBodyStr = JSON.stringify({
Expand Down
Loading
Loading