@wallaby-cash/cryptography 1.0.0
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/CHANGELOG.md +15 -0
- package/README.md +121 -0
- package/package.json +46 -0
- package/src/createIdentity.ts +66 -0
- package/src/decryptWithPrivateKey.ts +10 -0
- package/src/encryptWithPublicKey.ts +12 -0
- package/src/encryption-utils.ts +93 -0
- package/src/hash.ts +14 -0
- package/src/index.ts +33 -0
- package/src/publicKeyByPrivateKey.ts +17 -0
- package/src/recoverPublicKey.ts +26 -0
- package/src/sign.ts +27 -0
- package/src/types.ts +10 -0
- package/src/util.ts +110 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @wallaby-cash/cryptography
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- Initial release of @wallaby-cash/cryptography package
|
|
8
|
+
|
|
9
|
+
This release includes:
|
|
10
|
+
- Identity creation with public/private key pairs
|
|
11
|
+
- Message signing and verification
|
|
12
|
+
- Public key encryption and decryption
|
|
13
|
+
- Keccak256 hashing utilities
|
|
14
|
+
- Public key recovery from signatures
|
|
15
|
+
- Public key derivation from private keys
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @wallaby-cash/cryptography
|
|
2
|
+
|
|
3
|
+
Cryptography utilities for Wallaby Cash, providing Ethereum-compatible cryptographic operations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @wallaby-cash/cryptography
|
|
9
|
+
# or
|
|
10
|
+
bun add @wallaby-cash/cryptography
|
|
11
|
+
# or
|
|
12
|
+
yarn add @wallaby-cash/cryptography
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Identity Creation**: Generate new cryptographic identities with public/private key pairs
|
|
18
|
+
- **Signing**: Sign messages with private keys
|
|
19
|
+
- **Encryption**: Encrypt and decrypt data using public/private key cryptography
|
|
20
|
+
- **Hashing**: Keccak256 hashing utilities
|
|
21
|
+
- **Key Recovery**: Recover public keys from signatures
|
|
22
|
+
- **Key Derivation**: Derive public keys from private keys
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import {
|
|
28
|
+
createIdentity,
|
|
29
|
+
sign,
|
|
30
|
+
encryptWithPublicKey,
|
|
31
|
+
decryptWithPrivateKey,
|
|
32
|
+
hash,
|
|
33
|
+
publicKeyByPrivateKey,
|
|
34
|
+
recoverPublicKey
|
|
35
|
+
} from '@wallaby-cash/cryptography';
|
|
36
|
+
|
|
37
|
+
// Create a new identity
|
|
38
|
+
const identity = createIdentity();
|
|
39
|
+
console.log('Public Key:', identity.publicKey);
|
|
40
|
+
console.log('Private Key:', identity.privateKey);
|
|
41
|
+
|
|
42
|
+
// Sign a message
|
|
43
|
+
const message = 'Hello, Wallaby!';
|
|
44
|
+
const signature = sign(identity.privateKey, message);
|
|
45
|
+
|
|
46
|
+
// Hash data
|
|
47
|
+
const hashed = hash.keccak256('some data');
|
|
48
|
+
|
|
49
|
+
// Encrypt and decrypt
|
|
50
|
+
const encrypted = await encryptWithPublicKey(identity.publicKey, 'secret message');
|
|
51
|
+
const decrypted = await decryptWithPrivateKey(identity.privateKey, encrypted);
|
|
52
|
+
|
|
53
|
+
// Derive public key from private key
|
|
54
|
+
const pubKey = publicKeyByPrivateKey(identity.privateKey);
|
|
55
|
+
|
|
56
|
+
// Recover public key from signature
|
|
57
|
+
const recoveredPubKey = recoverPublicKey(signature, message);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API Reference
|
|
61
|
+
|
|
62
|
+
### `createIdentity()`
|
|
63
|
+
Generates a new cryptographic identity with a public/private key pair.
|
|
64
|
+
|
|
65
|
+
**Returns:** `{ publicKey: string, privateKey: string }`
|
|
66
|
+
|
|
67
|
+
### `sign(privateKey: string, message: string)`
|
|
68
|
+
Signs a message with a private key.
|
|
69
|
+
|
|
70
|
+
**Parameters:**
|
|
71
|
+
- `privateKey`: The private key to sign with
|
|
72
|
+
- `message`: The message to sign
|
|
73
|
+
|
|
74
|
+
**Returns:** Signature object
|
|
75
|
+
|
|
76
|
+
### `encryptWithPublicKey(publicKey: string, data: string)`
|
|
77
|
+
Encrypts data using a public key.
|
|
78
|
+
|
|
79
|
+
**Parameters:**
|
|
80
|
+
- `publicKey`: The recipient's public key
|
|
81
|
+
- `data`: The data to encrypt
|
|
82
|
+
|
|
83
|
+
**Returns:** `Promise<EncryptedData>`
|
|
84
|
+
|
|
85
|
+
### `decryptWithPrivateKey(privateKey: string, encrypted: EncryptedData)`
|
|
86
|
+
Decrypts data using a private key.
|
|
87
|
+
|
|
88
|
+
**Parameters:**
|
|
89
|
+
- `privateKey`: The private key to decrypt with
|
|
90
|
+
- `encrypted`: The encrypted data object
|
|
91
|
+
|
|
92
|
+
**Returns:** `Promise<string>`
|
|
93
|
+
|
|
94
|
+
### `hash.keccak256(data: string)`
|
|
95
|
+
Hashes data using Keccak256.
|
|
96
|
+
|
|
97
|
+
**Parameters:**
|
|
98
|
+
- `data`: The data to hash
|
|
99
|
+
|
|
100
|
+
**Returns:** Hash string
|
|
101
|
+
|
|
102
|
+
### `publicKeyByPrivateKey(privateKey: string)`
|
|
103
|
+
Derives a public key from a private key.
|
|
104
|
+
|
|
105
|
+
**Parameters:**
|
|
106
|
+
- `privateKey`: The private key
|
|
107
|
+
|
|
108
|
+
**Returns:** `string` - The derived public key
|
|
109
|
+
|
|
110
|
+
### `recoverPublicKey(signature: Signature, message: string)`
|
|
111
|
+
Recovers the public key from a signature and message.
|
|
112
|
+
|
|
113
|
+
**Parameters:**
|
|
114
|
+
- `signature`: The signature object
|
|
115
|
+
- `message`: The original message
|
|
116
|
+
|
|
117
|
+
**Returns:** `string` - The recovered public key
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wallaby-cash/cryptography",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cryptography utilities for Wallaby Cash",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./*": "./src/*.ts"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cryptography",
|
|
16
|
+
"encryption",
|
|
17
|
+
"ethereum",
|
|
18
|
+
"secp256k1",
|
|
19
|
+
"wallaby"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/wallaby-cash/sdk-monorepo.git",
|
|
25
|
+
"directory": "packages/cryptography"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"lint": "eslint . --max-warnings 0",
|
|
29
|
+
"generate:component": "turbo gen react-component",
|
|
30
|
+
"check-types": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@wallaby-cash/eslint-config": "*",
|
|
34
|
+
"@wallaby-cash/typescript-config": "*",
|
|
35
|
+
"@types/node": "^22.15.3",
|
|
36
|
+
"@types/react": "19.2.2",
|
|
37
|
+
"@types/react-dom": "19.2.2",
|
|
38
|
+
"eslint": "^9.39.1",
|
|
39
|
+
"typescript": "5.9.2"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"ethereum-cryptography": "^3.2.0",
|
|
43
|
+
"react": "^19.2.0",
|
|
44
|
+
"react-dom": "^19.2.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getRandomBytesSync as randomBytes } from 'ethereum-cryptography/random.js';
|
|
2
|
+
import { addLeading0x, stripHexPrefix, concatUint8Arrays } from './util';
|
|
3
|
+
import { publicKeyByPrivateKey } from './publicKeyByPrivateKey';
|
|
4
|
+
import { bytesToHex } from 'ethereum-cryptography/utils';
|
|
5
|
+
import { keccak256 } from 'ethereum-cryptography/keccak';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_ENTROPY_BYTES = 32;
|
|
8
|
+
export const MINIMUM_SHANNON_ENTROPY = 4;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* creates a new private key
|
|
12
|
+
* @param { Uint8Array } entropy - optional entropy to create the private key
|
|
13
|
+
* @returns a new private key
|
|
14
|
+
*/
|
|
15
|
+
export const createPrivateKey = (entropy?: Uint8Array) => {
|
|
16
|
+
if (entropy) {
|
|
17
|
+
if (!(entropy instanceof Uint8Array) || entropy.length < DEFAULT_ENTROPY_BYTES) {
|
|
18
|
+
throw new Error(`entropy must be a Uint8Array of at least ${DEFAULT_ENTROPY_BYTES} bytes`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check byte diversity
|
|
22
|
+
const uniqueBytes = new Set(entropy);
|
|
23
|
+
if (uniqueBytes.size < Math.min(8, entropy.length / 4)) {
|
|
24
|
+
throw new Error(`entropy is too repetitive (only ${uniqueBytes.size} unique byte values)`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Estimate Shannon entropy
|
|
28
|
+
const byteCounts = new Uint32Array(256);
|
|
29
|
+
entropy.forEach((b) => byteCounts[b]!++);
|
|
30
|
+
const total = entropy.length;
|
|
31
|
+
let shannonEntropy = 0;
|
|
32
|
+
for (let i = 0; i < 256; i++) {
|
|
33
|
+
if (byteCounts[i]! > 0) {
|
|
34
|
+
const p = byteCounts[i]! / total;
|
|
35
|
+
shannonEntropy -= p * Math.log2(p);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (shannonEntropy < MINIMUM_SHANNON_ENTROPY) {
|
|
39
|
+
throw new Error(`entropy has low Shannon entropy (${shannonEntropy.toFixed(2)} bits/byte)`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const outerHex = keccak256(entropy);
|
|
43
|
+
return addLeading0x(bytesToHex(outerHex));
|
|
44
|
+
} else {
|
|
45
|
+
const innerHex = keccak256(concatUint8Arrays([randomBytes(32), randomBytes(32)]));
|
|
46
|
+
const middleHex = concatUint8Arrays([concatUint8Arrays([randomBytes(32), innerHex]), randomBytes(32)]);
|
|
47
|
+
const outerHex = keccak256(middleHex);
|
|
48
|
+
return addLeading0x(bytesToHex(outerHex));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* creates a new identity
|
|
54
|
+
* @param { Uint8Array } entropy - optional entropy to create the private key
|
|
55
|
+
* @returns a new pair of private and public key
|
|
56
|
+
*/
|
|
57
|
+
export const createIdentity = (entropy?: Uint8Array) => {
|
|
58
|
+
const privateKey = createPrivateKey(entropy);
|
|
59
|
+
|
|
60
|
+
const walletPublicKey = publicKeyByPrivateKey(privateKey);
|
|
61
|
+
const identity = {
|
|
62
|
+
privateKey: privateKey,
|
|
63
|
+
publicKey: stripHexPrefix(walletPublicKey),
|
|
64
|
+
};
|
|
65
|
+
return identity;
|
|
66
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { stripHexPrefix } from './util';
|
|
2
|
+
import { decrypt } from './encryption-utils';
|
|
3
|
+
import { Encrypted } from './types';
|
|
4
|
+
|
|
5
|
+
export const decryptWithPrivateKey = (privateKey: string, encrypted: Encrypted) => {
|
|
6
|
+
// remove '0x' from privateKey
|
|
7
|
+
const twoStripped = stripHexPrefix(privateKey);
|
|
8
|
+
|
|
9
|
+
return decrypt(twoStripped, encrypted);
|
|
10
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { decompress } from './util';
|
|
2
|
+
import { encrypt } from './encryption-utils';
|
|
3
|
+
import { EncryptionOptions } from './types';
|
|
4
|
+
|
|
5
|
+
export const encryptWithPublicKey = (publicKey: string, message: string, options?: EncryptionOptions) => {
|
|
6
|
+
// ensure its an uncompressed publicKey
|
|
7
|
+
const decompressedKey = decompress(publicKey);
|
|
8
|
+
|
|
9
|
+
// re-add the compression-flag
|
|
10
|
+
const pubString = '04' + decompressedKey;
|
|
11
|
+
return encrypt(pubString, message, options);
|
|
12
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { sha512 } from 'ethereum-cryptography/sha512.js';
|
|
2
|
+
import { secp256k1 } from 'ethereum-cryptography/secp256k1';
|
|
3
|
+
import { getRandomBytesSync as randomBytes } from 'ethereum-cryptography/random.js';
|
|
4
|
+
import { hexToBytes, bytesToHex, bytesToUtf8 } from 'ethereum-cryptography/utils.js';
|
|
5
|
+
import { encrypt as aesEncrypt, decrypt as aesDecrypt } from 'ethereum-cryptography/aes.js';
|
|
6
|
+
import { Encrypted, EncryptionOptions } from './types';
|
|
7
|
+
import { hmacSha256Sign } from './sign';
|
|
8
|
+
import { concatUint8Arrays, utf8ToBytes } from './util';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* See https://github.com/bitchan/eccrypto for the original implementation that eth-crypto used, it's ancient and not maintained.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Encrypts a message using the recipient's public key.
|
|
16
|
+
* @param {string} publicKeyTo - The recipient's public key.
|
|
17
|
+
* @param {string} msg - The message to encrypt.
|
|
18
|
+
* @returns {Encrypted} The encrypted message.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const encrypt = (publicKeyTo: string, msg: string, options?: EncryptionOptions): Encrypted => {
|
|
22
|
+
const ephemPrivateKey = options?.ephemPrivateKey ? hexToBytes(options.ephemPrivateKey) : randomBytes(32);
|
|
23
|
+
const iv = randomBytes(16);
|
|
24
|
+
|
|
25
|
+
const compressedPub = secp256k1.getPublicKey(ephemPrivateKey); // defaults to compressed format
|
|
26
|
+
const point = secp256k1.ProjectivePoint.fromHex(compressedPub); // decompress the pub into an EC point
|
|
27
|
+
const ephemPublicKey = point.toRawBytes(false); // Get uncompressed SEC1 format pub
|
|
28
|
+
const sharedSecret = secp256k1.getSharedSecret(ephemPrivateKey, hexToBytes(publicKeyTo), true).slice(1);
|
|
29
|
+
const hash = sha512(sharedSecret);
|
|
30
|
+
const encryptionKey = hash.subarray(0, 32);
|
|
31
|
+
const macKey = hash.subarray(32);
|
|
32
|
+
const message = utf8ToBytes(msg);
|
|
33
|
+
const data = aesEncrypt(message, encryptionKey, iv, 'aes-256-cbc');
|
|
34
|
+
const dataToMac = concatUint8Arrays([iv, ephemPublicKey, data]);
|
|
35
|
+
const mac = hmacSha256Sign(macKey, dataToMac);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
iv: bytesToHex(iv),
|
|
39
|
+
ephemPublicKey: bytesToHex(ephemPublicKey),
|
|
40
|
+
ciphertext: bytesToHex(data),
|
|
41
|
+
mac: bytesToHex(mac),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decrypts an encrypted message using the recipient's private key.
|
|
47
|
+
* @param {string} privateKey - The recipient's private key.
|
|
48
|
+
* @param {Encrypted} opts - The encrypted message.
|
|
49
|
+
* @returns {string} The decrypted message.
|
|
50
|
+
*/
|
|
51
|
+
export const decrypt = (privateKey: string, opts: Encrypted) => {
|
|
52
|
+
let sharedSecret: Uint8Array;
|
|
53
|
+
try {
|
|
54
|
+
sharedSecret = secp256k1.getSharedSecret(hexToBytes(privateKey), opts.ephemPublicKey, true).slice(1);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
throw new Error(`Invalid MAC: data integrity check failed: ${e}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hash = sha512(sharedSecret);
|
|
60
|
+
const encryptionKey = hash.subarray(0, 32);
|
|
61
|
+
const macKey = hash.subarray(32);
|
|
62
|
+
|
|
63
|
+
const ciphertext = hexToBytes(opts.ciphertext);
|
|
64
|
+
const iv = hexToBytes(opts.iv);
|
|
65
|
+
const ephemPublicKey = hexToBytes(opts.ephemPublicKey);
|
|
66
|
+
const receivedMac = hexToBytes(opts.mac);
|
|
67
|
+
|
|
68
|
+
// Recompute MAC
|
|
69
|
+
const dataToMac = concatUint8Arrays([iv, ephemPublicKey, ciphertext]);
|
|
70
|
+
const expectedMac = hmacSha256Sign(macKey, dataToMac);
|
|
71
|
+
|
|
72
|
+
if (!constantTimeEqual(expectedMac, receivedMac)) {
|
|
73
|
+
throw new Error('Invalid MAC: data integrity check failed');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const decrypted = aesDecrypt(ciphertext, encryptionKey, iv, 'aes-256-cbc');
|
|
77
|
+
return bytesToUtf8(decrypted);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Compares two Uint8Arrays in constant time to prevent timing attacks.
|
|
82
|
+
* @param {Uint8Array} a - The first array.
|
|
83
|
+
* @param {Uint8Array} b - The second array.
|
|
84
|
+
* @returns {boolean} True if the arrays are equal, false otherwise.
|
|
85
|
+
*/
|
|
86
|
+
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
87
|
+
if (a.length !== b.length) return false;
|
|
88
|
+
let result = 0;
|
|
89
|
+
for (let i = 0; i < a.length; i++) {
|
|
90
|
+
result |= a[i]! ^ b[i]!;
|
|
91
|
+
}
|
|
92
|
+
return result === 0;
|
|
93
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { bytesToHex, hexToBytes } from 'ethereum-cryptography/utils.js';
|
|
2
|
+
import { keccak256 as _keccak256 } from 'ethereum-cryptography/keccak.js';
|
|
3
|
+
import { addLeading0x, utf8ToBytes } from './util';
|
|
4
|
+
|
|
5
|
+
const solidityPackedKeccak256 = (value: string) => {
|
|
6
|
+
const bytes = utf8ToBytes(value);
|
|
7
|
+
const hex = addLeading0x(bytesToHex(bytes));
|
|
8
|
+
const hash = _keccak256(hexToBytes(hex));
|
|
9
|
+
return addLeading0x(bytesToHex(hash));
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const keccak256 = (params: string) => {
|
|
13
|
+
return solidityPackedKeccak256(params);
|
|
14
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createIdentity } from './createIdentity';
|
|
2
|
+
import { sign } from './sign';
|
|
3
|
+
import { encryptWithPublicKey } from './encryptWithPublicKey';
|
|
4
|
+
import { decryptWithPrivateKey } from './decryptWithPrivateKey';
|
|
5
|
+
import { keccak256 } from './hash';
|
|
6
|
+
import { publicKeyByPrivateKey } from './publicKeyByPrivateKey';
|
|
7
|
+
import { recoverPublicKey } from './recoverPublicKey';
|
|
8
|
+
|
|
9
|
+
const hash = {
|
|
10
|
+
keccak256,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
createIdentity,
|
|
15
|
+
decryptWithPrivateKey,
|
|
16
|
+
encryptWithPublicKey,
|
|
17
|
+
hash,
|
|
18
|
+
keccak256,
|
|
19
|
+
publicKeyByPrivateKey,
|
|
20
|
+
recoverPublicKey,
|
|
21
|
+
sign,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default {
|
|
25
|
+
createIdentity,
|
|
26
|
+
decryptWithPrivateKey,
|
|
27
|
+
encryptWithPublicKey,
|
|
28
|
+
hash,
|
|
29
|
+
keccak256,
|
|
30
|
+
publicKeyByPrivateKey,
|
|
31
|
+
recoverPublicKey,
|
|
32
|
+
sign,
|
|
33
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { decompress, stripHexPrefix } from './util';
|
|
2
|
+
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js';
|
|
3
|
+
import { bytesToHex } from 'ethereum-cryptography/utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate publicKey from the privateKey.
|
|
7
|
+
* This creates the uncompressed publicKey,
|
|
8
|
+
* where 04 has stripped from left
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const publicKeyByPrivateKey = (privateKey: string) => {
|
|
13
|
+
const key = stripHexPrefix(privateKey);
|
|
14
|
+
const compressedPub = secp256k1.getPublicKey(key); // defaults to compressed format
|
|
15
|
+
const point = secp256k1.ProjectivePoint.fromHex(compressedPub); // decompress the pub into an EC point
|
|
16
|
+
return decompress(bytesToHex(point.toRawBytes(false))); // Get uncompressed SEC1 format pub
|
|
17
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ecdsaRecover } from 'ethereum-cryptography/secp256k1-compat';
|
|
2
|
+
import { stripHexPrefix } from './util';
|
|
3
|
+
import { bytesToHex, hexToBytes } from 'ethereum-cryptography/utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* returns the publicKey for the privateKey with which the messageHash was signed
|
|
7
|
+
* @param {string} signature
|
|
8
|
+
* @param {string} hash
|
|
9
|
+
* @return {string} publicKey
|
|
10
|
+
*/
|
|
11
|
+
export const recoverPublicKey = (signature: string, hash: string) => {
|
|
12
|
+
const noHex = stripHexPrefix(signature);
|
|
13
|
+
|
|
14
|
+
// split into v-value and sig
|
|
15
|
+
const sigOnly = noHex.substring(0, noHex.length - 2); // all but last 2 chars
|
|
16
|
+
const vValue = noHex.slice(-2); // last 2 chars
|
|
17
|
+
|
|
18
|
+
const recoveryNumber = vValue === '1c' ? 1 : 0;
|
|
19
|
+
|
|
20
|
+
let pubKey = bytesToHex(ecdsaRecover(hexToBytes(sigOnly), recoveryNumber, hexToBytes(stripHexPrefix(hash)), false));
|
|
21
|
+
|
|
22
|
+
// remove trailing '04'
|
|
23
|
+
pubKey = pubKey.slice(2);
|
|
24
|
+
|
|
25
|
+
return pubKey;
|
|
26
|
+
};
|
package/src/sign.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js';
|
|
2
|
+
import { hmac } from '@noble/hashes/hmac';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
4
|
+
import { hexToBytes } from 'ethereum-cryptography/utils';
|
|
5
|
+
import { addLeading0x, isHexString, stripHexPrefix } from './util';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* signs the given message
|
|
9
|
+
* @param {string} privateKey
|
|
10
|
+
* @param {string} hash
|
|
11
|
+
* @return {string} hexString
|
|
12
|
+
*/
|
|
13
|
+
export const sign = (privateKey: string, hash: string) => {
|
|
14
|
+
const hashWith0x = addLeading0x(hash);
|
|
15
|
+
if (hashWith0x.length !== 66 || !isHexString(hashWith0x)) throw new Error('Can only sign hashes, given: ' + hash);
|
|
16
|
+
|
|
17
|
+
const sigObj = secp256k1.sign(hexToBytes(stripHexPrefix(hash)), hexToBytes(stripHexPrefix(privateKey)));
|
|
18
|
+
|
|
19
|
+
const recoveryId = sigObj.recovery === 1 ? '1c' : '1b';
|
|
20
|
+
const newSignature = '0x' + sigObj.toCompactHex() + recoveryId;
|
|
21
|
+
return newSignature;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const hmacSha256Sign = (key: Uint8Array, msg: Uint8Array) => {
|
|
25
|
+
const result = hmac(sha256, key, msg);
|
|
26
|
+
return result;
|
|
27
|
+
};
|
package/src/types.ts
ADDED
package/src/util.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { hexToBytes } from 'ethereum-cryptography/utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a `Boolean` on whether or not the a `String` starts with '0x'
|
|
5
|
+
* @param str the string input value
|
|
6
|
+
* @return a boolean if it is or is not hex prefixed
|
|
7
|
+
* @throws if the str input is not a string
|
|
8
|
+
*/
|
|
9
|
+
export const isHexPrefixed = (str: string) => {
|
|
10
|
+
if (typeof str !== 'string') {
|
|
11
|
+
throw new Error(`[isHexPrefixed] input must be type 'string', received type ${typeof str}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return str[0] === '0' && str[1] === 'x';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Removes '0x' from a given `String` if present
|
|
19
|
+
* @param str the string value
|
|
20
|
+
* @returns the string without 0x prefix
|
|
21
|
+
*/
|
|
22
|
+
export const stripHexPrefix = (str: string): string => {
|
|
23
|
+
if (typeof str !== 'string') throw new Error(`[stripHexPrefix] input must be type 'string', received ${typeof str}`);
|
|
24
|
+
|
|
25
|
+
return isHexPrefixed(str) ? str.slice(2) : str;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Is the string a hex string.
|
|
30
|
+
*
|
|
31
|
+
* @param value
|
|
32
|
+
* @param length
|
|
33
|
+
* @returns output the string is a hex string
|
|
34
|
+
*/
|
|
35
|
+
export const isHexString = (value: string, length?: number): boolean => {
|
|
36
|
+
if (typeof value !== 'string' || !value.match(/^0x[0-9A-Fa-f]+$/)) return false;
|
|
37
|
+
|
|
38
|
+
if (length && value.length !== 2 + 2 * length) return false;
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Adds '0x' to a given `String` if not present
|
|
45
|
+
* @param str the string input value
|
|
46
|
+
* @return the string with a 0x prefix
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export const addLeading0x = (str: string) => {
|
|
50
|
+
if (!str.startsWith('0x')) return '0x' + str;
|
|
51
|
+
else return str;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const decompress = (startsWith02Or03: string) => {
|
|
55
|
+
const testByteArray = hexToBytes(startsWith02Or03);
|
|
56
|
+
let startsWith04 = startsWith02Or03;
|
|
57
|
+
if (testByteArray.length === 64) {
|
|
58
|
+
startsWith04 = '04' + startsWith02Or03;
|
|
59
|
+
}
|
|
60
|
+
return startsWith04.substring(2);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Helper function to concat UInt8Arrays mimicking the behaviour of the
|
|
64
|
+
* Buffer.concat function in Node.js
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
export const concatUint8Arrays = (uint8arrays: Uint8Array[]) => {
|
|
68
|
+
const totalLength = uint8arrays.reduce((total, uint8array) => total + uint8array.byteLength, 0);
|
|
69
|
+
const result = new Uint8Array(totalLength);
|
|
70
|
+
let offset = 0;
|
|
71
|
+
uint8arrays.forEach((uint8array) => {
|
|
72
|
+
result.set(uint8array, offset);
|
|
73
|
+
offset += uint8array.byteLength;
|
|
74
|
+
});
|
|
75
|
+
return result;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Converts a UTF-8 string to a Uint8Array without using TextEncoder, which is not available in mobile
|
|
80
|
+
* @example utf8ToBytes('abc') // new Uint8Array([97, 98, 99])
|
|
81
|
+
*/
|
|
82
|
+
export const utf8ToBytes = (str: string): Uint8Array => {
|
|
83
|
+
if (typeof str !== 'string') throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
|
|
84
|
+
const bytes = [];
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < str.length; i++) {
|
|
87
|
+
const codePoint = str.codePointAt(i);
|
|
88
|
+
|
|
89
|
+
if (!codePoint) {
|
|
90
|
+
throw new Error('Invalid code point');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (codePoint < 0x80) {
|
|
94
|
+
bytes.push(codePoint);
|
|
95
|
+
} else if (codePoint < 0x800) {
|
|
96
|
+
bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
|
|
97
|
+
} else if (codePoint < 0x10000) {
|
|
98
|
+
bytes.push(0xe0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3f), 0x80 | (codePoint & 0x3f));
|
|
99
|
+
} else {
|
|
100
|
+
i++; // skip one iteration since we have a surrogate pair
|
|
101
|
+
bytes.push(
|
|
102
|
+
0xf0 | (codePoint >> 18),
|
|
103
|
+
0x80 | ((codePoint >> 12) & 0x3f),
|
|
104
|
+
0x80 | ((codePoint >> 6) & 0x3f),
|
|
105
|
+
0x80 | (codePoint & 0x3f),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return new Uint8Array(bytes);
|
|
110
|
+
};
|