@bsv/sdk 1.9.19 → 1.9.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/auth/certificates/MasterCertificate.js +9 -2
- package/dist/cjs/src/auth/certificates/MasterCertificate.js.map +1 -1
- package/dist/cjs/src/primitives/DRBG.js +12 -1
- package/dist/cjs/src/primitives/DRBG.js.map +1 -1
- package/dist/cjs/src/primitives/Hash.js +6 -5
- package/dist/cjs/src/primitives/Hash.js.map +1 -1
- package/dist/cjs/src/primitives/hex.js +33 -0
- package/dist/cjs/src/primitives/hex.js.map +1 -0
- package/dist/cjs/src/primitives/index.js +1 -3
- package/dist/cjs/src/primitives/index.js.map +1 -1
- package/dist/cjs/src/primitives/utils.js +69 -59
- package/dist/cjs/src/primitives/utils.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/certificates/MasterCertificate.js +9 -2
- package/dist/esm/src/auth/certificates/MasterCertificate.js.map +1 -1
- package/dist/esm/src/primitives/DRBG.js +12 -1
- package/dist/esm/src/primitives/DRBG.js.map +1 -1
- package/dist/esm/src/primitives/Hash.js +6 -5
- package/dist/esm/src/primitives/Hash.js.map +1 -1
- package/dist/esm/src/primitives/hex.js +29 -0
- package/dist/esm/src/primitives/hex.js.map +1 -0
- package/dist/esm/src/primitives/index.js +0 -1
- package/dist/esm/src/primitives/index.js.map +1 -1
- package/dist/esm/src/primitives/utils.js +69 -59
- package/dist/esm/src/primitives/utils.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/certificates/MasterCertificate.d.ts.map +1 -1
- package/dist/types/src/primitives/DRBG.d.ts +12 -1
- package/dist/types/src/primitives/DRBG.d.ts.map +1 -1
- package/dist/types/src/primitives/Hash.d.ts.map +1 -1
- package/dist/types/src/primitives/hex.d.ts +3 -0
- package/dist/types/src/primitives/hex.d.ts.map +1 -0
- package/dist/types/src/primitives/index.d.ts +0 -1
- package/dist/types/src/primitives/index.d.ts.map +1 -1
- package/dist/types/src/primitives/utils.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/auth.md +2 -2
- package/docs/reference/primitives.md +90 -31
- package/package.json +1 -1
- package/src/auth/__tests/Peer.test.ts +2 -1
- package/src/auth/certificates/MasterCertificate.ts +9 -2
- package/src/auth/certificates/__tests/MasterCertificate.test.ts +46 -9
- package/src/primitives/DRBG.ts +12 -1
- package/src/primitives/Hash.ts +9 -6
- package/src/primitives/__tests/HMAC.test.ts +13 -2
- package/src/primitives/__tests/Hash.test.ts +24 -0
- package/src/primitives/__tests/hex.test.ts +57 -0
- package/src/primitives/__tests/utils.test.ts +39 -0
- package/src/primitives/hex.ts +35 -0
- package/src/primitives/index.ts +0 -1
- package/src/primitives/utils.ts +71 -65
- package/src/script/__tests/Script.test.ts +1 -1
package/docs/reference/auth.md
CHANGED
|
@@ -502,7 +502,7 @@ export class MasterCertificate extends Certificate {
|
|
|
502
502
|
static async createKeyringForVerifier(subjectWallet: ProtoWallet, certifier: WalletCounterparty, verifier: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, fieldsToReveal: string[], masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, serialNumber: Base64String, privileged?: boolean, privilegedReason?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
|
|
503
503
|
static async issueCertificateForSubject(certifierWallet: ProtoWallet, subject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>, certificateType: string, getRevocationOutpoint = async (_serial: string): Promise<string> => {
|
|
504
504
|
void _serial;
|
|
505
|
-
return "
|
|
505
|
+
return "00".repeat(32);
|
|
506
506
|
}, serialNumber?: string): Promise<MasterCertificate>
|
|
507
507
|
static async decryptFields(subjectOrCertifierWallet: ProtoWallet, masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, fields: Record<CertificateFieldNameUnder50Bytes, Base64String>, counterparty: WalletCounterparty, privileged?: boolean, privilegedReason?: string): Promise<Record<CertificateFieldNameUnder50Bytes, string>>
|
|
508
508
|
static async decryptField(subjectOrCertifierWallet: ProtoWallet, masterKeyring: Record<CertificateFieldNameUnder50Bytes, Base64String>, fieldName: Base64String, fieldValue: Base64String, counterparty: WalletCounterparty, privileged?: boolean, privilegedReason?: string): Promise<{
|
|
@@ -634,7 +634,7 @@ can also includes a revocation outpoint to manage potential revocation.
|
|
|
634
634
|
```ts
|
|
635
635
|
static async issueCertificateForSubject(certifierWallet: ProtoWallet, subject: WalletCounterparty, fields: Record<CertificateFieldNameUnder50Bytes, string>, certificateType: string, getRevocationOutpoint = async (_serial: string): Promise<string> => {
|
|
636
636
|
void _serial;
|
|
637
|
-
return "
|
|
637
|
+
return "00".repeat(32);
|
|
638
638
|
}, serialNumber?: string): Promise<MasterCertificate>
|
|
639
639
|
```
|
|
640
640
|
See also: [CertificateFieldNameUnder50Bytes](./wallet.md#type-certificatefieldnameunder50bytes), [MasterCertificate](./auth.md#class-mastercertificate), [ProtoWallet](./wallet.md#class-protowallet), [WalletCounterparty](./wallet.md#type-walletcounterparty)
|
|
@@ -829,7 +829,17 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
829
829
|
---
|
|
830
830
|
### Class: DRBG
|
|
831
831
|
|
|
832
|
-
|
|
832
|
+
HMAC-DRBG used **only** for deterministic ECDSA nonce generation.
|
|
833
|
+
|
|
834
|
+
This implementation follows the RFC 6979-style HMAC-DRBG construction for secp256k1
|
|
835
|
+
and is wired internally into the ECDSA signing code. It is **not forward-secure**
|
|
836
|
+
and MUST NOT be used as a general-purpose DRBG, key generator, or randomness source.
|
|
837
|
+
|
|
838
|
+
Security note:
|
|
839
|
+
- Intended scope: internal ECDSA nonce generation with fixed-size inputs.
|
|
840
|
+
- Out-of-scope: generic randomness, long-lived session keys, or any context
|
|
841
|
+
where forward secrecy is required.
|
|
842
|
+
- API stability: this class is internal.
|
|
833
843
|
|
|
834
844
|
Example
|
|
835
845
|
|
|
@@ -4953,8 +4963,10 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
4953
4963
|
| [AES](#function-aes) |
|
|
4954
4964
|
| [AESGCM](#function-aesgcm) |
|
|
4955
4965
|
| [AESGCMDecrypt](#function-aesgcmdecrypt) |
|
|
4966
|
+
| [assertValidHex](#function-assertvalidhex) |
|
|
4956
4967
|
| [base64ToArray](#function-base64toarray) |
|
|
4957
4968
|
| [ghash](#function-ghash) |
|
|
4969
|
+
| [normalizeHex](#function-normalizehex) |
|
|
4958
4970
|
| [pbkdf2](#function-pbkdf2) |
|
|
4959
4971
|
| [red](#function-red) |
|
|
4960
4972
|
| [toArray](#function-toarray) |
|
|
@@ -4994,6 +5006,15 @@ export function AESGCMDecrypt(cipherText: number[], additionalAuthenticatedData:
|
|
|
4994
5006
|
|
|
4995
5007
|
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
4996
5008
|
|
|
5009
|
+
---
|
|
5010
|
+
### Function: assertValidHex
|
|
5011
|
+
|
|
5012
|
+
```ts
|
|
5013
|
+
export function assertValidHex(msg: string): void
|
|
5014
|
+
```
|
|
5015
|
+
|
|
5016
|
+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
5017
|
+
|
|
4997
5018
|
---
|
|
4998
5019
|
### Function: base64ToArray
|
|
4999
5020
|
|
|
@@ -5012,6 +5033,15 @@ export function ghash(input: number[], hashSubKey: number[]): number[]
|
|
|
5012
5033
|
|
|
5013
5034
|
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
5014
5035
|
|
|
5036
|
+
---
|
|
5037
|
+
### Function: normalizeHex
|
|
5038
|
+
|
|
5039
|
+
```ts
|
|
5040
|
+
export function normalizeHex(msg: string): string
|
|
5041
|
+
```
|
|
5042
|
+
|
|
5043
|
+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
5044
|
+
|
|
5015
5045
|
---
|
|
5016
5046
|
### Function: pbkdf2
|
|
5017
5047
|
|
|
@@ -5939,7 +5969,7 @@ sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: Bi
|
|
|
5939
5969
|
if (kBN == null)
|
|
5940
5970
|
throw new Error("k is undefined");
|
|
5941
5971
|
kBN = truncateToN(kBN, true);
|
|
5942
|
-
if (kBN.cmpn(1)
|
|
5972
|
+
if (kBN.cmpn(1) < 0 || kBN.cmp(ns1) > 0) {
|
|
5943
5973
|
if (BigNumber.isBN(customK)) {
|
|
5944
5974
|
throw new Error("Invalid fixed custom K value (must be >1 and <N\u20111)");
|
|
5945
5975
|
}
|
|
@@ -6090,49 +6120,78 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
6090
6120
|
```ts
|
|
6091
6121
|
toUTF8 = (arr: number[]): string => {
|
|
6092
6122
|
let result = "";
|
|
6093
|
-
|
|
6123
|
+
const replacementChar = "\uFFFD";
|
|
6094
6124
|
for (let i = 0; i < arr.length; i++) {
|
|
6095
|
-
const
|
|
6096
|
-
if (
|
|
6097
|
-
|
|
6125
|
+
const byte1 = arr[i];
|
|
6126
|
+
if (byte1 <= 127) {
|
|
6127
|
+
result += String.fromCharCode(byte1);
|
|
6098
6128
|
continue;
|
|
6099
6129
|
}
|
|
6100
|
-
|
|
6101
|
-
result +=
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
const
|
|
6130
|
+
const emitReplacement = (): void => {
|
|
6131
|
+
result += replacementChar;
|
|
6132
|
+
};
|
|
6133
|
+
if (byte1 >= 192 && byte1 <= 223) {
|
|
6134
|
+
if (i + 1 >= arr.length) {
|
|
6135
|
+
emitReplacement();
|
|
6136
|
+
continue;
|
|
6137
|
+
}
|
|
6138
|
+
const byte2 = arr[i + 1];
|
|
6139
|
+
if ((byte2 & 192) !== 128) {
|
|
6140
|
+
emitReplacement();
|
|
6141
|
+
i += 1;
|
|
6142
|
+
continue;
|
|
6143
|
+
}
|
|
6144
|
+
const codePoint = ((byte1 & 31) << 6) | (byte2 & 63);
|
|
6109
6145
|
result += String.fromCharCode(codePoint);
|
|
6146
|
+
i += 1;
|
|
6110
6147
|
continue;
|
|
6111
6148
|
}
|
|
6112
|
-
if (
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
const
|
|
6149
|
+
if (byte1 >= 224 && byte1 <= 239) {
|
|
6150
|
+
if (i + 2 >= arr.length) {
|
|
6151
|
+
emitReplacement();
|
|
6152
|
+
continue;
|
|
6153
|
+
}
|
|
6154
|
+
const byte2 = arr[i + 1];
|
|
6155
|
+
const byte3 = arr[i + 2];
|
|
6156
|
+
if ((byte2 & 192) !== 128 || (byte3 & 192) !== 128) {
|
|
6157
|
+
emitReplacement();
|
|
6158
|
+
i += 2;
|
|
6159
|
+
continue;
|
|
6160
|
+
}
|
|
6161
|
+
const codePoint = ((byte1 & 15) << 12) |
|
|
6162
|
+
((byte2 & 63) << 6) |
|
|
6163
|
+
(byte3 & 63);
|
|
6118
6164
|
result += String.fromCharCode(codePoint);
|
|
6165
|
+
i += 2;
|
|
6119
6166
|
continue;
|
|
6120
6167
|
}
|
|
6121
|
-
if (
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
const
|
|
6168
|
+
if (byte1 >= 240 && byte1 <= 247) {
|
|
6169
|
+
if (i + 3 >= arr.length) {
|
|
6170
|
+
emitReplacement();
|
|
6171
|
+
continue;
|
|
6172
|
+
}
|
|
6173
|
+
const byte2 = arr[i + 1];
|
|
6174
|
+
const byte3 = arr[i + 2];
|
|
6175
|
+
const byte4 = arr[i + 3];
|
|
6176
|
+
if ((byte2 & 192) !== 128 ||
|
|
6177
|
+
(byte3 & 192) !== 128 ||
|
|
6178
|
+
(byte4 & 192) !== 128) {
|
|
6179
|
+
emitReplacement();
|
|
6180
|
+
i += 3;
|
|
6181
|
+
continue;
|
|
6182
|
+
}
|
|
6183
|
+
const codePoint = ((byte1 & 7) << 18) |
|
|
6128
6184
|
((byte2 & 63) << 12) |
|
|
6129
6185
|
((byte3 & 63) << 6) |
|
|
6130
6186
|
(byte4 & 63);
|
|
6131
|
-
const
|
|
6132
|
-
const
|
|
6133
|
-
|
|
6187
|
+
const offset = codePoint - 65536;
|
|
6188
|
+
const highSurrogate = 55296 + (offset >> 10);
|
|
6189
|
+
const lowSurrogate = 56320 + (offset & 1023);
|
|
6190
|
+
result += String.fromCharCode(highSurrogate, lowSurrogate);
|
|
6191
|
+
i += 3;
|
|
6134
6192
|
continue;
|
|
6135
6193
|
}
|
|
6194
|
+
emitReplacement();
|
|
6136
6195
|
}
|
|
6137
6196
|
return result;
|
|
6138
6197
|
}
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ import { SimplifiedFetchTransport } from '../../auth/transports/SimplifiedFetchT
|
|
|
12
12
|
const certifierPrivKey = new PrivateKey(21)
|
|
13
13
|
const alicePrivKey = new PrivateKey(22)
|
|
14
14
|
const bobPrivKey = new PrivateKey(23)
|
|
15
|
+
const DUMMY_REVOCATION_OUTPOINT_HEX = '00'.repeat(36)
|
|
15
16
|
|
|
16
17
|
jest.mock('../../auth/utils/getVerifiableCertificates')
|
|
17
18
|
|
|
@@ -101,7 +102,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
|
|
|
101
102
|
subjectPubKey,
|
|
102
103
|
fields,
|
|
103
104
|
certificateType,
|
|
104
|
-
async () =>
|
|
105
|
+
async () => DUMMY_REVOCATION_OUTPOINT_HEX
|
|
105
106
|
)
|
|
106
107
|
|
|
107
108
|
// For test consistency, you could override the auto-generated serialNumber:
|
|
@@ -232,7 +232,7 @@ export class MasterCertificate extends Certificate {
|
|
|
232
232
|
certificateType: string,
|
|
233
233
|
getRevocationOutpoint = async (_serial: string): Promise<string> => {
|
|
234
234
|
void _serial // Explicitly acknowledge unused parameter
|
|
235
|
-
return '
|
|
235
|
+
return '00'.repeat(32)
|
|
236
236
|
},
|
|
237
237
|
serialNumber?: string
|
|
238
238
|
): Promise<MasterCertificate> {
|
|
@@ -246,11 +246,18 @@ export class MasterCertificate extends Certificate {
|
|
|
246
246
|
// 3. Obtain a revocation outpoint
|
|
247
247
|
const revocationOutpoint = await getRevocationOutpoint(finalSerialNumber)
|
|
248
248
|
|
|
249
|
+
let subjectIdentityKey: string
|
|
250
|
+
if (subject === 'self') {
|
|
251
|
+
subjectIdentityKey = (await certifierWallet.getPublicKey({ identityKey: true })).publicKey
|
|
252
|
+
} else {
|
|
253
|
+
subjectIdentityKey = subject
|
|
254
|
+
}
|
|
255
|
+
|
|
249
256
|
// 4. Create new MasterCertificate instance
|
|
250
257
|
const certificate = new MasterCertificate(
|
|
251
258
|
certificateType,
|
|
252
259
|
finalSerialNumber,
|
|
253
|
-
|
|
260
|
+
subjectIdentityKey,
|
|
254
261
|
(await certifierWallet.getPublicKey({ identityKey: true })).publicKey,
|
|
255
262
|
revocationOutpoint,
|
|
256
263
|
certificateFields,
|
|
@@ -14,7 +14,8 @@ const verifierKey2 = new PrivateKey(81)
|
|
|
14
14
|
|
|
15
15
|
// A mock revocation outpoint for testing
|
|
16
16
|
const mockRevocationOutpoint =
|
|
17
|
-
'
|
|
17
|
+
'deadbeefdeadbeefdeadbeefdeadbeef00000001'
|
|
18
|
+
|
|
18
19
|
|
|
19
20
|
// Arbitrary certificate data (in plaintext)
|
|
20
21
|
const plaintextFields = {
|
|
@@ -355,33 +356,69 @@ describe('MasterCertificate', () => {
|
|
|
355
356
|
expect(newCert.fields[fieldName]).toMatch(/^[A-Za-z0-9+/]+=*$/)
|
|
356
357
|
}
|
|
357
358
|
})
|
|
359
|
+
|
|
358
360
|
it('should allow issuing a self-signed certificate and decrypt it with the same wallet', async () => {
|
|
359
|
-
// In a self-signed scenario, the subject and certifier are the same
|
|
360
361
|
const subjectWallet = new CompletedProtoWallet(subjectKey2)
|
|
361
362
|
|
|
362
|
-
// Some sample fields
|
|
363
363
|
const selfSignedFields = {
|
|
364
364
|
owner: 'Bob',
|
|
365
365
|
organization: 'SelfCo'
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
|
|
368
|
+
const subjectIdentityKey = (
|
|
369
|
+
await subjectWallet.getPublicKey({ identityKey: true })
|
|
370
|
+
).publicKey
|
|
371
|
+
|
|
372
|
+
// Issue the certificate: subject = actual identity key (valid hex)
|
|
369
373
|
const selfSignedCert = await MasterCertificate.issueCertificateForSubject(
|
|
370
|
-
subjectWallet,
|
|
371
|
-
'self',
|
|
374
|
+
subjectWallet, // acts as certifier
|
|
375
|
+
subjectIdentityKey, // <-- was 'self', now real hex
|
|
372
376
|
selfSignedFields,
|
|
373
377
|
'SELF_SIGNED_TEST'
|
|
374
378
|
)
|
|
375
379
|
|
|
376
|
-
//
|
|
380
|
+
// Decrypt with the same wallet
|
|
377
381
|
const decrypted = await MasterCertificate.decryptFields(
|
|
378
382
|
subjectWallet,
|
|
379
383
|
selfSignedCert.masterKeyring,
|
|
380
384
|
selfSignedCert.fields,
|
|
381
|
-
'self'
|
|
385
|
+
'self' // still fine here if decryptFields treats 'self' specially
|
|
382
386
|
)
|
|
383
387
|
|
|
384
388
|
expect(decrypted).toEqual(selfSignedFields)
|
|
385
389
|
})
|
|
390
|
+
|
|
391
|
+
it('resolves subject === "self" to the certifier wallet identity key', async () => {
|
|
392
|
+
const certifierWallet = new CompletedProtoWallet(new PrivateKey(99))
|
|
393
|
+
|
|
394
|
+
const certifierIdentityKey = (
|
|
395
|
+
await certifierWallet.getPublicKey({ identityKey: true })
|
|
396
|
+
).publicKey
|
|
397
|
+
|
|
398
|
+
const cert = await MasterCertificate.issueCertificateForSubject(
|
|
399
|
+
certifierWallet,
|
|
400
|
+
'self',
|
|
401
|
+
{ name: 'Alice' },
|
|
402
|
+
'TEST_CERT'
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
expect(cert.subject).toBe(certifierIdentityKey)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('uses provided subjectIdentityKey when subject is a valid hex string', async () => {
|
|
409
|
+
const certifierWallet = new CompletedProtoWallet(new PrivateKey(42))
|
|
410
|
+
|
|
411
|
+
const validPubkey =
|
|
412
|
+
'0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'
|
|
413
|
+
|
|
414
|
+
const cert = await MasterCertificate.issueCertificateForSubject(
|
|
415
|
+
certifierWallet,
|
|
416
|
+
validPubkey,
|
|
417
|
+
{ name: 'Alice' },
|
|
418
|
+
'TEST_CERT'
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
expect(cert.subject).toBe(validPubkey)
|
|
422
|
+
})
|
|
386
423
|
})
|
|
387
|
-
})
|
|
424
|
+
})
|
package/src/primitives/DRBG.ts
CHANGED
|
@@ -2,7 +2,18 @@ import { SHA256HMAC } from './Hash.js'
|
|
|
2
2
|
import { toHex, toArray } from './utils.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* HMAC-DRBG used **only** for deterministic ECDSA nonce generation.
|
|
6
|
+
*
|
|
7
|
+
* This implementation follows the RFC 6979-style HMAC-DRBG construction for secp256k1
|
|
8
|
+
* and is wired internally into the ECDSA signing code. It is **not forward-secure**
|
|
9
|
+
* and MUST NOT be used as a general-purpose DRBG, key generator, or randomness source.
|
|
10
|
+
*
|
|
11
|
+
* Security note:
|
|
12
|
+
* - Intended scope: internal ECDSA nonce generation with fixed-size inputs.
|
|
13
|
+
* - Out-of-scope: generic randomness, long-lived session keys, or any context
|
|
14
|
+
* where forward secrecy is required.
|
|
15
|
+
* - API stability: this class is internal.
|
|
16
|
+
*
|
|
6
17
|
* @class DRBG
|
|
7
18
|
*
|
|
8
19
|
* @constructor
|
package/src/primitives/Hash.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
4
|
+
import { assertValidHex, normalizeHex } from './hex.js'
|
|
5
|
+
|
|
4
6
|
const assert = (
|
|
5
7
|
expression: unknown,
|
|
6
8
|
message: string = 'Hash assertion failed'
|
|
@@ -169,6 +171,10 @@ abstract class BaseHash {
|
|
|
169
171
|
*/
|
|
170
172
|
private _pad (): number[] {
|
|
171
173
|
const len = this.pendingTotal
|
|
174
|
+
if (!Number.isSafeInteger(len) || len < 0) {
|
|
175
|
+
throw new Error('Message too long for this hash function')
|
|
176
|
+
}
|
|
177
|
+
|
|
172
178
|
const bytes = this._delta8
|
|
173
179
|
const k = bytes - ((len + this.padLength) % bytes)
|
|
174
180
|
const res = new Array(k + this.padLength)
|
|
@@ -177,8 +183,6 @@ abstract class BaseHash {
|
|
|
177
183
|
for (i = 1; i < k; i++) {
|
|
178
184
|
res[i] = 0
|
|
179
185
|
}
|
|
180
|
-
|
|
181
|
-
// Append length
|
|
182
186
|
const lengthBytes = this.padLength
|
|
183
187
|
const maxBits = 1n << BigInt(lengthBytes * 8)
|
|
184
188
|
let totalBits = BigInt(len) * 8n
|
|
@@ -204,6 +208,7 @@ abstract class BaseHash {
|
|
|
204
208
|
totalBits >>= 8n
|
|
205
209
|
}
|
|
206
210
|
}
|
|
211
|
+
|
|
207
212
|
return res
|
|
208
213
|
}
|
|
209
214
|
}
|
|
@@ -262,10 +267,8 @@ export function toArray (
|
|
|
262
267
|
}
|
|
263
268
|
}
|
|
264
269
|
} else {
|
|
265
|
-
msg
|
|
266
|
-
|
|
267
|
-
msg = '0' + msg
|
|
268
|
-
}
|
|
270
|
+
assertValidHex(msg)
|
|
271
|
+
msg = normalizeHex(msg)
|
|
269
272
|
for (let i = 0; i < msg.length; i += 2) {
|
|
270
273
|
res.push(parseInt(msg[i] + msg[i + 1], 16))
|
|
271
274
|
}
|
|
@@ -48,11 +48,22 @@ describe('HMAC', function () {
|
|
|
48
48
|
res: 'cf5ad5984f9e43917aa9087380dac46e410ddc8a7731859c84e9d0f31bd43655'
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
+
function normalizeKey (key: string | number[]): string | number[] {
|
|
52
|
+
if (typeof key === 'string') {
|
|
53
|
+
// test-only helper: remove whitespace between hex groups
|
|
54
|
+
return key.replace(/\s+/g, '')
|
|
55
|
+
}
|
|
56
|
+
return key
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
function test (opt): void {
|
|
52
60
|
it(`should not fail at ${opt.name as string}`, function (): void {
|
|
53
|
-
|
|
61
|
+
const key = normalizeKey(opt.key)
|
|
62
|
+
|
|
63
|
+
let h = new SHA256HMAC(key as any)
|
|
54
64
|
expect(h.update(opt.msg, opt.msgEnc).digestHex()).toEqual(opt.res)
|
|
55
|
-
|
|
65
|
+
|
|
66
|
+
h = new SHA256HMAC(key as any)
|
|
56
67
|
expect(
|
|
57
68
|
h
|
|
58
69
|
.update(opt.msg.slice(0, 10), opt.msgEnc)
|
|
@@ -3,6 +3,7 @@ import * as hash from '../../primitives/Hash'
|
|
|
3
3
|
import * as crypto from 'crypto'
|
|
4
4
|
import PBKDF2Vectors from './PBKDF2.vectors'
|
|
5
5
|
import { toArray, toHex } from '../../primitives/utils'
|
|
6
|
+
import { SHA1 } from '../..//primitives/Hash'
|
|
6
7
|
|
|
7
8
|
describe('Hash', function () {
|
|
8
9
|
function test (Hash, cases): void {
|
|
@@ -177,4 +178,27 @@ describe('Hash', function () {
|
|
|
177
178
|
})
|
|
178
179
|
}
|
|
179
180
|
})
|
|
181
|
+
|
|
182
|
+
describe('Hash strict length validation (TOB-21)', () => {
|
|
183
|
+
|
|
184
|
+
it('throws when pendingTotal is not a safe integer', () => {
|
|
185
|
+
const h = new SHA1()
|
|
186
|
+
|
|
187
|
+
h.pendingTotal = Number.MAX_SAFE_INTEGER + 10
|
|
188
|
+
|
|
189
|
+
expect(() => {
|
|
190
|
+
h.digest()
|
|
191
|
+
}).toThrow('Message too long for this hash function')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('throws when pendingTotal is negative', () => {
|
|
195
|
+
const h = new SHA1()
|
|
196
|
+
|
|
197
|
+
h.pendingTotal = -5
|
|
198
|
+
|
|
199
|
+
expect(() => {
|
|
200
|
+
h.digest()
|
|
201
|
+
}).toThrow('Message too long for this hash function')
|
|
202
|
+
})
|
|
203
|
+
})
|
|
180
204
|
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* eslint-env jest */
|
|
2
|
+
|
|
3
|
+
import { assertValidHex, normalizeHex } from '../../primitives/hex'
|
|
4
|
+
|
|
5
|
+
describe('hex utils', () => {
|
|
6
|
+
describe('assertValidHex', () => {
|
|
7
|
+
it('should not throw on valid hex strings', () => {
|
|
8
|
+
expect(() => assertValidHex('')).not.toThrow() // empty is allowed
|
|
9
|
+
expect(() => assertValidHex('00')).not.toThrow()
|
|
10
|
+
expect(() => assertValidHex('abcdef')).not.toThrow()
|
|
11
|
+
expect(() => assertValidHex('ABCDEF')).not.toThrow()
|
|
12
|
+
expect(() => assertValidHex('1234567890')).not.toThrow()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should throw on non-hex characters', () => {
|
|
16
|
+
expect(() => assertValidHex('zz')).toThrow('Invalid hex string')
|
|
17
|
+
expect(() => assertValidHex('0x1234')).toThrow('Invalid hex string')
|
|
18
|
+
expect(() => assertValidHex('12 34')).toThrow('Invalid hex string')
|
|
19
|
+
expect(() => assertValidHex('g1')).toThrow('Invalid hex string')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// ❌ old behavior: empty string was considered invalid
|
|
23
|
+
// it('should throw on empty string', () => {
|
|
24
|
+
// expect(() => assertValidHex('')).toThrow('Invalid hex string')
|
|
25
|
+
// })
|
|
26
|
+
|
|
27
|
+
it('should throw on undefined or null', () => {
|
|
28
|
+
expect(() => assertValidHex(undefined as any)).toThrow('Invalid hex string')
|
|
29
|
+
expect(() => assertValidHex(null as any)).toThrow('Invalid hex string')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('normalizeHex', () => {
|
|
34
|
+
it('should return lowercase hex', () => {
|
|
35
|
+
expect(normalizeHex('ABCD')).toBe('abcd')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should prepend 0 to odd-length hex strings', () => {
|
|
39
|
+
expect(normalizeHex('abc')).toBe('0abc')
|
|
40
|
+
expect(normalizeHex('f')).toBe('0f')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should leave even-length hex strings untouched (except lowercase)', () => {
|
|
44
|
+
expect(normalizeHex('AABB')).toBe('aabb')
|
|
45
|
+
expect(normalizeHex('001122')).toBe('001122')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should return empty string unchanged', () => {
|
|
49
|
+
expect(normalizeHex('')).toBe('')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should throw on invalid hex', () => {
|
|
53
|
+
expect(() => normalizeHex('xyz')).toThrow('Invalid hex string')
|
|
54
|
+
expect(() => normalizeHex('12 34')).toThrow('Invalid hex string')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -320,3 +320,42 @@ describe('verifyNotNull', () => {
|
|
|
320
320
|
expect(() => verifyNotNull(undefined, 'Another custom error')).toThrow('Another custom error')
|
|
321
321
|
})
|
|
322
322
|
})
|
|
323
|
+
|
|
324
|
+
describe('toUTF8 strict UTF-8 decoding (TOB-21)', () => {
|
|
325
|
+
|
|
326
|
+
it('replaces invalid 2-byte sequences with U+FFFD', () => {
|
|
327
|
+
// 0xC2 should expect a continuation byte 0x80–0xBF
|
|
328
|
+
const arr = [0xC2, 0x20] // 0x20 is INVALID continuation
|
|
329
|
+
const str = toUTF8(arr)
|
|
330
|
+
expect(str).toBe('\uFFFD')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('decodes valid 3-byte sequences', () => {
|
|
334
|
+
const euro = [0xE2, 0x82, 0xAC]
|
|
335
|
+
expect(toUTF8(euro)).toBe('€')
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('replaces invalid 3-byte sequences', () => {
|
|
339
|
+
// Middle byte invalid
|
|
340
|
+
const arr = [0xE2, 0x20, 0xAC]
|
|
341
|
+
expect(toUTF8(arr)).toBe('\uFFFD')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('decodes valid 4-byte sequences into surrogate pairs', () => {
|
|
345
|
+
const smile = [0xF0, 0x9F, 0x98, 0x80] // 😀
|
|
346
|
+
expect(toUTF8(smile)).toBe('😀')
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('replaces invalid 4-byte sequences with U+FFFD', () => {
|
|
350
|
+
// 0x9F is valid, 0x20 is INVALID continuation for byte 3
|
|
351
|
+
const arr = [0xF0, 0x9F, 0x20, 0x80]
|
|
352
|
+
expect(toUTF8(arr)).toBe('\uFFFD')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('replaces incomplete UTF-8 sequence at end', () => {
|
|
356
|
+
const arr = [0xE2] // incomplete 3-byte seq
|
|
357
|
+
expect(toUTF8(arr)).toBe('\uFFFD')
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
})
|
|
361
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/primitives/hex.ts
|
|
2
|
+
|
|
3
|
+
// Accepts empty string because empty byte arrays are valid in Bitcoin.
|
|
4
|
+
const PURE_HEX_REGEX = /^[0-9a-fA-F]*$/
|
|
5
|
+
|
|
6
|
+
export function assertValidHex (msg: string): void {
|
|
7
|
+
if (typeof msg !== 'string') {
|
|
8
|
+
console.error('assertValidHex FAIL (non-string):', msg)
|
|
9
|
+
throw new Error('Invalid hex string')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// allow empty
|
|
13
|
+
if (msg.length === 0) return
|
|
14
|
+
|
|
15
|
+
if (!PURE_HEX_REGEX.test(msg)) {
|
|
16
|
+
console.error('assertValidHex FAIL (bad hex):', msg)
|
|
17
|
+
throw new Error('Invalid hex string')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeHex (msg: string): string {
|
|
22
|
+
assertValidHex(msg)
|
|
23
|
+
|
|
24
|
+
// If empty, return empty — never force to "00"
|
|
25
|
+
if (msg.length === 0) return ''
|
|
26
|
+
|
|
27
|
+
let normalized = msg.toLowerCase()
|
|
28
|
+
|
|
29
|
+
// Pad odd-length hex
|
|
30
|
+
if (normalized.length % 2 !== 0) {
|
|
31
|
+
normalized = '0' + normalized
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return normalized
|
|
35
|
+
}
|
package/src/primitives/index.ts
CHANGED
|
@@ -5,7 +5,6 @@ export { default as PublicKey } from './PublicKey.js'
|
|
|
5
5
|
export { default as Signature } from './Signature.js'
|
|
6
6
|
export { default as PrivateKey, KeyShares } from './PrivateKey.js'
|
|
7
7
|
export { default as SymmetricKey } from './SymmetricKey.js'
|
|
8
|
-
export { default as DRBG } from './DRBG.js'
|
|
9
8
|
export * as ECDSA from './ECDSA.js'
|
|
10
9
|
export * as Utils from './utils.js'
|
|
11
10
|
export * as Hash from './Hash.js'
|