Skip to content

Commit 6ba9044

Browse files
feat(sdk): Adds OpenTDF.open method (#485)
This method allocates and returns a reader object, instead of directly returning an annotated webstream. This has several advantages, mostly for dealing with base TDFs at the moment: - allocating is synchronous and does not result in immediate network calls, allowing these objects to be pre-allocated. - inspection methods, most notably `attributes` and `manifest`, can be executed with loading less than the full file and without a key access server The attributes method does not work for remote or encrypted policies, and the manifest method doesn't have an implementation for Nano at the moment. Also potentially useful features, like accessing encrypted metadata, still also require doing a full decrypt, as the KAO processing code is still inline with the decrypt method in readStream
1 parent 203563c commit 6ba9044

File tree

16 files changed

+368
-113
lines changed

16 files changed

+368
-113
lines changed

.github/workflows/roundtrip/encrypt-decrypt.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,33 @@ _tdf3_test() {
6868
}
6969

7070
_tdf3_test @opentdf/ctl @opentdf/ctl
71+
72+
_tdf3_inspect_test() {
73+
counter=$((counter + 1))
74+
plain="./sample-${counter}.txt"
75+
echo "Hello World ${counter}" >"${plain}"
76+
npx "$1" --log-level DEBUG \
77+
--kasEndpoint http://localhost:65432/kas \
78+
--ignoreAllowList \
79+
--oidcEndpoint http://localhost:65432/auth/realms/opentdf \
80+
--auth testclient:secret \
81+
--output sample-with-attrs.txt.tdf \
82+
--attributes 'https://attr.io/attr/a/value/1,https://attr.io/attr/x/value/2' \
83+
encrypt "${plain}" \
84+
--containerType tdf3
85+
86+
[ -f sample-with-attrs.txt.tdf ]
87+
88+
npx "$1" --log-level DEBUG \
89+
inspect sample-with-attrs.txt.tdf > sample_inspect_out.txt
90+
91+
cat sample_inspect_out.txt
92+
93+
[ -f sample_inspect_out.txt ]
94+
grep -q 'https://attr.io/attr/a/value/1' sample_inspect_out.txt
95+
96+
echo "Inspect tdf3 successful!"
97+
rm -f "${plain}" sample-with-attrs.txt.tdf sample_inspect_out.txt
98+
}
99+
100+
_tdf3_inspect_test @opentdf/ctl

cli/bin/opentdf.bats

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
@test "requires optional arguments" {
1010
run $BATS_TEST_DIRNAME/opentdf.mjs encrypt noone
1111
echo "$output"
12-
[[ $output == *"Missing required"* ]]
12+
[[ $output == *"must be specified"* ]]
1313
}
1414

1515
@test "fails with missing file arguments" {

cli/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/cli.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,21 @@ type AuthToProcess = {
3030
clientId?: string;
3131
clientSecret?: string;
3232
concurrencyLimit?: number;
33-
oidcEndpoint: string;
33+
oidcEndpoint?: string;
3434
userId?: string;
3535
};
3636

3737
type LoggedAuthProvider = AuthProvider & {
3838
requestLog: HttpRequest[];
3939
};
4040

41+
class InvalidAuthProvider {
42+
async updateClientPublicKey(): Promise<void> {}
43+
withCreds(): Promise<HttpRequest> {
44+
throw new Error('Method not implemented.');
45+
}
46+
}
47+
4148
const bindingTypes = ['ecdsa', 'gmac'];
4249

4350
const containerTypes = ['tdf3', 'nano', 'dataset', 'ztdf'];
@@ -59,6 +66,9 @@ async function processAuth({
5966
userId,
6067
}: AuthToProcess): Promise<LoggedAuthProvider> {
6168
log('DEBUG', 'Processing auth params');
69+
if (!oidcEndpoint) {
70+
throw new CLIError('CRITICAL', 'oidcEndpoint must be specified');
71+
}
6272
if (auth) {
6373
log('DEBUG', 'Processing an auth string');
6474
const authParts = auth.split(':');
@@ -355,7 +365,6 @@ export const handleArgs = (args: string[]) => {
355365
description: 'URL to non-default KAS instance (https://mykas.net)',
356366
})
357367
.option('oidcEndpoint', {
358-
demandOption: true,
359368
group: 'Server Endpoints:',
360369
type: 'string',
361370
description: 'URL to non-default OIDC IdP (https://myidp.net)',
@@ -500,7 +509,6 @@ export const handleArgs = (args: string[]) => {
500509
},
501510
})
502511

503-
// COMMANDS
504512
.options({
505513
logLevel: {
506514
group: 'Verbosity:',
@@ -523,7 +531,7 @@ export const handleArgs = (args: string[]) => {
523531

524532
.command(
525533
'attrs',
526-
'Look up defintions of attributes',
534+
'Look up definitions of attributes',
527535
(yargs) => {
528536
yargs.strict();
529537
},
@@ -556,6 +564,36 @@ export const handleArgs = (args: string[]) => {
556564
}
557565
)
558566

567+
.command(
568+
'inspect [file]',
569+
'Inspect TDF or nanoTDF and extract header information, without decrypting',
570+
(yargs) => {
571+
yargs.strict().positional('file', {
572+
describe: 'path to encrypted file',
573+
type: 'string',
574+
});
575+
},
576+
async (argv) => {
577+
log('DEBUG', 'Running inspect command');
578+
const ct = new OpenTDF({
579+
authProvider: new InvalidAuthProvider(),
580+
});
581+
try {
582+
const reader = ct.open(await parseReadOptions(argv));
583+
const manifest = await reader.manifest();
584+
try {
585+
const dataAttributes = await reader.attributes();
586+
console.log(JSON.stringify({ manifest, dataAttributes }, null, 2));
587+
} catch (err) {
588+
console.error(err);
589+
console.log(JSON.stringify({ manifest }, null, 2));
590+
}
591+
} finally {
592+
ct.close();
593+
}
594+
}
595+
)
596+
559597
.command(
560598
'decrypt [file]',
561599
'Decrypt TDF to string',
@@ -573,6 +611,9 @@ export const handleArgs = (args: string[]) => {
573611
}
574612
log('DEBUG', `Allowed KASes: ${allowedKases}`);
575613
const ignoreAllowList = !!argv.ignoreAllowList;
614+
if (!argv.oidcEndpoint) {
615+
throw new CLIError('CRITICAL', 'oidcEndpoint must be specified');
616+
}
576617
const authProvider = await processAuth(argv);
577618
log('DEBUG', `Initialized auth provider ${JSON.stringify(authProvider)}`);
578619
const client = new OpenTDF({

lib/src/nanoclients.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
} from './nanotdf/index.js';
1111
import { keyAgreement } from './nanotdf-crypto/index.js';
1212
import { Policy } from './tdf/Policy.js';
13-
import { type TypedArray } from './tdf/TypedArray.js';
1413
import { createAttribute } from './tdf/AttributeObject.js';
1514
import { fetchECKasPubKey } from './access.js';
1615
import { ClientConfig } from './nanotdf/Client.js';
@@ -38,7 +37,7 @@ export class NanoTDFClient extends Client {
3837
*
3938
* @param ciphertext Ciphertext to decrypt
4039
*/
41-
async decrypt(ciphertext: string | TypedArray | ArrayBuffer): Promise<ArrayBuffer> {
40+
async decrypt(ciphertext: string | ArrayBufferLike): Promise<ArrayBuffer> {
4241
// Parse ciphertext
4342
const nanotdf = NanoTDF.from(ciphertext);
4443

@@ -68,7 +67,7 @@ export class NanoTDFClient extends Client {
6867
*
6968
* @param ciphertext Ciphertext to decrypt
7069
*/
71-
async decryptLegacyTDF(ciphertext: string | TypedArray | ArrayBuffer): Promise<ArrayBuffer> {
70+
async decryptLegacyTDF(ciphertext: string | ArrayBufferLike): Promise<ArrayBuffer> {
7271
// Parse ciphertext
7372
const nanotdf = NanoTDF.from(ciphertext, undefined, true);
7473

@@ -91,15 +90,12 @@ export class NanoTDFClient extends Client {
9190
/**
9291
* Encrypts the given data using the NanoTDF encryption scheme.
9392
*
94-
* @param {string | TypedArray | ArrayBuffer} data - The data to be encrypted.
95-
* @param {EncryptOptions} [options=defaultOptions] - The encryption options (currently unused).
96-
* @returns {Promise<ArrayBuffer>} A promise that resolves to the encrypted data as an ArrayBuffer.
97-
* @throws {Error} If the initialization vector is not a number.
93+
* @param data The data to be encrypted.
94+
* @param options The encryption options (currently unused).
95+
* @returns A promise that resolves to the encrypted data as an ArrayBuffer.
96+
* @throws If the initialization vector is not a number.
9897
*/
99-
async encrypt(
100-
data: string | TypedArray | ArrayBuffer,
101-
options?: EncryptOptions
102-
): Promise<ArrayBuffer> {
98+
async encrypt(data: string | ArrayBufferLike, options?: EncryptOptions): Promise<ArrayBuffer> {
10399
// For encrypt always generate the client ephemeralKeyPair
104100
const ephemeralKeyPair = await this.ephemeralKeyPair;
105101
const initializationVector = this.iv;
@@ -234,10 +230,7 @@ export class NanoTDFDatasetClient extends Client {
234230
*
235231
* @param data to decrypt
236232
*/
237-
async encrypt(
238-
data: string | TypedArray | ArrayBuffer,
239-
options?: EncryptOptions
240-
): Promise<ArrayBuffer> {
233+
async encrypt(data: string | ArrayBufferLike, options?: EncryptOptions): Promise<ArrayBuffer> {
241234
// Intial encrypt
242235
if (this.keyIterationCount == 0) {
243236
const mergedOptions: EncryptOptions = { ...defaultOptions, ...options };
@@ -323,7 +316,7 @@ export class NanoTDFDatasetClient extends Client {
323316
*
324317
* @param ciphertext Ciphertext to decrypt
325318
*/
326-
async decrypt(ciphertext: string | TypedArray | ArrayBuffer): Promise<ArrayBuffer> {
319+
async decrypt(ciphertext: string | ArrayBufferLike): Promise<ArrayBuffer> {
327320
// Parse ciphertext
328321
const nanotdf = NanoTDF.from(ciphertext);
329322

lib/src/nanotdf-crypto/digest.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import { TypedArray } from '../tdf/TypedArray.js';
2-
31
export default function digest(
42
hashType: AlgorithmIdentifier,
5-
data: TypedArray | ArrayBuffer
3+
data: ArrayBufferLike
64
): Promise<ArrayBuffer> {
75
return crypto.subtle.digest(hashType, data);
86
}

lib/src/nanotdf/Client.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { type TypedArray } from '../tdf/TypedArray.js';
21
import * as base64 from '../encodings/base64.js';
32
import { generateKeyPair, keyAgreement } from '../nanotdf-crypto/index.js';
43
import getHkdfSalt from './helpers/getHkdfSalt.js';
@@ -210,9 +209,9 @@ export default class Client {
210209
* @param clientVersion version of the client, as SemVer
211210
*/
212211
async rewrapKey(
213-
nanoTdfHeader: TypedArray | ArrayBuffer,
212+
nanoTdfHeader: ArrayBufferLike,
214213
kasRewrapUrl: string,
215-
magicNumberVersion: TypedArray | ArrayBuffer,
214+
magicNumberVersion: ArrayBufferLike,
216215
clientVersion: string
217216
): Promise<CryptoKey> {
218217
if (!this.allowedKases.allows(kasRewrapUrl)) {

lib/src/nanotdf/NanoTDF.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { TypedArray } from '../tdf/TypedArray.js';
21
import { base64 } from '../encodings/index.js';
32
import Header from './models/Header.js';
43
import Payload from './models/Payload.js';
@@ -22,7 +21,7 @@ export default class NanoTDF {
2221
public signature?: Signature;
2322

2423
static from(
25-
content: TypedArray | ArrayBuffer | string,
24+
content: ArrayBufferLike | string,
2625
encoding?: EncodingEnum,
2726
legacyTDF = false
2827
): NanoTDF {

lib/src/nanotdf/encrypt-dataset.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Header from './models/Header.js';
33
import DefaultParams from './models/DefaultParams.js';
44
import Payload from './models/Payload.js';
55
import { getBitLength as authTagLengthForCipher } from './models/Ciphers.js';
6-
import { TypedArray } from '../tdf/TypedArray.js';
76
import encrypt from '../nanotdf-crypto/encrypt.js';
87

98
/**
@@ -18,7 +17,7 @@ export default async function encryptDataset(
1817
symmetricKey: CryptoKey,
1918
header: Header,
2019
iv: Uint8Array,
21-
data: string | TypedArray | ArrayBuffer
20+
data: string | ArrayBufferLike
2221
): Promise<ArrayBuffer> {
2322
// Auth tag length for policy and payload
2423
const authTagLengthInBytes = authTagLengthForCipher(DefaultParams.symmetricCipher) / 8;

lib/src/nanotdf/encrypt.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import EmbeddedPolicy from './models/Policy/EmbeddedPolicy.js';
66
import Payload from './models/Payload.js';
77
import getHkdfSalt from './helpers/getHkdfSalt.js';
88
import { getBitLength as authTagLengthForCipher } from './models/Ciphers.js';
9-
import { TypedArray } from '../tdf/TypedArray.js';
109
import { GMAC_BINDING_LEN } from './constants.js';
1110
import { AlgorithmName, KeyFormat, KeyUsageType } from './../nanotdf-crypto/enums.js';
1211

@@ -35,7 +34,7 @@ export default async function encrypt(
3534
kasInfo: KasPublicKeyInfo,
3635
ephemeralKeyPair: CryptoKeyPair,
3736
iv: Uint8Array,
38-
data: string | TypedArray | ArrayBuffer,
37+
data: string | ArrayBufferLike,
3938
ecdsaBinding: boolean = DefaultParams.ecdsaBinding
4039
): Promise<ArrayBuffer> {
4140
// Generate a symmetric key.

0 commit comments

Comments
 (0)