Skip to content

Commit df9bdee

Browse files
authored
Merge pull request #49 from xmtp/human-friendly-sig-requests
Human friendly signature requests
2 parents 53d7425 + 23fea04 commit df9bdee

File tree

5 files changed

+148
-63
lines changed

5 files changed

+148
-63
lines changed

src/crypto/PrivateKeyBundle.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import PublicKey from './PublicKey'
44
import PublicKeyBundle from './PublicKeyBundle'
55
import Ciphertext from './Ciphertext'
66
import * as ethers from 'ethers'
7-
import { getRandomValues, hexToBytes } from './utils'
7+
import { bytesToHex, getRandomValues, hexToBytes } from './utils'
88
import { decrypt, encrypt } from './encryption'
99
import { NoMatchingPreKeyError } from './errors'
1010

@@ -98,6 +98,19 @@ export default class PrivateKeyBundle implements proto.PrivateKeyBundle {
9898
return secret
9999
}
100100

101+
static storageSigRequestText(preKey: Uint8Array): string {
102+
// Note that an update to this signature request text will require
103+
// addition of backward compatability for existing encrypted bundles
104+
// and/or a migration; otherwise clients will no longer be able to
105+
// decrypt those bundles.
106+
return (
107+
'XMTP : Enable Identity\n' +
108+
`${bytesToHex(preKey)}\n` +
109+
'\n' +
110+
'For more info: https://xmtp.org/signatures/'
111+
)
112+
}
113+
101114
// encrypts/serializes the bundle for storage
102115
async encode(wallet: ethers.Signer): Promise<Uint8Array> {
103116
// serialize the contents
@@ -112,7 +125,9 @@ export default class PrivateKeyBundle implements proto.PrivateKeyBundle {
112125
preKeys: this.preKeys,
113126
}).finish()
114127
const wPreKey = getRandomValues(new Uint8Array(32))
115-
const secret = hexToBytes(await wallet.signMessage(wPreKey))
128+
const secret = hexToBytes(
129+
await wallet.signMessage(PrivateKeyBundle.storageSigRequestText(wPreKey))
130+
)
116131
const ciphertext = await encrypt(bytes, secret)
117132
return proto.EncryptedPrivateKeyBundle.encode({
118133
walletPreKey: wPreKey,
@@ -129,7 +144,11 @@ export default class PrivateKeyBundle implements proto.PrivateKeyBundle {
129144
if (!encrypted.walletPreKey) {
130145
throw new Error('missing wallet pre-key')
131146
}
132-
const secret = hexToBytes(await wallet.signMessage(encrypted.walletPreKey))
147+
const secret = hexToBytes(
148+
await wallet.signMessage(
149+
PrivateKeyBundle.storageSigRequestText(encrypted.walletPreKey)
150+
)
151+
)
133152
if (!encrypted.ciphertext?.aes256GcmHkdfSha256) {
134153
throw new Error('missing bundle ciphertext')
135154
}

src/crypto/PublicKey.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as proto from '../../src/proto/messaging'
22
import * as secp from '@noble/secp256k1'
33
import Signature from './Signature'
4-
import { hexToBytes } from './utils'
4+
import { bytesToHex, hexToBytes } from './utils'
55
import * as ethers from 'ethers'
66
import { sha256 } from './encryption'
77

@@ -63,6 +63,19 @@ export default class PublicKey implements proto.PublicKey {
6363
}).finish()
6464
}
6565

66+
identitySigRequestText(): string {
67+
// Note that an update to this signature request text will require
68+
// addition of backward compatability for existing signatures
69+
// and/or a migration; otherwise clients will fail to verify previously
70+
// signed keys.
71+
return (
72+
'XMTP : Create Identity\n' +
73+
`${bytesToHex(this.bytesToSign())}\n` +
74+
'\n' +
75+
'For more info: https://xmtp.org/signatures/'
76+
)
77+
}
78+
6679
// verify that the provided PublicKey was signed by the corresponding PrivateKey
6780
async verifyKey(pub: PublicKey): Promise<boolean> {
6881
if (typeof pub.signature === undefined) {
@@ -80,7 +93,7 @@ export default class PublicKey implements proto.PublicKey {
8093
if (!this.secp256k1Uncompressed) {
8194
throw new Error('missing public key')
8295
}
83-
const sigString = await wallet.signMessage(this.bytesToSign())
96+
const sigString = await wallet.signMessage(this.identitySigRequestText())
8497
const eSig = ethers.utils.splitSignature(sigString)
8598
const r = hexToBytes(eSig.r)
8699
const s = hexToBytes(eSig.s)
@@ -104,7 +117,9 @@ export default class PublicKey implements proto.PublicKey {
104117
if (!this.secp256k1Uncompressed) {
105118
throw new Error('missing public key')
106119
}
107-
const digest = hexToBytes(ethers.utils.hashMessage(this.bytesToSign()))
120+
const digest = hexToBytes(
121+
ethers.utils.hashMessage(this.identitySigRequestText())
122+
)
108123
const pk = this.signature.getPublicKey(digest)
109124
if (!pk) {
110125
throw new Error('key was not signed by a wallet')

test/crypto/PrivateKeyBundle.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as assert from 'assert'
2+
import { PrivateKey, PrivateKeyBundle } from '../../src/crypto'
3+
import * as ethers from 'ethers'
4+
import { bytesToHex, getRandomValues, hexToBytes } from '../../src/crypto/utils'
5+
6+
describe('Crypto', function () {
7+
describe('PrivateKeyBundle', function () {
8+
it('encrypts private key bundle for storage using a wallet', async function () {
9+
// create a wallet using a generated key
10+
const bobPri = PrivateKey.generate()
11+
assert.ok(bobPri.secp256k1)
12+
const wallet = new ethers.Wallet(bobPri.secp256k1.bytes)
13+
// generate key bundle
14+
const bob = await PrivateKeyBundle.generate(wallet)
15+
// encrypt and serialize the bundle for storage
16+
const bytes = await bob.encode(wallet)
17+
// decrypt and decode the bundle from storage
18+
const bobDecoded = await PrivateKeyBundle.decode(wallet, bytes)
19+
assert.ok(bob.identityKey)
20+
assert.ok(bobDecoded.identityKey)
21+
assert.ok(bob.identityKey.publicKey.signature)
22+
assert.ok(bobDecoded.identityKey.publicKey.signature)
23+
assert.deepEqual(
24+
bob.identityKey.publicKey.signature?.ecdsaCompact?.bytes,
25+
bobDecoded.identityKey.publicKey.signature?.ecdsaCompact?.bytes
26+
)
27+
assert.ok(bob.identityKey.secp256k1)
28+
assert.ok(bobDecoded.identityKey.secp256k1)
29+
assert.deepEqual(
30+
bob.identityKey.secp256k1.bytes,
31+
bobDecoded.identityKey.secp256k1.bytes
32+
)
33+
assert.ok(bob.preKeys[0].secp256k1)
34+
assert.ok(bobDecoded.preKeys[0].secp256k1)
35+
assert.deepEqual(
36+
bob.preKeys[0].secp256k1.bytes,
37+
bobDecoded.preKeys[0].secp256k1.bytes
38+
)
39+
})
40+
41+
it('human-friendly storage signature request text', async function () {
42+
const pri = PrivateKey.fromBytes(
43+
hexToBytes(
44+
'08aaa9dad3ed2f12220a206fd789a6ee2376bb6595b4ebace57c7a79e6e4f1f12c8416d611399eda6c74cb1a4c08aaa9dad3ed2f1a430a4104e208133ea0973a9968fe5362e5ac0a8bbbe2aa16d796add31f3d027a1b894389873d7f282163bceb1fc3ca60d589d1e667956c40fed4cdaa7edc1392d2100b8a'
45+
)
46+
)
47+
assert.ok(pri.secp256k1)
48+
const wallet = new ethers.Wallet(pri.secp256k1.bytes)
49+
const bundle = await PrivateKeyBundle.generate(wallet)
50+
const preKey = hexToBytes(
51+
'f51bd1da9ec2239723ae2cf6a9f8d0ac37546b27e634002c653d23bacfcc67ad'
52+
)
53+
const actual = PrivateKeyBundle.storageSigRequestText(preKey)
54+
const expected =
55+
'XMTP : Enable Identity\nf51bd1da9ec2239723ae2cf6a9f8d0ac37546b27e634002c653d23bacfcc67ad\n\nFor more info: https://xmtp.org/signatures/'
56+
assert.equal(actual, expected)
57+
assert.ok(true)
58+
})
59+
})
60+
})

test/crypto/PublicKey.test.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as assert from 'assert'
2+
import { PrivateKey, PublicKey, utils } from '../../src/crypto'
3+
import * as ethers from 'ethers'
4+
import { bytesToHex, hexToBytes } from '../../src/crypto/utils'
5+
6+
describe('Crypto', function () {
7+
describe('PublicKey', function () {
8+
it('derives address from public key', function () {
9+
// using the sample from https://kobl.one/blog/create-full-ethereum-keypair-and-address/
10+
const bytes = utils.hexToBytes(
11+
'04836b35a026743e823a90a0ee3b91bf615c6a757e2b60b9e1dc1826fd0dd16106f7bc1e8179f665015f43c6c81f39062fc2086ed849625c06e04697698b21855e'
12+
)
13+
const pub = new PublicKey({
14+
secp256k1Uncompressed: { bytes },
15+
timestamp: new Date().getTime(),
16+
})
17+
const address = pub.getEthereumAddress()
18+
assert.equal(address, '0x0BED7ABd61247635c1973eB38474A2516eD1D884')
19+
})
20+
21+
it('human-friendly identity key signature request', async function () {
22+
const alice = PrivateKey.fromBytes(
23+
hexToBytes(
24+
'08aaa9dad3ed2f12220a206fd789a6ee2376bb6595b4ebace57c7a79e6e4f1f12c8416d611399eda6c74cb1a4c08aaa9dad3ed2f1a430a4104e208133ea0973a9968fe5362e5ac0a8bbbe2aa16d796add31f3d027a1b894389873d7f282163bceb1fc3ca60d589d1e667956c40fed4cdaa7edc1392d2100b8a'
25+
)
26+
)
27+
const actual = alice.publicKey.identitySigRequestText()
28+
const expected =
29+
'XMTP : Create Identity\n08aaa9dad3ed2f1a430a4104e208133ea0973a9968fe5362e5ac0a8bbbe2aa16d796add31f3d027a1b894389873d7f282163bceb1fc3ca60d589d1e667956c40fed4cdaa7edc1392d2100b8a\n\nFor more info: https://xmtp.org/signatures/'
30+
assert.equal(actual, expected)
31+
})
32+
33+
it('signs keys using a wallet', async function () {
34+
// create a wallet using a generated key
35+
const alice = PrivateKey.generate()
36+
assert.ok(alice.secp256k1)
37+
const wallet = new ethers.Wallet(alice.secp256k1.bytes)
38+
// sanity check that we agree with the wallet about the address
39+
assert.ok(wallet.address, alice.publicKey.getEthereumAddress())
40+
// sign the public key using the wallet
41+
await alice.publicKey.signWithWallet(wallet)
42+
assert.ok(alice.publicKey.signature)
43+
// validate the key signature and return wallet address
44+
const address = alice.publicKey.walletSignatureAddress()
45+
assert.equal(address, wallet.address)
46+
})
47+
})
48+
})

test/crypto/index.test.ts

-57
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,6 @@ describe('Crypto', function () {
6262
pri.publicKey.secp256k1Uncompressed.bytes
6363
)
6464
})
65-
it('derives address from public key', function () {
66-
// using the sample from https://kobl.one/blog/create-full-ethereum-keypair-and-address/
67-
const bytes = utils.hexToBytes(
68-
'04836b35a026743e823a90a0ee3b91bf615c6a757e2b60b9e1dc1826fd0dd16106f7bc1e8179f665015f43c6c81f39062fc2086ed849625c06e04697698b21855e'
69-
)
70-
const pub = new PublicKey({
71-
secp256k1Uncompressed: { bytes },
72-
timestamp: new Date().getTime(),
73-
})
74-
const address = pub.getEthereumAddress()
75-
assert.equal(address, '0x0BED7ABd61247635c1973eB38474A2516eD1D884')
76-
})
7765
it('encrypts and decrypts payload with key bundles', async function () {
7866
const alice = await PrivateKeyBundle.generate()
7967
const bob = await PrivateKeyBundle.generate()
@@ -99,49 +87,4 @@ describe('Crypto', function () {
9987
assert.ok(pub2.preKey)
10088
assert.ok(pub2.identityKey.verifyKey(pub2.preKey))
10189
})
102-
it('signs keys using a wallet', async function () {
103-
// create a wallet using a generated key
104-
const alice = PrivateKey.generate()
105-
assert.ok(alice.secp256k1)
106-
const wallet = new ethers.Wallet(alice.secp256k1.bytes)
107-
// sanity check that we agree with the wallet about the address
108-
assert.ok(wallet.address, alice.publicKey.getEthereumAddress())
109-
// sign the public key using the wallet
110-
await alice.publicKey.signWithWallet(wallet)
111-
// validate the key signature and return wallet address
112-
const address = alice.publicKey.walletSignatureAddress()
113-
assert.equal(address, wallet.address)
114-
})
115-
it('encrypts private key bundle for storage using a wallet', async function () {
116-
// create a wallet using a generated key
117-
const alice = PrivateKey.generate()
118-
assert.ok(alice.secp256k1)
119-
const wallet = new ethers.Wallet(alice.secp256k1.bytes)
120-
// generate key bundle
121-
const bob = await PrivateKeyBundle.generate(wallet)
122-
// encrypt and serialize the bundle for storage
123-
const bytes = await bob.encode(wallet)
124-
// decrypt and decode the bundle from storage
125-
const bobDecoded = await PrivateKeyBundle.decode(wallet, bytes)
126-
assert.ok(bob.identityKey)
127-
assert.ok(bobDecoded.identityKey)
128-
assert.ok(bob.identityKey.publicKey.signature)
129-
assert.ok(bobDecoded.identityKey.publicKey.signature)
130-
assert.deepEqual(
131-
bob.identityKey.publicKey.signature?.ecdsaCompact?.bytes,
132-
bobDecoded.identityKey.publicKey.signature?.ecdsaCompact?.bytes
133-
)
134-
assert.ok(bob.identityKey.secp256k1)
135-
assert.ok(bobDecoded.identityKey.secp256k1)
136-
assert.deepEqual(
137-
bob.identityKey.secp256k1.bytes,
138-
bobDecoded.identityKey.secp256k1.bytes
139-
)
140-
assert.ok(bob.preKeys[0].secp256k1)
141-
assert.ok(bobDecoded.preKeys[0].secp256k1)
142-
assert.deepEqual(
143-
bob.preKeys[0].secp256k1.bytes,
144-
bobDecoded.preKeys[0].secp256k1.bytes
145-
)
146-
})
14790
})

0 commit comments

Comments
 (0)