generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 57
/
concat-kdf.ts
231 lines (212 loc) · 8.87 KB
/
concat-kdf.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
import { sha256 } from '@noble/hashes/sha256';
import { Convert, universalTypeOf } from '@web5/common';
import { TypedArray, concatBytes } from '@noble/hashes/utils';
export type ConcatKdfOtherInfo = {
/**
* The algorithm the derived secret keying material will be used with.
*/
algorithmId: string;
/**
* Information related to party U (initiator) involved in the key agreement
* transaction. It could be a public key, identifier, or any other data.
*/
partyUInfo: string | TypedArray;
/**
* Information related to party V (receiver) involved in the key
* agreement transaction. Similar to partyUInfo, it could be a
* public key, identifier, etc.
*/
partyVInfo: string | TypedArray;
/**
* Optional field. It is usually used to ensure the uniqueness of the
* derived keying material when the input keying material is used in
* multiple key-derivation key-agreement transactions. It is usually
* a public value such as the keyDataLen.
*/
suppPubInfo?: number;
/**
* Optional field. It is used when it is desired to secretively
* bind additional information into the derived keying material.
* It is a secret value agreed upon by the entities who are party
* to the key agreement.
*/
suppPrivInfo?: string | TypedArray;
}
/**
* An implementation of the Concatenation Key Derivation Function (ConcatKDF)
* as specified in NIST.800-56A, a single-step key-derivation function (SSKDF).
* ConcatKDF produces a derived key from a secret key (like a shared secret
* from ECDH), and other optional public information. This implementation
* specifically uses SHA-256 as the pseudorandom function (PRF).
*
* Note: This implementation allows for only a single round / repetition using the function
* `K(1) = H(counter || Z || OtherInfo)`, where:
* - `K(1)` is the derived key material after one round
* - `H` is the SHA-256 hashing function
* - `counter` is a 32-bit, big-endian bit string counter set to 0x00000001
* - `Z` is the shared secret value obtained from a key agreement protocol
* - `OtherInfo` is a bit string used to ensure that the derived keying material is adequately
* "bound" to the key-agreement transaction.
*
* @example
* ```ts
* // Key Derivation
* const derivedKeyingMaterial = await ConcatKdf.deriveKey({
* sharedSecret: utils.randomBytes(32),
* keyDataLen: 128,
* otherInfo: {
* algorithmId: "A128GCM",
* partyUInfo: "Alice",
* partyVInfo: "Bob",
* suppPubInfo: 128,
* },
* });
* ```
*
* Additional Information:
*
* `Z`, or "shared secret":
* The shared secret value obtained from a key agreement protocol, such as
* Diffie-Hellman, ECDH (Elliptic Curve Diffie-Hellman). Importantly, this
* shared secret is not directly used as the encryption or authentication
* key, but as an input to a key derivation function (KDF), such as Concat
* KDF, to generate the actual key. This adds an extra layer of security, as
* even if the shared secret gets compromised, the actual encryption or
* authentication key stays safe. This shared secret `Z` value is kept
* confidential between the two parties in the key agreement protocol.
*
* @see {@link https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Ar3.pdf | NIST.800-56A}
* @see {@link https://datatracker.ietf.org/doc/html/rfc7518#section-4.6.2 | RFC 7518, Section 4.6.2}
*/
export class ConcatKdf {
/**
* Derives a key of a specified length from the input parameters.
*
* @example
* ```ts
* // Key Derivation
* const derivedKeyingMaterial = await ConcatKdf.deriveKey({
* sharedSecret: utils.randomBytes(32),
* keyDataLen: 128,
* otherInfo: {
* algorithmId: "A128GCM",
* partyUInfo: "Alice",
* partyVInfo: "Bob",
* suppPubInfo: 128,
* },
* });
* ```
*
* @param params - Input parameters for key derivation.
* @param params.keyDataLen - The desired length of the derived key in bits.
* @param params.sharedSecret - The shared secret key to derive from.
* @param params.otherInfo - Additional public information to use in key derivation.
* @returns The derived key as a Uint8Array.
*
* @throws {Error} If the `keyDataLen` would require multiple rounds.
*/
public static async deriveKey({ keyDataLen, otherInfo, sharedSecret }: {
keyDataLen: number;
otherInfo: ConcatKdfOtherInfo;
sharedSecret: Uint8Array;
}): Promise<Uint8Array> {
// RFC 7518 Section 4.6.2 specifies using SHA-256 for ECDH key agreement:
// "Key derivation is performed using the Concat KDF, as defined in
// Section 5.8.1 of [NIST.800-56A], where the Digest Method is SHA-256."
// Reference: https://tools.ietf.org/html/rfc7518#section-4.6.2
const hashLen = 256;
// This implementation only supports single round Concat KDF.
const roundCount = Math.ceil(keyDataLen / hashLen);
if (roundCount !== 1) {
throw new Error(`Concat KDF with ${roundCount} rounds not supported.`);
}
// Initialize a 32-bit, big-endian bit string counter as 0x00000001.
const counter = new Uint8Array(4);
new DataView(counter.buffer).setUint32(0, roundCount);
// Compute the OtherInfo bit-string.
const otherInfoBytes = ConcatKdf.computeOtherInfo(otherInfo);
// Compute K(i) = H(counter || Z || OtherInfo)
// return concatBytes(counter, sharedSecretZ, otherInfo);
const derivedKeyingMaterial = sha256(concatBytes(counter, sharedSecret, otherInfoBytes));
// Return the bit string of derived keying material of length keyDataLen bits.
return derivedKeyingMaterial.slice(0, keyDataLen / 8);
}
/**
* Computes the `OtherInfo` parameter for Concat KDF, which binds the derived key material to the
* context of the key agreement transaction.
*
* @remarks
* This implementation follows the recommended format for `OtherInfo` specified in section
* 5.8.1.2.1 of the NIST.800-56A publication.
*
* `OtherInfo` is a bit string equal to the following concatenation:
* `AlgorithmID || PartyUInfo || PartyVInfo {|| SuppPubInfo }{|| SuppPrivInfo }`.
*
* `SuppPubInfo` is the key length in bits, big endian encoded as a 32-bit number. For example,
* 128 would be [0, 0, 0, 128] and 256 would be [0, 0, 1, 0].
*
* @param params - Input data to construct OtherInfo.
* @returns OtherInfo as a Uint8Array.
*/
private static computeOtherInfo(params:
ConcatKdfOtherInfo
): Uint8Array {
// Required sub-fields.
const algorithmId = ConcatKdf.toDataLenData({ data: params.algorithmId });
const partyUInfo = ConcatKdf.toDataLenData({ data: params.partyUInfo });
const partyVInfo = ConcatKdf.toDataLenData({ data: params.partyVInfo });
// Optional sub-fields.
const suppPubInfo = ConcatKdf.toDataLenData({ data: params.suppPubInfo, variableLength: false });
const suppPrivInfo = ConcatKdf.toDataLenData({ data: params.suppPrivInfo });
// Concatenate AlgorithmID || PartyUInfo || PartyVInfo || SuppPubInfo || SuppPrivInfo.
const otherInfo = concatBytes(algorithmId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo);
return otherInfo;
}
/**
* Encodes input data as a length-prefixed byte string, or
* as a fixed-length bit string if specified.
*
* If variableLength = true, return the data in the form Datalen || Data,
* where Data is a variable-length string of zero or more (eight-bit)
* bytes, and Datalen is a fixed-length, big-endian counter that
* indicates the length (in bytes) of Data.
*
* If variableLength = false, return the data formatted as a
* fixed-length bit string.
*
* @param params - Input data and options for the conversion.
* @param params.data - The input data to encode. Must be a type convertible to Uint8Array by the Convert class.
* @param params.variableLength - Whether to output the data as variable length. Default is true.
*
* @returns The input data encoded as a Uint8Array.
*
* @throws {TypeError} If fixed-length data is not a number.
*/
private static toDataLenData({ data, variableLength = true }: {
data: unknown;
variableLength?: boolean;
}): Uint8Array {
let encodedData: Uint8Array;
const dataType = universalTypeOf(data);
// Return an emtpy octet sequence if data is not specified.
if (dataType === 'Undefined') {
return new Uint8Array(0);
}
if (variableLength) {
const dataU8A = (dataType === 'Uint8Array')
? data as Uint8Array
: new Convert(data, dataType).toUint8Array();
const bufferLength = dataU8A.length;
encodedData = new Uint8Array(4 + bufferLength);
new DataView(encodedData.buffer).setUint32(0, bufferLength);
encodedData.set(dataU8A, 4);
} else {
if (typeof data !== 'number') {
throw TypeError('Fixed length input must be a number.');
}
encodedData = new Uint8Array(4);
new DataView(encodedData.buffer).setUint32(0, data);
}
return encodedData;
}
}