Skip to content

Commit ad300fb

Browse files
beguenejanniks
authored andcommitted
feat: Add encode / decode messages to support
arbitrary message signing closes #1231
1 parent ade4444 commit ad300fb

File tree

4 files changed

+68
-1
lines changed

4 files changed

+68
-1
lines changed

packages/encryption/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"bn.js": "^5.2.0",
4242
"bs58": "^5.0.0",
4343
"ripemd160-min": "^0.0.6",
44-
"sha.js": "^2.4.11"
44+
"sha.js": "^2.4.11",
45+
"varuint-bitcoin": "^1.1.2"
4546
},
4647
"devDependencies": {
4748
"@peculiar/webcrypto": "^1.1.6",

packages/encryption/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ export * from './sha2Hash';
1111
export * from './encryption';
1212

1313
export * from './utils';
14+
15+
export * from './messageSignature';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { sha256 } from 'sha.js';
2+
import { encode, decode, encodingLength } from 'varuint-bitcoin';
3+
import { Buffer } from '@stacks/common';
4+
5+
// 'Stacks Message Signing:\n'.length // = 24
6+
// 'Stacks Message Signing:\n'.length.toString(16) // = 18
7+
const chainPrefix = '\x18Stacks Message Signing:\n';
8+
9+
export function hashMessage(message: string) {
10+
return new sha256().update(encodeMessage(message)).digest();
11+
}
12+
13+
export function encodeMessage(message: string | Buffer): Buffer {
14+
const encoded = encode(Buffer.from(message).length);
15+
return Buffer.concat([Buffer.from(chainPrefix), encoded, Buffer.from(message)]);
16+
}
17+
18+
export function decodeMessage(encodedMessage: Buffer): Buffer {
19+
// Remove the chain prefix: 1 for the varint and 24 for the length of the string
20+
// 'Stacks Message Signing:\n'
21+
const messageWithoutChainPrefix = encodedMessage.subarray(1 + 24);
22+
const decoded = decode(messageWithoutChainPrefix);
23+
const varIntLength = encodingLength(decoded);
24+
// Remove the varint prefix
25+
return messageWithoutChainPrefix.slice(varIntLength);
26+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { decodeMessage, encodeMessage, hashMessage } from '../src/messageSignature';
2+
3+
test('encodeMessage / decodeMessage', () => {
4+
// array of messages and their expected encoded message
5+
const messages = [
6+
['hello world', '\x18Stacks Message Signing:\n\x0bhello world'],
7+
['', '\x18Stacks Message Signing:\n\x00'],
8+
// Longer message (to test a different length for the var_int prefix)
9+
['This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix',
10+
Buffer.concat([
11+
Buffer.from('\x18Stacks Message Signing:\n'),
12+
// message length = 284 (decimal) = 011c (hex) <=> \x1c\x01 (little endian encoding)
13+
// Since length = 284 is < 0xFFFF, prefix the int with 0xFD followed by 2 bytes for a total of 3 bytes (see https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer)
14+
Buffer.from(new Uint8Array([253, 28, 1])),
15+
Buffer.from('This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix')
16+
])],
17+
]
18+
for (let messageArr of messages) {
19+
const [message, expectedEncodedMessage] = messageArr;
20+
const encodedMessage = encodeMessage(message);
21+
expect(encodedMessage.equals(Buffer.from(expectedEncodedMessage))).toBeTruthy();
22+
const decodedMessage = decodeMessage(encodedMessage);
23+
expect(decodedMessage.toString()).toEqual(message);
24+
}
25+
26+
27+
});
28+
29+
test('hash message vs hash of manually constructed message', () => {
30+
// echo -n '\x18Stacks Message Signing:\n\x0bhello world' | openssl dgst -sha256
31+
// 664d1478d36935361c1a8eda75fce73c49a93b58e55ed7cb45c3860317814991
32+
33+
const message1 = 'hello world';
34+
const hash1 = hashMessage(message1);
35+
const expectedHash = '664d1478d36935361c1a8eda75fce73c49a93b58e55ed7cb45c3860317814991';
36+
expect(hash1.length).toEqual(32); // 32 bytes of sha256
37+
expect(hash1.toString('hex')).toEqual(expectedHash);
38+
});

0 commit comments

Comments
 (0)