@atproto/crypto 0.0.1
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/README.md +3 -0
- package/build.js +22 -0
- package/dist/aes.d.ts +8 -0
- package/dist/const.d.ts +5 -0
- package/dist/did.d.ts +7 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +3955 -0
- package/dist/index.js.map +7 -0
- package/dist/multibase.d.ts +1 -0
- package/dist/p256/encoding.d.ts +2 -0
- package/dist/p256/keypair.d.ts +19 -0
- package/dist/p256/operations.d.ts +4 -0
- package/dist/p256/plugin.d.ts +3 -0
- package/dist/plugins.d.ts +2 -0
- package/dist/random.d.ts +4 -0
- package/dist/secp256k1/encoding.d.ts +2 -0
- package/dist/secp256k1/keypair.d.ts +19 -0
- package/dist/secp256k1/operations.d.ts +1 -0
- package/dist/secp256k1/plugin.d.ts +3 -0
- package/dist/sha.d.ts +3 -0
- package/dist/verify.d.ts +1 -0
- package/jest.config.js +6 -0
- package/package.json +29 -0
- package/src/aes.ts +64 -0
- package/src/const.ts +6 -0
- package/src/did.ts +56 -0
- package/src/index.ts +15 -0
- package/src/multibase.ts +26 -0
- package/src/p256/encoding.ts +81 -0
- package/src/p256/keypair.ts +85 -0
- package/src/p256/operations.ts +64 -0
- package/src/p256/plugin.ts +13 -0
- package/src/plugins.ts +6 -0
- package/src/random.ts +19 -0
- package/src/secp256k1/encoding.ts +16 -0
- package/src/secp256k1/keypair.ts +65 -0
- package/src/secp256k1/operations.ts +16 -0
- package/src/secp256k1/plugin.ts +11 -0
- package/src/sha.ts +28 -0
- package/src/verify.ts +15 -0
- package/tests/did.test.ts +95 -0
- package/tests/export.test.ts +50 -0
- package/tests/key-compression.test.ts +69 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +9 -0
- package/update-pkg.js +14 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as ucan from '@ucans/core';
|
|
2
|
+
export declare type EcdsaKeypairOptions = {
|
|
3
|
+
exportable: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare class EcdsaKeypair implements ucan.DidableKey {
|
|
6
|
+
jwtAlg: string;
|
|
7
|
+
private publicKey;
|
|
8
|
+
private keypair;
|
|
9
|
+
private exportable;
|
|
10
|
+
constructor(keypair: CryptoKeyPair, publicKey: Uint8Array, exportable: boolean);
|
|
11
|
+
static create(opts?: Partial<EcdsaKeypairOptions>): Promise<EcdsaKeypair>;
|
|
12
|
+
static import(jwk: JsonWebKey, opts?: Partial<EcdsaKeypairOptions>): Promise<EcdsaKeypair>;
|
|
13
|
+
publicKeyBytes(): Uint8Array;
|
|
14
|
+
publicKeyStr(encoding?: ucan.Encodings): string;
|
|
15
|
+
did(): string;
|
|
16
|
+
sign(msg: Uint8Array): Promise<Uint8Array>;
|
|
17
|
+
export(): Promise<JsonWebKey>;
|
|
18
|
+
}
|
|
19
|
+
export default EcdsaKeypair;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const importKeypairJwk: (jwk: JsonWebKey, exportable?: boolean) => Promise<CryptoKeyPair>;
|
|
2
|
+
export declare const verifyDidSig: (did: string, data: Uint8Array, sig: Uint8Array) => Promise<boolean>;
|
|
3
|
+
export declare const verify: (publicKey: Uint8Array, data: Uint8Array, sig: Uint8Array) => Promise<boolean>;
|
|
4
|
+
export declare const importEcdsaPublicKey: (keyBytes: Uint8Array) => Promise<CryptoKey>;
|
package/dist/random.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { SupportedEncodings } from 'uint8arrays/to-string';
|
|
2
|
+
export declare const randomBytes: (length: number) => Uint8Array;
|
|
3
|
+
export declare const randomIV: () => Uint8Array;
|
|
4
|
+
export declare const randomStr: (byteLength: number, encoding: SupportedEncodings) => string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as ucan from '@ucans/core';
|
|
2
|
+
export declare type Secp256k1KeypairOptions = {
|
|
3
|
+
exportable: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare class Secp256k1Keypair implements ucan.DidableKey {
|
|
6
|
+
private privateKey;
|
|
7
|
+
private exportable;
|
|
8
|
+
jwtAlg: string;
|
|
9
|
+
private publicKey;
|
|
10
|
+
constructor(privateKey: Uint8Array, exportable: boolean);
|
|
11
|
+
static create(opts?: Partial<Secp256k1KeypairOptions>): Promise<Secp256k1Keypair>;
|
|
12
|
+
static import(privKey: Uint8Array | string, opts?: Partial<Secp256k1KeypairOptions>): Promise<Secp256k1Keypair>;
|
|
13
|
+
publicKeyBytes(): Uint8Array;
|
|
14
|
+
publicKeyStr(encoding?: ucan.Encodings): string;
|
|
15
|
+
did(): string;
|
|
16
|
+
sign(msg: Uint8Array): Promise<Uint8Array>;
|
|
17
|
+
export(): Promise<Uint8Array>;
|
|
18
|
+
}
|
|
19
|
+
export default Secp256k1Keypair;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const verifyDidSig: (did: string, data: Uint8Array, sig: Uint8Array) => Promise<boolean>;
|
package/dist/sha.d.ts
ADDED
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const verifyDidSig: (did: string, data: Uint8Array, sig: Uint8Array) => Promise<boolean>;
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atproto/crypto",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest ",
|
|
8
|
+
"prettier": "prettier --check src/",
|
|
9
|
+
"prettier:fix": "prettier --write src/",
|
|
10
|
+
"lint": "eslint . --ext .ts,.tsx",
|
|
11
|
+
"lint:fix": "yarn lint --fix",
|
|
12
|
+
"verify": "run-p prettier lint",
|
|
13
|
+
"verify:fix": "yarn prettier:fix && yarn lint:fix",
|
|
14
|
+
"build": "node ./build.js",
|
|
15
|
+
"postbuild": "tsc --build tsconfig.build.json",
|
|
16
|
+
"update-main-to-dist": "node ./update-pkg.js --update-main-to-dist",
|
|
17
|
+
"update-main-to-src": "node ./update-pkg.js --update-main-to-src",
|
|
18
|
+
"prepublish": "npm run update-main-to-dist",
|
|
19
|
+
"postpublish": "npm run update-main-to-src"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@noble/secp256k1": "^1.7.0",
|
|
23
|
+
"@ucans/core": "0.11.0",
|
|
24
|
+
"big-integer": "^1.6.51",
|
|
25
|
+
"multiformats": "^9.6.4",
|
|
26
|
+
"one-webcrypto": "^1.0.3",
|
|
27
|
+
"uint8arrays": "3.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/aes.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { webcrypto } from 'one-webcrypto'
|
|
2
|
+
import * as uint8arrays from 'uint8arrays'
|
|
3
|
+
|
|
4
|
+
import * as random from './random'
|
|
5
|
+
|
|
6
|
+
export class AesKey {
|
|
7
|
+
private key: CryptoKey
|
|
8
|
+
constructor(key: CryptoKey) {
|
|
9
|
+
this.key = key
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static async create(): Promise<AesKey> {
|
|
13
|
+
const key = await webcrypto.subtle.generateKey(
|
|
14
|
+
{
|
|
15
|
+
name: 'AES-GCM',
|
|
16
|
+
length: 256,
|
|
17
|
+
},
|
|
18
|
+
true,
|
|
19
|
+
['encrypt', 'decrypt'],
|
|
20
|
+
)
|
|
21
|
+
return new AesKey(key)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// utf8 data -> base64pad cipher
|
|
25
|
+
// returns base64 encrypted data with iv prepended
|
|
26
|
+
async encrypt(data: string): Promise<string> {
|
|
27
|
+
const iv = random.randomIV()
|
|
28
|
+
const dataBytes = uint8arrays.fromString(data, 'utf8')
|
|
29
|
+
const buf = await webcrypto.subtle.encrypt(
|
|
30
|
+
{
|
|
31
|
+
name: 'AES-GCM',
|
|
32
|
+
iv,
|
|
33
|
+
},
|
|
34
|
+
this.key,
|
|
35
|
+
dataBytes,
|
|
36
|
+
)
|
|
37
|
+
const encryptedBytes = new Uint8Array(buf)
|
|
38
|
+
const encrypted = uint8arrays.toString(
|
|
39
|
+
uint8arrays.concat([iv, encryptedBytes]),
|
|
40
|
+
'base64pad',
|
|
41
|
+
)
|
|
42
|
+
return encrypted
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// base64pad cipher -> utf8 data
|
|
46
|
+
// expects base64 encrypted data with iv prepended
|
|
47
|
+
async decrypt(data: string): Promise<string> {
|
|
48
|
+
const dataBytes = uint8arrays.fromString(data, 'base64pad')
|
|
49
|
+
const iv = dataBytes.slice(0, 12)
|
|
50
|
+
const encrypted = dataBytes.slice(12)
|
|
51
|
+
const buf = await webcrypto.subtle.decrypt(
|
|
52
|
+
{
|
|
53
|
+
name: 'AES-GCM',
|
|
54
|
+
iv,
|
|
55
|
+
},
|
|
56
|
+
this.key,
|
|
57
|
+
encrypted,
|
|
58
|
+
)
|
|
59
|
+
const decryptedBytes = new Uint8Array(buf)
|
|
60
|
+
return uint8arrays.toString(decryptedBytes, 'utf8')
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default AesKey
|
package/src/const.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const P256_DID_PREFIX = new Uint8Array([0x80, 0x24])
|
|
2
|
+
export const SECP256K1_DID_PREFIX = new Uint8Array([0xe7, 0x01])
|
|
3
|
+
export const BASE58_DID_PREFIX = 'did:key:z' // z is the multibase prefix for base58btc byte encoding
|
|
4
|
+
|
|
5
|
+
export const P256_JWT_ALG = 'ES256'
|
|
6
|
+
export const SECP256K1_JWT_ALG = 'ES256K'
|
package/src/did.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as uint8arrays from 'uint8arrays'
|
|
2
|
+
import * as p256 from './p256/encoding'
|
|
3
|
+
import * as secp from './secp256k1/encoding'
|
|
4
|
+
import plugins from './plugins'
|
|
5
|
+
import { P256_JWT_ALG, SECP256K1_JWT_ALG } from './const'
|
|
6
|
+
|
|
7
|
+
export const DID_KEY_BASE58_PREFIX = 'did:key:z'
|
|
8
|
+
|
|
9
|
+
export type ParsedDidKey = {
|
|
10
|
+
jwtAlg: string
|
|
11
|
+
keyBytes: Uint8Array
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const parseDidKey = (did: string): ParsedDidKey => {
|
|
15
|
+
if (!did.startsWith(DID_KEY_BASE58_PREFIX)) {
|
|
16
|
+
throw new Error(`Incorrect prefix for did:key: ${did}`)
|
|
17
|
+
}
|
|
18
|
+
const prefixedBytes = uint8arrays.fromString(
|
|
19
|
+
did.slice(DID_KEY_BASE58_PREFIX.length),
|
|
20
|
+
'base58btc',
|
|
21
|
+
)
|
|
22
|
+
const plugin = plugins.find((p) => hasPrefix(prefixedBytes, p.prefix))
|
|
23
|
+
if (!plugin) {
|
|
24
|
+
throw new Error('Unsupported key type')
|
|
25
|
+
}
|
|
26
|
+
let keyBytes = prefixedBytes.slice(plugin.prefix.length)
|
|
27
|
+
if (plugin.jwtAlg === P256_JWT_ALG) {
|
|
28
|
+
keyBytes = p256.decompressPubkey(keyBytes)
|
|
29
|
+
} else if (plugin.jwtAlg === SECP256K1_JWT_ALG) {
|
|
30
|
+
keyBytes = secp.decompressPubkey(keyBytes)
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
jwtAlg: plugin.jwtAlg,
|
|
34
|
+
keyBytes,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const formatDidKey = (jwtAlg: string, keyBytes: Uint8Array): string => {
|
|
39
|
+
const plugin = plugins.find((p) => p.jwtAlg === jwtAlg)
|
|
40
|
+
if (!plugin) {
|
|
41
|
+
throw new Error('Unsupported key type')
|
|
42
|
+
}
|
|
43
|
+
if (jwtAlg === P256_JWT_ALG) {
|
|
44
|
+
keyBytes = p256.compressPubkey(keyBytes)
|
|
45
|
+
} else if (jwtAlg === SECP256K1_JWT_ALG) {
|
|
46
|
+
keyBytes = secp.compressPubkey(keyBytes)
|
|
47
|
+
}
|
|
48
|
+
const prefixedBytes = uint8arrays.concat([plugin.prefix, keyBytes])
|
|
49
|
+
return (
|
|
50
|
+
DID_KEY_BASE58_PREFIX + uint8arrays.toString(prefixedBytes, 'base58btc')
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hasPrefix = (bytes: Uint8Array, prefix: Uint8Array): boolean => {
|
|
55
|
+
return uint8arrays.equals(prefix, bytes.subarray(0, prefix.byteLength))
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './aes'
|
|
2
|
+
export * from './const'
|
|
3
|
+
export * from './did'
|
|
4
|
+
export * from './multibase'
|
|
5
|
+
export * from './random'
|
|
6
|
+
export * from './sha'
|
|
7
|
+
export * from './verify'
|
|
8
|
+
|
|
9
|
+
export * from './p256/keypair'
|
|
10
|
+
export * from './p256/plugin'
|
|
11
|
+
|
|
12
|
+
export * from './secp256k1/keypair'
|
|
13
|
+
export * from './secp256k1/plugin'
|
|
14
|
+
|
|
15
|
+
export type { DidableKey } from '@ucans/core'
|
package/src/multibase.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as uint8arrays from 'uint8arrays'
|
|
2
|
+
|
|
3
|
+
export const multibaseToBytes = (mb: string): Uint8Array => {
|
|
4
|
+
const base = mb[0]
|
|
5
|
+
const key = mb.slice(1)
|
|
6
|
+
switch (base) {
|
|
7
|
+
case 'f':
|
|
8
|
+
return uint8arrays.fromString(key, 'base16')
|
|
9
|
+
case 'F':
|
|
10
|
+
return uint8arrays.fromString(key, 'base16upper')
|
|
11
|
+
case 'b':
|
|
12
|
+
return uint8arrays.fromString(key, 'base32')
|
|
13
|
+
case 'B':
|
|
14
|
+
return uint8arrays.fromString(key, 'base32upper')
|
|
15
|
+
case 'z':
|
|
16
|
+
return uint8arrays.fromString(key, 'base58btc')
|
|
17
|
+
case 'm':
|
|
18
|
+
return uint8arrays.fromString(key, 'base64')
|
|
19
|
+
case 'u':
|
|
20
|
+
return uint8arrays.fromString(key, 'base64url')
|
|
21
|
+
case 'U':
|
|
22
|
+
return uint8arrays.fromString(key, 'base64urlpad')
|
|
23
|
+
default:
|
|
24
|
+
throw new Error(`Unsupported multibase: :${mb}`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import bigInt from 'big-integer'
|
|
2
|
+
import * as uint8arrays from 'uint8arrays'
|
|
3
|
+
|
|
4
|
+
// PUBLIC KEY COMPRESSION
|
|
5
|
+
// ------------------------
|
|
6
|
+
|
|
7
|
+
// Compression & Decompression algos from:
|
|
8
|
+
// https://stackoverflow.com/questions/48521840/biginteger-to-a-uint8array-of-bytes
|
|
9
|
+
|
|
10
|
+
// Public key compression for NIST P-256
|
|
11
|
+
export const compressPubkey = (pubkeyBytes: Uint8Array): Uint8Array => {
|
|
12
|
+
if (pubkeyBytes.length !== 65) {
|
|
13
|
+
throw new Error('Expected 65 byte pubkey')
|
|
14
|
+
} else if (pubkeyBytes[0] !== 0x04) {
|
|
15
|
+
throw new Error('Expected first byte to be 0x04')
|
|
16
|
+
}
|
|
17
|
+
// first byte is a prefix
|
|
18
|
+
const x = pubkeyBytes.slice(1, 33)
|
|
19
|
+
const y = pubkeyBytes.slice(33, 65)
|
|
20
|
+
const out = new Uint8Array(x.length + 1)
|
|
21
|
+
|
|
22
|
+
out[0] = 2 + (y[y.length - 1] & 1)
|
|
23
|
+
out.set(x, 1)
|
|
24
|
+
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Public key decompression for NIST P-256
|
|
29
|
+
export const decompressPubkey = (compressed: Uint8Array): Uint8Array => {
|
|
30
|
+
if (compressed.length !== 33) {
|
|
31
|
+
throw new Error('Expected 33 byte compress pubkey')
|
|
32
|
+
} else if (compressed[0] !== 0x02 && compressed[0] !== 0x03) {
|
|
33
|
+
throw new Error('Expected first byte to be 0x02 or 0x03')
|
|
34
|
+
}
|
|
35
|
+
// Consts for P256 curve
|
|
36
|
+
const two = bigInt(2)
|
|
37
|
+
// 115792089210356248762697446949407573530086143415290314195533631308867097853951
|
|
38
|
+
const prime = two
|
|
39
|
+
.pow(256)
|
|
40
|
+
.subtract(two.pow(224))
|
|
41
|
+
.add(two.pow(192))
|
|
42
|
+
.add(two.pow(96))
|
|
43
|
+
.subtract(1)
|
|
44
|
+
const b = bigInt(
|
|
45
|
+
'41058363725152142129326129780047268409114441015993725554835256314039467401291',
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Pre-computed value, or literal
|
|
49
|
+
const pIdent = prime.add(1).divide(4) // 28948022302589062190674361737351893382521535853822578548883407827216774463488
|
|
50
|
+
|
|
51
|
+
// This value must be 2 or 3. 4 indicates an uncompressed key, and anything else is invalid.
|
|
52
|
+
const signY = bigInt(compressed[0] - 2)
|
|
53
|
+
const x = compressed.slice(1)
|
|
54
|
+
const xBig = bigInt(uint8arrays.toString(x, 'base10'))
|
|
55
|
+
|
|
56
|
+
// y^2 = x^3 - 3x + b
|
|
57
|
+
const maybeY = xBig
|
|
58
|
+
.pow(3)
|
|
59
|
+
.subtract(xBig.multiply(3))
|
|
60
|
+
.add(b)
|
|
61
|
+
.modPow(pIdent, prime)
|
|
62
|
+
|
|
63
|
+
let yBig
|
|
64
|
+
// If the parity matches, we found our root, otherwise it's the other root
|
|
65
|
+
if (maybeY.mod(2).equals(signY)) {
|
|
66
|
+
yBig = maybeY
|
|
67
|
+
} else {
|
|
68
|
+
// y = prime - y
|
|
69
|
+
yBig = prime.subtract(maybeY)
|
|
70
|
+
}
|
|
71
|
+
const y = uint8arrays.fromString(yBig.toString(10), 'base10')
|
|
72
|
+
|
|
73
|
+
// left-pad for smaller than 32 byte y
|
|
74
|
+
const offset = 32 - y.length
|
|
75
|
+
const yPadded = new Uint8Array(32)
|
|
76
|
+
yPadded.set(y, offset)
|
|
77
|
+
|
|
78
|
+
// concat coords & prepend P-256 prefix
|
|
79
|
+
const publicKey = uint8arrays.concat([[0x04], x, yPadded])
|
|
80
|
+
return publicKey
|
|
81
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { webcrypto } from 'one-webcrypto'
|
|
2
|
+
import * as uint8arrays from 'uint8arrays'
|
|
3
|
+
|
|
4
|
+
import * as ucan from '@ucans/core'
|
|
5
|
+
|
|
6
|
+
import * as did from '../did'
|
|
7
|
+
import * as operations from './operations'
|
|
8
|
+
import { P256_JWT_ALG } from '../const'
|
|
9
|
+
|
|
10
|
+
export type EcdsaKeypairOptions = {
|
|
11
|
+
exportable: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class EcdsaKeypair implements ucan.DidableKey {
|
|
15
|
+
jwtAlg = P256_JWT_ALG
|
|
16
|
+
private publicKey: Uint8Array
|
|
17
|
+
private keypair: CryptoKeyPair
|
|
18
|
+
private exportable: boolean
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
keypair: CryptoKeyPair,
|
|
22
|
+
publicKey: Uint8Array,
|
|
23
|
+
exportable: boolean,
|
|
24
|
+
) {
|
|
25
|
+
this.keypair = keypair
|
|
26
|
+
this.publicKey = publicKey
|
|
27
|
+
this.exportable = exportable
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static async create(
|
|
31
|
+
opts?: Partial<EcdsaKeypairOptions>,
|
|
32
|
+
): Promise<EcdsaKeypair> {
|
|
33
|
+
const { exportable = false } = opts || {}
|
|
34
|
+
const keypair = await webcrypto.subtle.generateKey(
|
|
35
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
36
|
+
exportable,
|
|
37
|
+
['sign', 'verify'],
|
|
38
|
+
)
|
|
39
|
+
const pubkeyBuf = await webcrypto.subtle.exportKey('raw', keypair.publicKey)
|
|
40
|
+
const pubkeyBytes = new Uint8Array(pubkeyBuf)
|
|
41
|
+
return new EcdsaKeypair(keypair, pubkeyBytes, exportable)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static async import(
|
|
45
|
+
jwk: JsonWebKey,
|
|
46
|
+
opts?: Partial<EcdsaKeypairOptions>,
|
|
47
|
+
): Promise<EcdsaKeypair> {
|
|
48
|
+
const { exportable = false } = opts || {}
|
|
49
|
+
const keypair = await operations.importKeypairJwk(jwk, exportable)
|
|
50
|
+
const pubkeyBuf = await webcrypto.subtle.exportKey('raw', keypair.publicKey)
|
|
51
|
+
const pubkeyBytes = new Uint8Array(pubkeyBuf)
|
|
52
|
+
return new EcdsaKeypair(keypair, pubkeyBytes, exportable)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
publicKeyBytes(): Uint8Array {
|
|
56
|
+
return this.publicKey
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
publicKeyStr(encoding: ucan.Encodings = 'base64pad'): string {
|
|
60
|
+
return uint8arrays.toString(this.publicKey, encoding)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
did(): string {
|
|
64
|
+
return did.formatDidKey(this.jwtAlg, this.publicKey)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async sign(msg: Uint8Array): Promise<Uint8Array> {
|
|
68
|
+
const buf = await webcrypto.subtle.sign(
|
|
69
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
70
|
+
this.keypair.privateKey,
|
|
71
|
+
msg.buffer,
|
|
72
|
+
)
|
|
73
|
+
return new Uint8Array(buf)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async export(): Promise<JsonWebKey> {
|
|
77
|
+
if (!this.exportable) {
|
|
78
|
+
throw new Error('Private key is not exportable')
|
|
79
|
+
}
|
|
80
|
+
const jwk = await webcrypto.subtle.exportKey('jwk', this.keypair.privateKey)
|
|
81
|
+
return jwk
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default EcdsaKeypair
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { webcrypto } from 'one-webcrypto'
|
|
2
|
+
import { P256_JWT_ALG } from '../const'
|
|
3
|
+
import { parseDidKey } from '../did'
|
|
4
|
+
|
|
5
|
+
export const importKeypairJwk = async (
|
|
6
|
+
jwk: JsonWebKey,
|
|
7
|
+
exportable = false,
|
|
8
|
+
): Promise<CryptoKeyPair> => {
|
|
9
|
+
const privateKey = await webcrypto.subtle.importKey(
|
|
10
|
+
'jwk',
|
|
11
|
+
jwk,
|
|
12
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
13
|
+
exportable,
|
|
14
|
+
['sign'],
|
|
15
|
+
)
|
|
16
|
+
const { kty, crv, x, y } = jwk
|
|
17
|
+
const pubKeyJwk = { kty, crv, x, y }
|
|
18
|
+
const publicKey = await webcrypto.subtle.importKey(
|
|
19
|
+
'jwk',
|
|
20
|
+
pubKeyJwk,
|
|
21
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
22
|
+
true,
|
|
23
|
+
['verify'],
|
|
24
|
+
)
|
|
25
|
+
return { privateKey, publicKey }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const verifyDidSig = async (
|
|
29
|
+
did: string,
|
|
30
|
+
data: Uint8Array,
|
|
31
|
+
sig: Uint8Array,
|
|
32
|
+
): Promise<boolean> => {
|
|
33
|
+
const { jwtAlg, keyBytes } = parseDidKey(did)
|
|
34
|
+
if (jwtAlg !== P256_JWT_ALG) {
|
|
35
|
+
throw new Error(`Not a P-256 did:key: ${did}`)
|
|
36
|
+
}
|
|
37
|
+
return verify(keyBytes, data, sig)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const verify = async (
|
|
41
|
+
publicKey: Uint8Array,
|
|
42
|
+
data: Uint8Array,
|
|
43
|
+
sig: Uint8Array,
|
|
44
|
+
): Promise<boolean> => {
|
|
45
|
+
const importedKey = await importEcdsaPublicKey(publicKey)
|
|
46
|
+
return webcrypto.subtle.verify(
|
|
47
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
48
|
+
importedKey,
|
|
49
|
+
sig,
|
|
50
|
+
data,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const importEcdsaPublicKey = async (
|
|
55
|
+
keyBytes: Uint8Array,
|
|
56
|
+
): Promise<CryptoKey> => {
|
|
57
|
+
return webcrypto.subtle.importKey(
|
|
58
|
+
'raw',
|
|
59
|
+
keyBytes,
|
|
60
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
61
|
+
true,
|
|
62
|
+
['verify'],
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DidKeyPlugin } from '@ucans/core'
|
|
2
|
+
|
|
3
|
+
import { P256_DID_PREFIX, P256_JWT_ALG } from '../const'
|
|
4
|
+
|
|
5
|
+
import * as operations from './operations'
|
|
6
|
+
|
|
7
|
+
export const p256Plugin: DidKeyPlugin = {
|
|
8
|
+
prefix: P256_DID_PREFIX,
|
|
9
|
+
jwtAlg: P256_JWT_ALG,
|
|
10
|
+
verifySignature: operations.verifyDidSig,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default p256Plugin
|
package/src/plugins.ts
ADDED
package/src/random.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as uint8arrays from 'uint8arrays'
|
|
2
|
+
import { webcrypto } from 'one-webcrypto'
|
|
3
|
+
import { SupportedEncodings } from 'uint8arrays/to-string'
|
|
4
|
+
|
|
5
|
+
export const randomBytes = (length: number): Uint8Array => {
|
|
6
|
+
return webcrypto.getRandomValues(new Uint8Array(length))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const randomIV = (): Uint8Array => {
|
|
10
|
+
return randomBytes(12)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const randomStr = (
|
|
14
|
+
byteLength: number,
|
|
15
|
+
encoding: SupportedEncodings,
|
|
16
|
+
): string => {
|
|
17
|
+
const bytes = randomBytes(byteLength)
|
|
18
|
+
return uint8arrays.toString(bytes, encoding)
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as secp from '@noble/secp256k1'
|
|
2
|
+
|
|
3
|
+
export const compressPubkey = (pubkeyBytes: Uint8Array): Uint8Array => {
|
|
4
|
+
const hex = secp.utils.bytesToHex(pubkeyBytes)
|
|
5
|
+
const point = secp.Point.fromHex(hex)
|
|
6
|
+
return point.toRawBytes(true)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const decompressPubkey = (compressed: Uint8Array): Uint8Array => {
|
|
10
|
+
if (compressed.length !== 33) {
|
|
11
|
+
throw new Error('Expected 33 byte compress pubkey')
|
|
12
|
+
}
|
|
13
|
+
const hex = secp.utils.bytesToHex(compressed)
|
|
14
|
+
const point = secp.Point.fromHex(hex)
|
|
15
|
+
return point.toRawBytes(false)
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as ucan from '@ucans/core'
|
|
2
|
+
import * as secp from '@noble/secp256k1'
|
|
3
|
+
import * as uint8arrays from 'uint8arrays'
|
|
4
|
+
import * as did from '../did'
|
|
5
|
+
import { SECP256K1_JWT_ALG } from '../const'
|
|
6
|
+
|
|
7
|
+
export type Secp256k1KeypairOptions = {
|
|
8
|
+
exportable: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class Secp256k1Keypair implements ucan.DidableKey {
|
|
12
|
+
jwtAlg = SECP256K1_JWT_ALG
|
|
13
|
+
private publicKey: Uint8Array
|
|
14
|
+
|
|
15
|
+
constructor(private privateKey: Uint8Array, private exportable: boolean) {
|
|
16
|
+
this.publicKey = secp.getPublicKey(privateKey)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static async create(
|
|
20
|
+
opts?: Partial<Secp256k1KeypairOptions>,
|
|
21
|
+
): Promise<Secp256k1Keypair> {
|
|
22
|
+
const { exportable = false } = opts || {}
|
|
23
|
+
const privKey = secp.utils.randomPrivateKey()
|
|
24
|
+
return new Secp256k1Keypair(privKey, exportable)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static async import(
|
|
28
|
+
privKey: Uint8Array | string,
|
|
29
|
+
opts?: Partial<Secp256k1KeypairOptions>,
|
|
30
|
+
): Promise<Secp256k1Keypair> {
|
|
31
|
+
const { exportable = false } = opts || {}
|
|
32
|
+
const privKeyBytes =
|
|
33
|
+
typeof privKey === 'string'
|
|
34
|
+
? uint8arrays.fromString(privKey, 'hex')
|
|
35
|
+
: privKey
|
|
36
|
+
return new Secp256k1Keypair(privKeyBytes, exportable)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
publicKeyBytes(): Uint8Array {
|
|
40
|
+
return this.publicKey
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
publicKeyStr(encoding: ucan.Encodings = 'base64pad'): string {
|
|
44
|
+
return uint8arrays.toString(this.publicKey, encoding)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
did(): string {
|
|
48
|
+
return did.formatDidKey(this.jwtAlg, this.publicKey)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async sign(msg: Uint8Array): Promise<Uint8Array> {
|
|
52
|
+
const msgHash = await secp.utils.sha256(msg)
|
|
53
|
+
// return raw 64 byte sig not DER-encoded
|
|
54
|
+
return secp.sign(msgHash, this.privateKey, { der: false })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async export(): Promise<Uint8Array> {
|
|
58
|
+
if (!this.exportable) {
|
|
59
|
+
throw new Error('Private key is not exportable')
|
|
60
|
+
}
|
|
61
|
+
return this.privateKey
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default Secp256k1Keypair
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as secp from '@noble/secp256k1'
|
|
2
|
+
import { SECP256K1_JWT_ALG } from '../const'
|
|
3
|
+
import { parseDidKey } from '../did'
|
|
4
|
+
|
|
5
|
+
export const verifyDidSig = async (
|
|
6
|
+
did: string,
|
|
7
|
+
data: Uint8Array,
|
|
8
|
+
sig: Uint8Array,
|
|
9
|
+
): Promise<boolean> => {
|
|
10
|
+
const { jwtAlg, keyBytes } = parseDidKey(did)
|
|
11
|
+
if (jwtAlg !== SECP256K1_JWT_ALG) {
|
|
12
|
+
throw new Error(`Not a secp256k1 did:key: ${did}`)
|
|
13
|
+
}
|
|
14
|
+
const msgHash = await secp.utils.sha256(data)
|
|
15
|
+
return secp.verify(sig, msgHash, keyBytes)
|
|
16
|
+
}
|