Skip to content

Commit 1e011d8

Browse files
authored
Key version conversion (#176)
feat: key version conversion Allow converting legacy keys into the new key format
1 parent 18449ab commit 1e011d8

File tree

6 files changed

+97
-13
lines changed

6 files changed

+97
-13
lines changed

package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"dependencies": {
6262
"@noble/secp256k1": "^1.5.2",
6363
"@stardazed/streams-polyfill": "^2.4.0",
64-
"@xmtp/proto": "^3.1.0",
64+
"@xmtp/proto": "^3.2.0",
6565
"ethers": "^5.5.3",
6666
"long": "^5.2.0",
6767
"node-localstorage": "^2.2.1"

src/crypto/PrivateKey.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class SignedPrivateKey
5454
secp256k1Uncompressed: {
5555
bytes: secp.getPublicKey(secp256k1.bytes),
5656
},
57-
createdNs: createdNs,
57+
createdNs,
5858
})
5959
const signed = await signer.signKey(unsigned)
6060
return new SignedPrivateKey({
@@ -159,6 +159,17 @@ export class SignedPrivateKey
159159
static fromBytes(bytes: Uint8Array): SignedPrivateKey {
160160
return new SignedPrivateKey(privateKey.SignedPrivateKey.decode(bytes))
161161
}
162+
163+
static fromLegacyKey(
164+
key: PrivateKey,
165+
signedByWallet?: boolean
166+
): SignedPrivateKey {
167+
return new SignedPrivateKey({
168+
createdNs: key.timestamp.mul(1000000),
169+
secp256k1: key.secp256k1,
170+
publicKey: SignedPublicKey.fromLegacyKey(key.publicKey, signedByWallet),
171+
})
172+
}
162173
}
163174

164175
// LEGACY: PrivateKey represents a secp256k1 private key.
@@ -193,7 +204,7 @@ export class PrivateKey implements privateKey.PrivateKey {
193204
secp256k1Uncompressed: {
194205
bytes: secp.getPublicKey(secp256k1.bytes),
195206
},
196-
timestamp: timestamp,
207+
timestamp,
197208
}),
198209
})
199210
}

src/crypto/PublicKey.ts

+34-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ function secp256k1UncompressedCheck(key: secp256k1Uncompressed): void {
2222
}
2323
}
2424

25+
const MS_NS_TIMESTAMP_THRESHOLD = new Long(10 ** 9).mul(10 ** 9)
26+
2527
// Basic public key without a signature.
2628
export class UnsignedPublicKey implements publicKey.UnsignedPublicKey {
27-
createdNs: Long // time the key was generated, ns since epoch
29+
// time the key was generated, normally ns since epoch, however
30+
// to allow transparent conversion of pre-existing signed PublicKey to SignedPublicKey
31+
// it can also be ms since epoch; use MS_NS_TIMESTAMP_THRESHOLD to distinguish
32+
// the two cases.
33+
createdNs: Long
2834
secp256k1Uncompressed: secp256k1Uncompressed // eslint-disable-line camelcase
2935

3036
constructor(obj: publicKey.UnsignedPublicKey) {
@@ -38,7 +44,14 @@ export class UnsignedPublicKey implements publicKey.UnsignedPublicKey {
3844

3945
// The time the key was generated.
4046
generated(): Date | undefined {
41-
return new Date(this.createdNs.div(1000000).toNumber())
47+
return new Date(this.timestamp.toNumber())
48+
}
49+
50+
// creation time in milliseconds
51+
get timestamp(): Long {
52+
return this.createdNs < MS_NS_TIMESTAMP_THRESHOLD
53+
? this.createdNs
54+
: this.createdNs.div(1000000)
4255
}
4356

4457
// Verify that signature was created from the digest using matching private key.
@@ -155,6 +168,25 @@ export class SignedPublicKey
155168
static fromBytes(bytes: Uint8Array): SignedPublicKey {
156169
return new SignedPublicKey(publicKey.SignedPublicKey.decode(bytes))
157170
}
171+
172+
static fromLegacyKey(
173+
legacyKey: PublicKey,
174+
signedByWallet?: boolean
175+
): SignedPublicKey {
176+
if (!legacyKey.signature) {
177+
throw new Error('key is not signed')
178+
}
179+
let signature = legacyKey.signature
180+
if (signedByWallet) {
181+
signature = new Signature({
182+
walletEcdsaCompact: signature.ecdsaCompact,
183+
})
184+
}
185+
return new SignedPublicKey({
186+
keyBytes: legacyKey.bytesToSign(),
187+
signature,
188+
})
189+
}
158190
}
159191

160192
// LEGACY: PublicKey optionally signed with another trusted key pair or a wallet.

src/crypto/PublicKeyBundle.ts

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ export class SignedPublicKeyBundle implements publicKey.SignedPublicKeyBundle {
3838
const decoded = publicKey.SignedPublicKeyBundle.decode(bytes)
3939
return new SignedPublicKeyBundle(decoded)
4040
}
41+
42+
static fromLegacyBundle(bundle: PublicKeyBundle): SignedPublicKeyBundle {
43+
return new SignedPublicKeyBundle({
44+
identityKey: SignedPublicKey.fromLegacyKey(bundle.identityKey),
45+
preKey: SignedPublicKey.fromLegacyKey(bundle.preKey),
46+
})
47+
}
4148
}
4249

4350
// LEGACY: PublicKeyBundle packages all the keys that a participant should advertise.

test/crypto/PublicKey.test.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '../../src/crypto'
1212
import { Wallet } from 'ethers'
1313
import * as secp from '@noble/secp256k1'
14-
import { hexToBytes } from '../../src/crypto/utils'
14+
import { hexToBytes, equalBytes } from '../../src/crypto/utils'
1515

1616
describe('Crypto', function () {
1717
describe('Signed Keys', function () {
@@ -79,5 +79,39 @@ describe('Crypto', function () {
7979
const address = alice.publicKey.walletSignatureAddress()
8080
assert.equal(address, wallet.address)
8181
})
82+
it('converts legacy keys to new keys', async function () {
83+
// Key signed by a wallet
84+
const wallet = new Wallet(PrivateKey.generate().secp256k1.bytes)
85+
const identityKey = PrivateKey.generate()
86+
await identityKey.publicKey.signWithWallet(wallet)
87+
const iPub = identityKey.publicKey
88+
assert.equal(iPub.walletSignatureAddress(), wallet.address)
89+
const iPub2 = SignedPublicKey.fromLegacyKey(iPub, true)
90+
assert.ok(
91+
equalBytes(
92+
iPub2.secp256k1Uncompressed.bytes,
93+
iPub.secp256k1Uncompressed.bytes
94+
)
95+
)
96+
assert.equal(iPub2.generated, iPub.generated)
97+
assert.ok(equalBytes(iPub2.keyBytes, iPub.bytesToSign()))
98+
const address = await iPub2.walletSignatureAddress()
99+
assert.equal(address, wallet.address)
100+
101+
// Key signed by a key
102+
const preKey = PrivateKey.generate()
103+
await identityKey.signKey(preKey.publicKey)
104+
const pPub = preKey.publicKey
105+
const pPub2 = SignedPublicKey.fromLegacyKey(pPub)
106+
assert.ok(
107+
equalBytes(
108+
pPub2.secp256k1Uncompressed.bytes,
109+
pPub.secp256k1Uncompressed.bytes
110+
)
111+
)
112+
assert.equal(pPub2.generated, pPub.generated)
113+
assert.ok(equalBytes(pPub2.keyBytes, pPub.bytesToSign()))
114+
assert.ok(iPub2.verifyKey(pPub2))
115+
})
82116
})
83117
})

0 commit comments

Comments
 (0)