1
1
import { privateKey } from '@xmtp/proto'
2
2
import * as secp from '@noble/secp256k1'
3
3
import Long from 'long'
4
- import Signature from './Signature'
5
- import PublicKey from './PublicKey'
4
+ import Signature , {
5
+ ECDSACompactWithRecovery ,
6
+ ecdsaSignerKey ,
7
+ KeySigner ,
8
+ } from './Signature'
9
+ import { PublicKey , SignedPublicKey , UnsignedPublicKey } from './PublicKey'
6
10
import Ciphertext from './Ciphertext'
7
11
import { decrypt , encrypt , sha256 } from './encryption'
12
+ import { equalBytes } from './utils'
8
13
9
- // PrivateKey represents a secp256k1 private key.
10
- export default class PrivateKey implements privateKey . PrivateKey {
14
+ // SECP256k1 private key
15
+ type secp256k1 = {
16
+ bytes : Uint8Array // D big-endian, 32 bytes
17
+ }
18
+
19
+ // Validate SECP256k1 private key
20
+ function secp256k1Check ( key : secp256k1 ) : void {
21
+ if ( key . bytes . length !== 32 ) {
22
+ throw new Error ( `invalid private key length: ${ key . bytes . length } ` )
23
+ }
24
+ }
25
+
26
+ // A private key signed with another key pair or a wallet.
27
+ export class SignedPrivateKey
28
+ implements privateKey . SignedPrivateKey , KeySigner
29
+ {
30
+ createdNs : Long // time the key was generated, ns since epoch
31
+ secp256k1 : secp256k1 // eslint-disable-line camelcase
32
+ publicKey : SignedPublicKey // caches corresponding PublicKey
33
+
34
+ constructor ( obj : privateKey . SignedPrivateKey ) {
35
+ if ( ! obj . secp256k1 ) {
36
+ throw new Error ( 'invalid private key' )
37
+ }
38
+ secp256k1Check ( obj . secp256k1 )
39
+ this . secp256k1 = obj . secp256k1
40
+ this . createdNs = obj . createdNs
41
+ if ( ! obj . publicKey ) {
42
+ throw new Error ( 'missing public key' )
43
+ }
44
+ this . publicKey = new SignedPublicKey ( obj . publicKey )
45
+ }
46
+
47
+ // Create a random key pair signed by the signer.
48
+ static async generate ( signer : KeySigner ) : Promise < SignedPrivateKey > {
49
+ const secp256k1 = {
50
+ bytes : secp . utils . randomPrivateKey ( ) ,
51
+ }
52
+ const createdNs = Long . fromNumber ( new Date ( ) . getTime ( ) ) . mul ( 1000000 )
53
+ const unsigned = new UnsignedPublicKey ( {
54
+ secp256k1Uncompressed : {
55
+ bytes : secp . getPublicKey ( secp256k1 . bytes ) ,
56
+ } ,
57
+ createdNs : createdNs ,
58
+ } )
59
+ const signed = await signer . signKey ( unsigned )
60
+ return new SignedPrivateKey ( {
61
+ secp256k1,
62
+ createdNs,
63
+ publicKey : signed ,
64
+ } )
65
+ }
66
+
67
+ // Time the key was generated.
68
+ generated ( ) : Date | undefined {
69
+ return new Date ( this . createdNs . div ( 1000000 ) . toNumber ( ) )
70
+ }
71
+
72
+ // Sign provided digest.
73
+ async sign ( digest : Uint8Array ) : Promise < Signature > {
74
+ const [ signature , recovery ] = await secp . sign (
75
+ digest ,
76
+ this . secp256k1 . bytes ,
77
+ {
78
+ recovered : true ,
79
+ der : false ,
80
+ }
81
+ )
82
+ return new Signature ( {
83
+ ecdsaCompact : { bytes : signature , recovery } ,
84
+ } )
85
+ }
86
+
87
+ // Sign provided public key.
88
+ async signKey ( pub : UnsignedPublicKey ) : Promise < SignedPublicKey > {
89
+ const keyBytes = pub . toBytes ( )
90
+ const digest = await sha256 ( keyBytes )
91
+ const signature = await this . sign ( digest )
92
+ return new SignedPublicKey ( {
93
+ keyBytes,
94
+ signature,
95
+ } )
96
+ }
97
+
98
+ // Return public key of the signer of the provided signed key.
99
+ static async signerKey (
100
+ key : SignedPublicKey ,
101
+ signature : ECDSACompactWithRecovery
102
+ ) : Promise < UnsignedPublicKey | undefined > {
103
+ const digest = await sha256 ( key . bytesToSign ( ) )
104
+ return ecdsaSignerKey ( digest , signature )
105
+ }
106
+
107
+ // Derive shared secret from peer's PublicKey;
108
+ // the peer can derive the same secret using their private key and our public key.
109
+ sharedSecret ( peer : SignedPublicKey | UnsignedPublicKey ) : Uint8Array {
110
+ return secp . getSharedSecret (
111
+ this . secp256k1 . bytes ,
112
+ peer . secp256k1Uncompressed . bytes ,
113
+ false
114
+ )
115
+ }
116
+
117
+ // encrypt plain bytes using a shared secret derived from peer's PublicKey;
118
+ // additionalData allows including unencrypted parts of a Message in the authentication
119
+ // protection provided by the encrypted part (to make the whole Message tamper evident)
120
+ encrypt (
121
+ plain : Uint8Array ,
122
+ peer : UnsignedPublicKey ,
123
+ additionalData ?: Uint8Array
124
+ ) : Promise < Ciphertext > {
125
+ const secret = this . sharedSecret ( peer )
126
+ return encrypt ( plain , secret , additionalData )
127
+ }
128
+
129
+ // decrypt Ciphertext using a shared secret derived from peer's PublicKey;
130
+ // throws if any part of Ciphertext or additionalData was tampered with
131
+ decrypt (
132
+ encrypted : Ciphertext ,
133
+ peer : UnsignedPublicKey ,
134
+ additionalData ?: Uint8Array
135
+ ) : Promise < Uint8Array > {
136
+ const secret = this . sharedSecret ( peer )
137
+ return decrypt ( encrypted , secret , additionalData )
138
+ }
139
+
140
+ // Does the provided PublicKey correspond to this PrivateKey?
141
+ matches ( key : SignedPublicKey ) : boolean {
142
+ return this . publicKey . equals ( key )
143
+ }
144
+
145
+ // Is other the same/equivalent key?
146
+ equals ( other : this) : boolean {
147
+ return (
148
+ equalBytes ( this . secp256k1 . bytes , other . secp256k1 . bytes ) &&
149
+ this . publicKey . equals ( other . publicKey )
150
+ )
151
+ }
152
+
153
+ // Encode this key into bytes.
154
+ toBytes ( ) : Uint8Array {
155
+ return privateKey . SignedPrivateKey . encode ( this ) . finish ( )
156
+ }
157
+
158
+ // Decode key from bytes.
159
+ static fromBytes ( bytes : Uint8Array ) : SignedPrivateKey {
160
+ return new SignedPrivateKey ( privateKey . SignedPrivateKey . decode ( bytes ) )
161
+ }
162
+ }
163
+
164
+ // LEGACY: PrivateKey represents a secp256k1 private key.
165
+ export class PrivateKey implements privateKey . PrivateKey {
11
166
timestamp : Long
12
- secp256k1 : privateKey . PrivateKey_Secp256k1 | undefined // eslint-disable-line camelcase
167
+ secp256k1 : secp256k1 // eslint-disable-line camelcase
13
168
publicKey : PublicKey // caches corresponding PublicKey
14
169
15
170
constructor ( obj : privateKey . PrivateKey ) {
16
171
if ( ! obj . secp256k1 ) {
17
172
throw new Error ( 'invalid private key' )
18
173
}
19
- if ( obj . secp256k1 . bytes . length !== 32 ) {
20
- throw new Error (
21
- `invalid private key length: ${ obj . secp256k1 . bytes . length } `
22
- )
23
- }
174
+ secp256k1Check ( obj . secp256k1 )
24
175
this . timestamp = obj . timestamp
25
176
this . secp256k1 = obj . secp256k1
26
177
if ( ! obj . publicKey ) {
@@ -48,17 +199,11 @@ export default class PrivateKey implements privateKey.PrivateKey {
48
199
}
49
200
50
201
generated ( ) : Date | undefined {
51
- if ( ! this . timestamp ) {
52
- return undefined
53
- }
54
202
return new Date ( this . timestamp . toNumber ( ) )
55
203
}
56
204
57
205
// sign provided digest
58
206
async sign ( digest : Uint8Array ) : Promise < Signature > {
59
- if ( ! this . secp256k1 ) {
60
- throw new Error ( 'invalid private key' )
61
- }
62
207
const [ signature , recovery ] = await secp . sign (
63
208
digest ,
64
209
this . secp256k1 . bytes ,
@@ -74,9 +219,6 @@ export default class PrivateKey implements privateKey.PrivateKey {
74
219
75
220
// sign provided public key
76
221
async signKey ( pub : PublicKey ) : Promise < PublicKey > {
77
- if ( ! pub . secp256k1Uncompressed ) {
78
- throw new Error ( 'invalid public key' )
79
- }
80
222
const digest = await sha256 ( pub . bytesToSign ( ) )
81
223
pub . signature = await this . sign ( digest )
82
224
return pub
@@ -85,12 +227,6 @@ export default class PrivateKey implements privateKey.PrivateKey {
85
227
// derive shared secret from peer's PublicKey;
86
228
// the peer can derive the same secret using their PrivateKey and our PublicKey
87
229
sharedSecret ( peer : PublicKey ) : Uint8Array {
88
- if ( ! peer . secp256k1Uncompressed ) {
89
- throw new Error ( 'invalid public key' )
90
- }
91
- if ( ! this . secp256k1 ) {
92
- throw new Error ( 'invalid private key' )
93
- }
94
230
return secp . getSharedSecret (
95
231
this . secp256k1 . bytes ,
96
232
peer . secp256k1Uncompressed . bytes ,
@@ -121,15 +257,17 @@ export default class PrivateKey implements privateKey.PrivateKey {
121
257
return decrypt ( encrypted , secret , additionalData )
122
258
}
123
259
124
- // Does the provided PublicKey correspnd to this PrivateKey?
260
+ // Does the provided PublicKey correspond to this PrivateKey?
125
261
matches ( key : PublicKey ) : boolean {
126
262
return this . publicKey . equals ( key )
127
263
}
128
264
265
+ // Encode this key into bytes.
129
266
toBytes ( ) : Uint8Array {
130
267
return privateKey . PrivateKey . encode ( this ) . finish ( )
131
268
}
132
269
270
+ // Decode key from bytes.
133
271
static fromBytes ( bytes : Uint8Array ) : PrivateKey {
134
272
return new PrivateKey ( privateKey . PrivateKey . decode ( bytes ) )
135
273
}
0 commit comments