Skip to content

Commit e1ae891

Browse files
fix(nano): Allow padding of kids (#338)
* fix(nano): Allow padding of kids also does a refactor: parse should take a buffer; constructor should take the types Still a lot of code duplication. Arguably these both should be static methods that call a hidden constructor * prettier * Update package.json * more error tests; simplified construction * sonarcloud suggestions * Update ResourceLocator.test.ts * more error condition tests * Update ResourceLocator.test.ts * fixup: names
1 parent 2e81fcb commit e1ae891

File tree

8 files changed

+171
-145
lines changed

8 files changed

+171
-145
lines changed

lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"test": "npm run build && npm run test:with-server",
5656
"test:with-server": "node dist/web/tests/server.js & trap \"node dist/web/tests/stopServer.js\" EXIT; npm run test:mocha && npm run test:wtr && npm run test:browser && npm run coverage:merge",
5757
"test:browser": "npx webpack --config webpack.test.config.cjs && npx karma start karma.conf.cjs",
58-
"test:mocha": "c8 --exclude=\"dist/web/tests/\" --exclude=\"dist/web/tdf3/src/utils/aws-lib-storage/\" --exclude=\"dist/web/tests/**/*\" --report-dir=./coverage/mocha mocha 'dist/web/tests/mocha/**/*.spec.js' --file dist/web/tests/mocha/setup.js && npx c8 report --reporter=json --report-dir=./coverage/mocha",
58+
"test:mocha": "c8 --exclude=\"dist/web/tests/**/*\" --report-dir=./coverage/mocha mocha 'dist/web/tests/mocha/**/*.spec.js' --file dist/web/tests/mocha/setup.js && npx c8 report --reporter=json --report-dir=./coverage/mocha",
5959
"test:wtr": "web-test-runner",
6060
"watch": "(trap 'kill 0' SIGINT; npm run build && (npm run build:watch & npm run test -- --watch))"
6161
},

lib/src/access.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ export async function fetchECKasPubKey(kasEndpoint: string): Promise<KasPublicKe
7575
`unable to load KAS public key from [${pkUrlV2}]. Received [${kasPubKeyResponse.status}:${kasPubKeyResponse.statusText}]`
7676
);
7777
}
78-
console.log('falling back to v1 key');
7978
// most likely a server that does not implement v2 endpoint, so no key identifier
8079
const pkUrlV1 = `${kasEndpoint}/kas_public_key?algorithm=ec:secp256r1`;
8180
const r2 = await fetch(pkUrlV1);
@@ -85,7 +84,6 @@ export async function fetchECKasPubKey(kasEndpoint: string): Promise<KasPublicKe
8584
);
8685
}
8786
const pem = await r2.json();
88-
console.log('pem returned', pem);
8987
return {
9088
key: pemToCryptoPublicKey(pem),
9189
publicKey: pem,

lib/src/nanotdf/encrypt.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,7 @@ export default async function encrypt(
4545
);
4646

4747
// Construct the kas locator
48-
let kasResourceLocator;
49-
if (kasInfo.kid) {
50-
kasResourceLocator = ResourceLocator.parse(kasInfo.url, kasInfo.kid);
51-
} else {
52-
kasResourceLocator = ResourceLocator.parse(kasInfo.url);
53-
}
48+
const kasResourceLocator = ResourceLocator.fromURL(kasInfo.url, kasInfo.kid);
5449

5550
// Auth tag length for policy and payload
5651
const authTagLengthInBytes = authTagLengthForCipher(DefaultParams.symmetricCipher) / 8;

lib/src/nanotdf/models/Header.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import CurveNameEnum from '../enum/CurveNameEnum.js';
1111
import { lengthOfPublicKey } from '../helpers/calculateByCurve.js';
1212
import DefaultParams from './DefaultParams.js';
1313
import { InvalidEphemeralKeyError } from '../../errors.js';
14+
import { rstrip } from '../../utils.js';
1415

1516
/**
1617
* NanoTDF Header
@@ -95,7 +96,7 @@ export default class Header {
9596
* @link https://github.com/virtru/nanotdf/blob/master/spec/index.md#3312-kas
9697
* @link https://github.com/virtru/nanotdf/blob/master/spec/index.md#341-resource-locator
9798
*/
98-
const kas = new ResourceLocator(buff.subarray(offset));
99+
const kas = ResourceLocator.parse(buff.subarray(offset));
99100
offset += kas.length;
100101

101102
/**
@@ -313,7 +314,7 @@ export default class Header {
313314
*/
314315
getKasRewrapUrl(): string {
315316
try {
316-
return `${this.kas.getUrl()}/v2/rewrap`;
317+
return `${rstrip(this.kas.url, '/')}/v2/rewrap`;
317318
} catch (e) {
318319
throw new Error(`Cannot construct KAS Rewrap URL: ${e.message}`);
319320
}

lib/src/nanotdf/models/Policy/RemotePolicy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class RemotePolicy extends AbstractPolicy implements RemotePolicyInterface {
1818
useEcdsaBinding: boolean
1919
): { offset: number; policy: RemotePolicy } {
2020
let offset = 0;
21-
const resource = new ResourceLocator(buff);
21+
const resource = ResourceLocator.parse(buff);
2222
offset += resource.offset;
2323

2424
const { binding, newOffset: bindingOffset } = this.parseBinding(buff, useEcdsaBinding, offset);

lib/src/nanotdf/models/ResourceLocator.ts

Lines changed: 103 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@ import ResourceLocatorIdentifierEnum from '../enum/ResourceLocatorIdentifierEnum
1717
* @link https://github.com/virtru/nanotdf/blob/master/spec/index.md#341-resource-locator
1818
*/
1919
export default class ResourceLocator {
20-
readonly protocol: ProtocolEnum;
21-
readonly lengthOfBody: number;
22-
readonly body: string;
23-
readonly identifier: string;
24-
readonly identifierType: ResourceLocatorIdentifierEnum = ResourceLocatorIdentifierEnum.None;
25-
readonly offset: number = 0;
26-
2720
static readonly PROTOCOL_OFFSET = 0;
2821
static readonly PROTOCOL_LENGTH = 1;
2922
static readonly LENGTH_OFFSET = 1;
@@ -34,100 +27,121 @@ export default class ResourceLocator {
3427
static readonly IDENTIFIER_8_BYTE: number = 2 << 4; // 32
3528
static readonly IDENTIFIER_32_BYTE: number = 3 << 4; // 48
3629

37-
static parse(url: string, identifier: string = ''): ResourceLocator {
38-
const [protocol, body] = url.split('://');
30+
constructor(
31+
readonly protocol: ProtocolEnum,
32+
readonly lengthOfBody: number,
33+
readonly body: string,
34+
readonly offset: number,
35+
readonly id?: string,
36+
readonly idType: ResourceLocatorIdentifierEnum = ResourceLocatorIdentifierEnum.None
37+
) {}
38+
39+
static fromURL(url: string, identifier?: string): ResourceLocator {
40+
const [protocolStr, body] = url.split('://');
41+
42+
let protocol: ProtocolEnum;
3943

4044
// Validate and set protocol identifier byte
41-
const protocolIdentifierByte = new Uint8Array(1);
42-
switch (protocol.toLowerCase()) {
45+
switch (protocolStr.toLowerCase()) {
4346
case 'http':
44-
protocolIdentifierByte[0] = 0x00;
47+
protocol = ProtocolEnum.Http;
4548
break;
4649
case 'https':
47-
protocolIdentifierByte[0] = 0x01;
50+
protocol = ProtocolEnum.Https;
4851
break;
4952
default:
50-
throw new Error('resource locator protocol unsupported');
53+
throw new Error(`resource locator protocol [${protocolStr}] unsupported`);
5154
}
5255

5356
// Set identifier padded length and protocol identifier byte
54-
const identifierPaddedLength = (() => {
55-
switch (identifier.length) {
56-
case 0:
57-
protocolIdentifierByte[0] |= ResourceLocator.IDENTIFIER_0_BYTE;
58-
return ResourceLocatorIdentifierEnum.None.valueOf();
59-
case 2:
60-
protocolIdentifierByte[0] |= ResourceLocator.IDENTIFIER_2_BYTE;
61-
return ResourceLocatorIdentifierEnum.TwoBytes.valueOf();
62-
case 8:
63-
protocolIdentifierByte[0] |= ResourceLocator.IDENTIFIER_8_BYTE;
64-
return ResourceLocatorIdentifierEnum.EightBytes.valueOf();
65-
case 32:
66-
protocolIdentifierByte[0] |= ResourceLocator.IDENTIFIER_32_BYTE;
67-
return ResourceLocatorIdentifierEnum.ThirtyTwoBytes.valueOf();
68-
default:
69-
throw new Error(`Unsupported identifier length: ${identifier.length}`);
57+
const identifierType = (() => {
58+
if (!identifier) {
59+
return ResourceLocatorIdentifierEnum.None;
60+
}
61+
const identifierLength = new TextEncoder().encode(identifier).length;
62+
if (identifierLength <= 2) {
63+
return ResourceLocatorIdentifierEnum.TwoBytes;
64+
} else if (identifierLength <= 8) {
65+
return ResourceLocatorIdentifierEnum.EightBytes;
66+
} else if (identifierLength <= 32) {
67+
return ResourceLocatorIdentifierEnum.ThirtyTwoBytes;
7068
}
69+
throw new Error(`unsupported identifier length: ${identifier.length}`);
7170
})();
7271

7372
// Create buffer to hold protocol, body length, body, and identifier
74-
const bodyBytes = new TextEncoder().encode(body);
75-
const buffer = new Uint8Array(1 + 1 + bodyBytes.length + identifierPaddedLength);
76-
77-
// Set the protocol, body length, body and identifier into buffer
78-
buffer.set(protocolIdentifierByte, 0);
79-
buffer.set([bodyBytes.length], 1);
80-
buffer.set(bodyBytes, 2);
81-
82-
if (identifierPaddedLength > 0) {
83-
const identifierBytes = new TextEncoder()
84-
.encode(identifier)
85-
.subarray(0, identifierPaddedLength);
86-
buffer.set(identifierBytes, 2 + bodyBytes.length);
73+
const lengthOfBody = new TextEncoder().encode(body).length;
74+
if (lengthOfBody == 0) {
75+
throw new Error('url body empty');
8776
}
88-
89-
return new ResourceLocator(buffer);
77+
const identifierLength = identifierType.valueOf();
78+
const offset = ResourceLocator.BODY_OFFSET + lengthOfBody + identifierLength;
79+
return new ResourceLocator(protocol, lengthOfBody, body, offset, identifier, identifierType);
9080
}
9181

92-
constructor(buff: Uint8Array) {
82+
static parse(buff: Uint8Array) {
9383
// Protocol
94-
this.protocol = buff[ResourceLocator.PROTOCOL_OFFSET];
84+
const protocolAndIdentifierType = buff[ResourceLocator.PROTOCOL_OFFSET];
9585
// Length of body
96-
this.lengthOfBody = buff[ResourceLocator.LENGTH_OFFSET];
86+
const lengthOfBody = buff[ResourceLocator.LENGTH_OFFSET];
87+
if (lengthOfBody == 0) {
88+
throw new Error('url body empty');
89+
}
9790
// Body as utf8 string
9891
const decoder = new TextDecoder();
99-
this.body = decoder.decode(
100-
buff.subarray(ResourceLocator.BODY_OFFSET, ResourceLocator.BODY_OFFSET + this.lengthOfBody)
101-
);
92+
let offset = ResourceLocator.BODY_OFFSET + lengthOfBody;
93+
if (offset > buff.length) {
94+
throw new Error('parse out of bounds error');
95+
}
96+
const body = decoder.decode(buff.subarray(ResourceLocator.BODY_OFFSET, offset));
97+
const protocol = protocolAndIdentifierType & 0xf;
98+
switch (protocol) {
99+
case ProtocolEnum.Http:
100+
case ProtocolEnum.Https:
101+
break;
102+
default:
103+
throw new Error(`unsupported protocol type [${protocol}]`);
104+
}
102105
// identifier
103-
const identifierTypeNibble = this.protocol & 0xf0;
106+
const identifierTypeNibble = protocolAndIdentifierType & 0xf0;
107+
let identifierType = ResourceLocatorIdentifierEnum.None;
104108
if (identifierTypeNibble === ResourceLocator.IDENTIFIER_2_BYTE) {
105-
this.identifierType = ResourceLocatorIdentifierEnum.TwoBytes;
109+
identifierType = ResourceLocatorIdentifierEnum.TwoBytes;
106110
} else if (identifierTypeNibble === ResourceLocator.IDENTIFIER_8_BYTE) {
107-
this.identifierType = ResourceLocatorIdentifierEnum.EightBytes;
111+
identifierType = ResourceLocatorIdentifierEnum.EightBytes;
108112
} else if (identifierTypeNibble === ResourceLocator.IDENTIFIER_32_BYTE) {
109-
this.identifierType = ResourceLocatorIdentifierEnum.ThirtyTwoBytes;
113+
identifierType = ResourceLocatorIdentifierEnum.ThirtyTwoBytes;
114+
} else if (identifierTypeNibble !== ResourceLocator.IDENTIFIER_0_BYTE) {
115+
throw new Error(`unsupported key identifier type [${identifierTypeNibble}]`);
110116
}
111-
switch (this.identifierType) {
117+
118+
let identifier: string | undefined = undefined;
119+
120+
switch (identifierType) {
112121
case ResourceLocatorIdentifierEnum.None:
113122
// noop
114123
break;
115124
case ResourceLocatorIdentifierEnum.TwoBytes:
116125
case ResourceLocatorIdentifierEnum.EightBytes:
117-
case ResourceLocatorIdentifierEnum.ThirtyTwoBytes:
118-
const start = ResourceLocator.BODY_OFFSET + this.lengthOfBody;
119-
const end = start + this.identifierType.valueOf();
120-
const subarray = buff.subarray(start, end);
126+
case ResourceLocatorIdentifierEnum.ThirtyTwoBytes: {
127+
const kidStart = offset;
128+
offset = kidStart + identifierType.valueOf();
129+
if (offset > buff.length) {
130+
throw new Error('parse out of bounds error');
131+
}
132+
const kidSubarray = buff.subarray(kidStart, offset);
121133
// Remove padding (assuming the padding is null bytes, 0x00)
122-
const trimmedSubarray = subarray.filter((byte) => byte !== 0x00);
123-
this.identifier = decoder.decode(trimmedSubarray);
134+
const zeroIndex = kidSubarray.indexOf(0);
135+
if (zeroIndex >= 0) {
136+
const trimmedSubarray = kidSubarray.subarray(0, zeroIndex);
137+
identifier = decoder.decode(trimmedSubarray);
138+
} else {
139+
identifier = decoder.decode(kidSubarray);
140+
}
124141
break;
142+
}
125143
}
126-
this.offset =
127-
ResourceLocator.PROTOCOL_LENGTH +
128-
ResourceLocator.LENGTH_LENGTH +
129-
this.lengthOfBody +
130-
this.identifierType.valueOf();
144+
return new ResourceLocator(protocol, lengthOfBody, body, offset, identifier, identifierType);
131145
}
132146

133147
/**
@@ -136,20 +150,11 @@ export default class ResourceLocator {
136150
* @returns { number } Length of resource locator
137151
*/
138152
get length(): number {
139-
return (
140-
// Protocol
141-
1 +
142-
// Length of the body( 1 byte)
143-
1 +
144-
// Content length
145-
this.body.length +
146-
// Identifier length
147-
this.identifierType.valueOf()
148-
);
153+
return this.offset;
149154
}
150155

151156
get url(): string | never {
152-
switch (this.protocol & 0xf) {
157+
switch (this.protocol) {
153158
case ProtocolEnum.Http:
154159
return 'http://' + this.body;
155160
case ProtocolEnum.Https:
@@ -163,33 +168,26 @@ export default class ResourceLocator {
163168
* Return the contents of the Resource Locator in buffer
164169
*/
165170
toBuffer(): Uint8Array {
166-
const buffer = new Uint8Array(2 + this.body.length + this.identifierType.valueOf());
167-
buffer.set([this.protocol], 0);
168-
buffer.set([this.lengthOfBody], 1);
169-
buffer.set(new TextEncoder().encode(this.body), 2);
170-
if (this.identifier) {
171-
buffer.set(new TextEncoder().encode(this.identifier), 2 + this.body.length);
171+
const buffer = new Uint8Array(ResourceLocator.BODY_OFFSET + this.body.length + this.idType);
172+
let idTypeNibble = 0;
173+
switch (this.idType) {
174+
case ResourceLocatorIdentifierEnum.TwoBytes:
175+
idTypeNibble = ResourceLocator.IDENTIFIER_2_BYTE;
176+
break;
177+
case ResourceLocatorIdentifierEnum.EightBytes:
178+
idTypeNibble = ResourceLocator.IDENTIFIER_8_BYTE;
179+
break;
180+
case ResourceLocatorIdentifierEnum.ThirtyTwoBytes:
181+
idTypeNibble = ResourceLocator.IDENTIFIER_32_BYTE;
182+
break;
172183
}
173-
return buffer;
174-
}
175-
176-
/**
177-
* Get URL
178-
*
179-
* Construct URL from ResourceLocator or throw error
180-
*/
181-
getUrl(): string | never {
182-
let protocol: string;
183-
// protocolIndex get the first four bits
184-
const protocolIndex: number = this.protocol & 0xf;
185-
if (protocolIndex === ProtocolEnum.Http) {
186-
protocol = 'http';
187-
} else if (protocolIndex === ProtocolEnum.Https) {
188-
protocol = 'https';
189-
} else {
190-
throw new Error(`Cannot create URL from protocol, "${ProtocolEnum[this.protocol]}"`);
184+
buffer.set([this.protocol | idTypeNibble], ResourceLocator.PROTOCOL_OFFSET);
185+
buffer.set([this.lengthOfBody], ResourceLocator.LENGTH_OFFSET);
186+
buffer.set(new TextEncoder().encode(this.body), ResourceLocator.BODY_OFFSET);
187+
if (this.id) {
188+
buffer.set(new TextEncoder().encode(this.id), ResourceLocator.BODY_OFFSET + this.body.length);
191189
}
192-
return `${protocol}://${this.body}`;
190+
return buffer;
193191
}
194192

195193
/**
@@ -198,7 +196,7 @@ export default class ResourceLocator {
198196
* Returns the identifier of the ResourceLocator or an empty string if no identifier is present.
199197
* @returns { string } Identifier of the resource locator.
200198
*/
201-
getIdentifier(): string {
202-
return this.identifier || '';
199+
get identifier(): string {
200+
return this.id ?? '';
203201
}
204202
}

lib/tests/web/nano-roundtrip.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ describe('Local roundtrip Tests', () => {
2323
const cipherText = await client.encrypt('hello world');
2424
const client2 = new NanoTDFClient({ authProvider, kasEndpoint });
2525
const nanotdfParsed = NanoTDF.from(cipherText);
26+
2627
expect(nanotdfParsed.header.kas.url).to.equal(kasEndpoint);
27-
expect(nanotdfParsed.header.kas.getIdentifier()).to.equal('e1');
28+
expect(nanotdfParsed.header.kas.identifier).to.equal('e1');
29+
2830
const actual = await client2.decrypt(cipherText);
2931
expect(new TextDecoder().decode(actual)).to.be.equal('hello world');
3032
});

0 commit comments

Comments
 (0)