Skip to content

Commit 031bbb7

Browse files
authored
fix(nano): ecdsa policy binding support for encrypt (#346)
1 parent e1ae891 commit 031bbb7

File tree

6 files changed

+310
-41
lines changed

6 files changed

+310
-41
lines changed

lib/src/index.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ import { TypedArray, createAttribute, Policy } from './tdf/index.js';
1313
import { fetchECKasPubKey } from './access.js';
1414
import { ClientConfig } from './nanotdf/Client.js';
1515

16+
// Define the EncryptOptions type
17+
export type EncryptOptions = {
18+
ecdsaBinding: boolean;
19+
};
20+
21+
// Define default options
22+
const defaultOptions: EncryptOptions = {
23+
ecdsaBinding: false,
24+
};
25+
1626
/**
1727
* NanoTDF SDK Client
1828
*
@@ -104,13 +114,17 @@ export class NanoTDFClient extends Client {
104114
}
105115

106116
/**
107-
* Encrypt data
117+
* Encrypts the given data using the NanoTDF encryption scheme.
108118
*
109-
* Pass a string, TypedArray, or ArrayBuffer data and get a promise which resolves ciphertext
110-
*
111-
* @param data to decrypt
119+
* @param {string | TypedArray | ArrayBuffer} data - The data to be encrypted.
120+
* @param {EncryptOptions} [options=defaultOptions] - The encryption options (currently unused).
121+
* @returns {Promise<ArrayBuffer>} A promise that resolves to the encrypted data as an ArrayBuffer.
122+
* @throws {Error} If the initialization vector is not a number.
112123
*/
113-
async encrypt(data: string | TypedArray | ArrayBuffer): Promise<ArrayBuffer> {
124+
async encrypt(
125+
data: string | TypedArray | ArrayBuffer,
126+
options?: EncryptOptions
127+
): Promise<ArrayBuffer> {
114128
// For encrypt always generate the client ephemeralKeyPair
115129
const ephemeralKeyPair = await this.ephemeralKeyPair;
116130
const initializationVector = this.iv;
@@ -155,7 +169,15 @@ export class NanoTDFClient extends Client {
155169
payloadIV[10] = lengthAsUint24[1];
156170
payloadIV[11] = lengthAsUint24[0];
157171

158-
return encrypt(policyObjectAsStr, this.kasPubKey, ephemeralKeyPair, payloadIV, data);
172+
const mergedOptions: EncryptOptions = { ...defaultOptions, ...options };
173+
return encrypt(
174+
policyObjectAsStr,
175+
this.kasPubKey,
176+
ephemeralKeyPair,
177+
payloadIV,
178+
data,
179+
mergedOptions.ecdsaBinding
180+
);
159181
}
160182
}
161183

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { AlgorithmName } from './../nanotdf-crypto/enums.js';
2+
3+
/**
4+
* Computes an ECDSA signature for the given data using the provided private key.
5+
*
6+
* This function uses the Web Crypto API to generate a digital signature
7+
* for the input data using the ECDSA algorithm with SHA-256 as the hash function.
8+
*
9+
* @param {CryptoKey} privateKey - The ECDSA private key used for signing.
10+
* @param {Uint8Array} data - The data to be signed.
11+
* @returns {Promise<ArrayBuffer>} - A promise that resolves to the generated signature.
12+
*/
13+
export async function computeECDSASig(
14+
privateKey: CryptoKey,
15+
data: Uint8Array
16+
): Promise<ArrayBuffer> {
17+
const signature = await crypto.subtle.sign(
18+
{
19+
name: AlgorithmName.ECDSA,
20+
hash: { name: 'SHA-256' },
21+
},
22+
privateKey,
23+
data
24+
);
25+
return signature;
26+
}
27+
28+
/**
29+
* Verifies an ECDSA signature using the provided public key and data.
30+
*
31+
* This function uses the Web Crypto API to verify the digital signature
32+
* for the input data using the ECDSA algorithm with SHA-256 as the hash function.
33+
*
34+
* @param {CryptoKey} publicKey - The ECDSA public key used for verification.
35+
* @param {Uint8Array} signature - The signature to be verified.
36+
* @param {Uint8Array} data - The data that was signed.
37+
* @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether the signature is valid.
38+
*/
39+
export async function verifyECDSASignature(
40+
publicKey: CryptoKey,
41+
signature: Uint8Array,
42+
data: Uint8Array
43+
): Promise<boolean> {
44+
const isValid = await crypto.subtle.verify(
45+
{
46+
name: AlgorithmName.ECDSA,
47+
hash: { name: 'SHA-256' },
48+
},
49+
publicKey,
50+
signature,
51+
data
52+
);
53+
return isValid;
54+
}
55+
56+
/**
57+
* Extracts the r and s values from a given ECDSA signature.
58+
*
59+
* @param {Uint8Array} signatureBytes - The raw ECDSA signature bytes.
60+
* @returns {{ r: Uint8Array; s: Uint8Array }} An object containing the r and s values as Uint8Arrays.
61+
* @throws {Error} If the validation of the signature fails.
62+
*/
63+
export function extractRSValuesFromSignature(signatureBytes: Uint8Array): {
64+
r: Uint8Array;
65+
s: Uint8Array;
66+
} {
67+
// Split the raw signature into r and s values
68+
const halfLength = Math.floor(signatureBytes.length / 2);
69+
const rValue = signatureBytes.slice(0, halfLength);
70+
const sValue = signatureBytes.slice(halfLength);
71+
72+
// Correct validation
73+
if (!concatAndCompareUint8Arrays(rValue, sValue, signatureBytes)) {
74+
throw new Error('Invalid ECDSA signature');
75+
}
76+
77+
return {
78+
r: rValue,
79+
s: sValue,
80+
};
81+
}
82+
83+
function concatAndCompareUint8Arrays(
84+
arr1: Uint8Array,
85+
arr2: Uint8Array,
86+
arr3: Uint8Array
87+
): boolean {
88+
// Create a new Uint8Array with the combined length of arr1 and arr2
89+
const concatenated = new Uint8Array(arr1.length + arr2.length);
90+
91+
// Copy arr1 and arr2 into the new array
92+
concatenated.set(arr1, 0);
93+
concatenated.set(arr2, arr1.length);
94+
95+
// Check if the lengths are the same
96+
if (concatenated.length !== arr3.length) {
97+
return false;
98+
}
99+
100+
// Compare each element
101+
for (let i = 0; i < concatenated.length; i++) {
102+
if (concatenated[i] !== arr3[i]) {
103+
return false;
104+
}
105+
}
106+
107+
return true;
108+
}

lib/src/nanotdf/encrypt.ts

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ 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 { lengthOfBinding } from './helpers/calculateByCipher.js';
109
import { TypedArray } from '../tdf/index.js';
10+
import { GMAC_BINDING_LEN } from './constants.js';
11+
import { AlgorithmName, KeyFormat, KeyUsageType } from './../nanotdf-crypto/enums.js';
1112

1213
import {
1314
encrypt as cryptoEncrypt,
@@ -16,6 +17,7 @@ import {
1617
exportCryptoKey,
1718
} from '../nanotdf-crypto/index.js';
1819
import { KasPublicKeyInfo } from '../access.js';
20+
import { computeECDSASig, extractRSValuesFromSignature } from '../nanotdf-crypto/ecdsaSignature.js';
1921

2022
/**
2123
* Encrypt the plain data into nanotdf buffer
@@ -25,13 +27,15 @@ import { KasPublicKeyInfo } from '../access.js';
2527
* @param ephemeralKeyPair SDK ephemeral key pair to generate symmetric key
2628
* @param iv
2729
* @param data The data to be encrypted
30+
* @param ecdsaBinding Flag to enable ECDSA binding
2831
*/
2932
export default async function encrypt(
3033
policy: string,
3134
kasInfo: KasPublicKeyInfo,
3235
ephemeralKeyPair: CryptoKeyPair,
3336
iv: Uint8Array,
34-
data: string | TypedArray | ArrayBuffer
37+
data: string | TypedArray | ArrayBuffer,
38+
ecdsaBinding: boolean = DefaultParams.ecdsaBinding
3539
): Promise<ArrayBuffer> {
3640
// Generate a symmetric key.
3741
if (!ephemeralKeyPair.privateKey) {
@@ -60,33 +64,34 @@ export default async function encrypt(
6064
authTagLengthInBytes * 8
6165
);
6266

63-
// Enable - once ecdsaBinding is true
64-
// if (!DefaultParams.ecdsaBinding) {
65-
// throw new Error("ECDSA binding should enable by default.");
66-
// }
67-
68-
// // Calculate the policy binding.
69-
// const policyBinding = await calculateSignature(this.ephemeralKeyPair.privateKey, new Uint8Array(encryptedPolicy));
70-
// console.log("Length of the policyBinding " + policyBinding.byteLength);
71-
72-
// // Create embedded policy
73-
// const embeddedPolicy = new EmbeddedPolicy(DefaultParams.policyType,
74-
// new Uint8Array(policyBinding),
75-
// new Uint8Array(encryptedPolicy)
76-
// );
67+
let policyBinding: Uint8Array;
7768

7869
// Calculate the policy binding.
79-
const lengthOfPolicyBinding = lengthOfBinding(
80-
DefaultParams.ecdsaBinding,
81-
DefaultParams.ephemeralCurveName
82-
);
83-
84-
const policyBinding = await digest('SHA-256', new Uint8Array(encryptedPolicy));
70+
if (ecdsaBinding) {
71+
const curveName = await getCurveNameFromPrivateKey(ephemeralKeyPair.privateKey);
72+
const ecdsaPrivateKey = await convertECDHToECDSA(ephemeralKeyPair.privateKey, curveName);
73+
const ecdsaSignature = await computeECDSASig(ecdsaPrivateKey, new Uint8Array(encryptedPolicy));
74+
const { r, s } = extractRSValuesFromSignature(new Uint8Array(ecdsaSignature));
75+
76+
const rLength = r.length;
77+
const sLength = s.length;
78+
79+
policyBinding = new Uint8Array(1 + rLength + 1 + sLength);
80+
81+
// Set the lengths and values of r and s in policyBinding
82+
policyBinding[0] = rLength;
83+
policyBinding.set(r, 1);
84+
policyBinding[1 + rLength] = sLength;
85+
policyBinding.set(s, 1 + rLength + 1);
86+
} else {
87+
const signature = await digest('SHA-256', new Uint8Array(encryptedPolicy));
88+
policyBinding = new Uint8Array(signature.slice(-GMAC_BINDING_LEN));
89+
}
8590

8691
// Create embedded policy
8792
const embeddedPolicy = new EmbeddedPolicy(
8893
DefaultParams.policyType,
89-
new Uint8Array(policyBinding.slice(-lengthOfPolicyBinding)),
94+
policyBinding,
9095
new Uint8Array(encryptedPolicy)
9196
);
9297

@@ -99,7 +104,7 @@ export default async function encrypt(
99104
const header = new Header(
100105
DefaultParams.magicNumberVersion,
101106
kasResourceLocator,
102-
DefaultParams.ecdsaBinding,
107+
ecdsaBinding,
103108
DefaultParams.signatureCurveName,
104109
DefaultParams.signature,
105110
DefaultParams.signatureCurveName,
@@ -134,3 +139,58 @@ export default async function encrypt(
134139
const nanoTDF = new NanoTDF(header, payload);
135140
return nanoTDF.toBuffer();
136141
}
142+
143+
/**
144+
* Retrieves the curve name from a given ECDH private key.
145+
*
146+
* This function exports the provided ECDH private key in JWK format and extracts
147+
* the curve name from the 'crv' property of the JWK.
148+
*
149+
* @param {CryptoKey} privateKey - The ECDH private key from which to retrieve the curve name.
150+
* @returns {Promise<string>} - A promise that resolves to the curve name.
151+
*
152+
* @throws {Error} - Throws an error if the curve name is undefined.
153+
*
154+
*/
155+
async function getCurveNameFromPrivateKey(privateKey: CryptoKey): Promise<string> {
156+
// Export the private key
157+
const keyData = await crypto.subtle.exportKey('jwk', privateKey);
158+
159+
// The curve name is stored in the 'crv' property of the JWK
160+
if (!keyData.crv) {
161+
throw new Error('Curve name is undefined');
162+
}
163+
164+
return keyData.crv;
165+
}
166+
167+
/**
168+
* Converts an ECDH private key to an ECDSA private key.
169+
*
170+
* This function exports the given ECDH private key in PKCS#8 format and then
171+
* imports it as an ECDSA private key using the specified curve name.
172+
*
173+
* @param {CryptoKey} key - The ECDH private key to be converted.
174+
* @param {string} curveName - The name of the elliptic curve to be used for the ECDSA key.
175+
* @returns {Promise<CryptoKey>} - A promise that resolves to the converted ECDSA private key.
176+
*
177+
* @throws {Error} - Throws an error if the key export or import fails.
178+
*/
179+
async function convertECDHToECDSA(key: CryptoKey, curveName: string): Promise<CryptoKey> {
180+
// Export the ECDH private key
181+
const ecdhPrivateKey = await crypto.subtle.exportKey('pkcs8', key);
182+
183+
// Import the ECDH private key as an ECDSA private key
184+
const ecdsaPrivateKey = await crypto.subtle.importKey(
185+
KeyFormat.Pkcs8,
186+
ecdhPrivateKey,
187+
{
188+
name: AlgorithmName.ECDSA,
189+
namedCurve: curveName,
190+
},
191+
true,
192+
[KeyUsageType.Sign]
193+
);
194+
195+
return ecdsaPrivateKey;
196+
}

lib/src/nanotdf/models/DefaultParams.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const enc = new TextEncoder();
1010
* @link https://github.com/virtru/tdf3-cpp/blob/develop/tdf3-src/lib/src/nanotdf_builder_impl.h
1111
*/
1212
const DefaultParams = {
13-
// Enabling ECDSA is not currently supported. Conflict with reusing key for `verify/sign` and `encrypt/decrypt`
1413
ecdsaBinding: false,
1514
ephemeralCurveName: CurveNameEnum.SECP256R1,
1615
magicNumberVersion: enc.encode('L1L'),

0 commit comments

Comments
 (0)