Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions .github/workflows/format.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: '🤖 🎨'
on:
pull_request:
# on:
# pull_request:
jobs:
format:
runs-on: ubuntu-latest
Expand Down
69 changes: 66 additions & 3 deletions lib/src/access/access-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import {
} from '../access.js';

import { type AuthProvider } from '../auth/auth.js';
import { ConfigurationError, NetworkError } from '../errors.js';
import {
ConfigurationError,
InvalidFileError,
NetworkError,
PermissionDeniedError,
ServiceError,
UnauthenticatedError,
} from '../errors.js';
import { PlatformClient } from '../platform.js';
import { RewrapResponse } from '../platform/kas/kas_pb.js';
import { ListKeyAccessServersResponse } from '../platform/policy/kasregistry/key_access_server_registry_pb.js';
Expand All @@ -19,6 +26,7 @@ import {
validateSecureUrl,
} from '../utils.js';
import { X_REWRAP_ADDITIONAL_CONTEXT } from './constants.js';
import { ConnectError, Code } from '@connectrpc/connect';

/**
* Get a rewrapped access key to the document, if possible
Expand All @@ -42,11 +50,66 @@ export async function fetchWrappedKey(
[X_REWRAP_ADDITIONAL_CONTEXT]: rewrapAdditionalContextHeader,
};
}
let response: RewrapResponse;
try {
return await platform.v1.access.rewrap({ signedRequestToken }, options);
response = await platform.v1.access.rewrap({ signedRequestToken }, options);
} catch (e) {
throw new NetworkError(`[${platformUrl}] [Rewrap] ${extractRpcErrorMessage(e)}`);
handleRpcRewrapError(e, platformUrl);
}
return response;
}

export function handleRpcRewrapError(e: unknown, platformUrl: string): never {
if (e instanceof ConnectError) {
console.log('Error is a ConnectError with code:', e.code);
switch (e.code) {
case Code.InvalidArgument: // 400 Bad Request
throw new InvalidFileError(`400 for [${platformUrl}]: rewrap bad request [${e.message}]`);
case Code.PermissionDenied: // 403 Forbidden
throw new PermissionDeniedError(`403 for [${platformUrl}]; rewrap permission denied`);
case Code.Unauthenticated: // 401 Unauthorized
throw new UnauthenticatedError(`401 for [${platformUrl}]; rewrap auth failure`);
case Code.Internal:
case Code.Unimplemented:
case Code.DataLoss:
case Code.Unknown:
case Code.DeadlineExceeded:
case Code.Unavailable: // >=500 Server Error
throw new ServiceError(
`${e.code} for [${platformUrl}]: rewrap failure due to service error [${e.message}]`
);
default:
throw new NetworkError(`[${platformUrl}] [Rewrap] ${e.message}`);
}
}
throw new NetworkError(`[${platformUrl}] [Rewrap] ${extractRpcErrorMessage(e)}`);
}

export function handleRpcRewrapErrorString(e: string, platformUrl: string): never {
if (e.includes(Code[Code.InvalidArgument])) {
// 400 Bad Request
throw new InvalidFileError(`400 for [${platformUrl}]: rewrap bad request [${e}]`);
}
if (e.includes(Code[Code.PermissionDenied])) {
// 403 Forbidden
throw new PermissionDeniedError(`403 for [${platformUrl}]; rewrap permission denied`);
}
if (e.includes(Code[Code.Unauthenticated])) {
// 401 Unauthorized
throw new UnauthenticatedError(`401 for [${platformUrl}]; rewrap auth failure`);
}
if (
e.includes(Code[Code.Internal]) ||
e.includes(Code[Code.Unimplemented]) ||
e.includes(Code[Code.DataLoss]) ||
e.includes(Code[Code.Unknown]) ||
e.includes(Code[Code.DeadlineExceeded]) ||
e.includes(Code[Code.Unavailable])
) {
// >=500
throw new ServiceError(`500+ [${platformUrl}]: rewrap failure due to service error [${e}]`);
}
throw new NetworkError(`[${platformUrl}] [Rewrap] ${e}}`);
}

export async function fetchKeyAccessServers(
Expand Down
61 changes: 52 additions & 9 deletions lib/src/nanotdf/Client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import * as base64 from '../encodings/base64.js';
import { create, toJsonString } from '@bufbuild/protobuf';
import {
UnsignedRewrapRequest_WithPolicyRequestSchema,
UnsignedRewrapRequestSchema,
} from '../platform/kas/kas_pb.js';
import { generateKeyPair, keyAgreement } from '../nanotdf-crypto/index.js';
import getHkdfSalt from './helpers/getHkdfSalt.js';
import DefaultParams from './models/DefaultParams.js';
Expand All @@ -8,13 +12,16 @@ import {
KasPublicKeyInfo,
OriginAllowList,
} from '../access.js';
import { handleRpcRewrapErrorString } from '../../src/access/access-rpc.js';
import { AuthProvider, isAuthProvider, reqSignature } from '../auth/providers.js';
import { ConfigurationError, DecryptError, TdfError, UnsafeUrlError } from '../errors.js';
import {
cryptoPublicToPem,
getRequiredObligationFQNs,
pemToCryptoPublicKey,
upgradeRewrapResponseV1,
validateSecureUrl,
getPlatformUrlFromKasEndpoint,
} from '../utils.js';

export interface ClientConfig {
Expand Down Expand Up @@ -260,18 +267,35 @@ export default class Client {
throw new ConfigurationError('Signer key has not been set or generated');
}

const requestBodyStr = JSON.stringify({
algorithm: DefaultParams.defaultECAlgorithm,
// nano keyAccess minimum, header is used for nano
const unsignedRequest = create(UnsignedRewrapRequestSchema, {
clientPublicKey: await cryptoPublicToPem(ephemeralKeyPair.publicKey),
requests: [
create(UnsignedRewrapRequest_WithPolicyRequestSchema, {
keyAccessObjects: [
{
keyAccessObjectId: 'kao-0',
keyAccessObject: {
header: new Uint8Array(nanoTdfHeader),
kasUrl: '',
protocol: Client.KAS_PROTOCOL,
keyType: Client.KEY_ACCESS_REMOTE,
},
},
],
algorithm: DefaultParams.defaultECAlgorithm,
}),
],
keyAccess: {
type: Client.KEY_ACCESS_REMOTE,
url: '',
header: new Uint8Array(nanoTdfHeader),
kasUrl: '',
protocol: Client.KAS_PROTOCOL,
header: base64.encodeArrayBuffer(nanoTdfHeader),
keyType: Client.KEY_ACCESS_REMOTE,
},
clientPublicKey: await cryptoPublicToPem(ephemeralKeyPair.publicKey),
algorithm: DefaultParams.defaultECAlgorithm,
});

const requestBodyStr = toJsonString(UnsignedRewrapRequestSchema, unsignedRequest);

const jwtPayload = { requestBody: requestBodyStr };

const signedRequestToken = await reqSignature(jwtPayload, requestSignerKeyPair.privateKey, {
Expand All @@ -285,9 +309,28 @@ export default class Client {
this.authProvider,
this.fulfillableObligationFQNs
);
upgradeRewrapResponseV1(rewrapResp);

// Assume only one response and one result for now (V1 style)
const result = rewrapResp.responses[0].results[0];
let entityWrappedKey: Uint8Array<ArrayBufferLike>;
switch (result.result.case) {
case 'kasWrappedKey': {
entityWrappedKey = result.result.value;
break;
}
case 'error': {
handleRpcRewrapErrorString(
result.result.value,
getPlatformUrlFromKasEndpoint(kasRewrapUrl)
);
}
default: {
throw new DecryptError('KAS rewrap response missing wrapped key');
}
}

// Extract the iv and ciphertext
const entityWrappedKey = rewrapResp.entityWrappedKey;
const ivLength =
clientVersion == Client.SDK_INITIAL_RELEASE ? Client.INITIAL_RELEASE_IV_SIZE : Client.IV_SIZE;
const iv = entityWrappedKey.subarray(0, ivLength);
Expand Down
35 changes: 34 additions & 1 deletion lib/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { exportSPKI, importX509 } from 'jose';
import { base64 } from './encodings/index.js';
import { pemCertToCrypto, pemPublicToCrypto } from './nanotdf-crypto/pemPublicToCrypto.js';
import { ConfigurationError } from './errors.js';
import { RewrapResponse } from './platform/kas/kas_pb.js';
import {
RewrapResponse,
PolicyRewrapResultSchema,
KeyAccessRewrapResultSchema,
} from './platform/kas/kas_pb.js';
import { create } from '@bufbuild/protobuf';
import { ConnectError } from '@connectrpc/connect';

const REQUIRED_OBLIGATIONS_METADATA_KEY = 'X-Required-Obligations';
Expand Down Expand Up @@ -255,3 +260,31 @@ export function getRequiredObligationFQNs(response: RewrapResponse) {

return [...requiredObligations.values()];
}

/**
* Upgrades a RewrapResponse from v1 format to v2.
*/
export function upgradeRewrapResponseV1(response: RewrapResponse) {
if (response.responses.length > 0) {
return;
}
if (response.entityWrappedKey.length === 0) {
return;
}

response.responses = [
create(PolicyRewrapResultSchema, {
policyId: 'policy',
results: [
create(KeyAccessRewrapResultSchema, {
keyAccessObjectId: 'kao-0',
status: 'permit',
result: {
case: 'kasWrappedKey',
value: response.entityWrappedKey,
},
}),
],
}),
];
}
Loading
Loading