tangled
alpha
login
or
join now
mary.my.id
/
boat
22
fork
atom
handy online tools for AT Protocol
boat.kelinci.net
atproto
bluesky
atcute
typescript
solidjs
22
fork
atom
overview
issues
pulls
pipelines
feat: crypto info
mary.my.id
3 months ago
d35afbe1
81c2be02
verified
This commit was signed with the committer's
known signature
.
mary.my.id
SSH Key Fingerprint:
SHA256:ZlTP/auFSGpGnaoDg4mCTG1g9OZvXp62jWR4c6H4O3c=
+261
-2
4 changed files
expand all
collapse all
unified
split
src
routes.ts
views
crypto
crypto-generate.tsx
crypto-info.tsx
frontpage.tsx
+4
src/routes.ts
···
22
path: '/crypto-generate',
23
component: lazy(() => import('./views/crypto/crypto-generate')),
24
},
0
0
0
0
25
26
{
27
path: '/did-lookup',
···
22
path: '/crypto-generate',
23
component: lazy(() => import('./views/crypto/crypto-generate')),
24
},
25
+
{
26
+
path: '/crypto-info',
27
+
component: lazy(() => import('./views/crypto/crypto-info')),
28
+
},
29
30
{
31
path: '/did-lookup',
+1
-1
src/views/crypto/crypto-generate.tsx
···
51
]);
52
53
const result: KeypairResult = {
54
-
type: keypair.type,
55
publicDidKey,
56
privateHex,
57
privateMultikey,
···
51
]);
52
53
const result: KeypairResult = {
54
+
type: keypair.type as KeyType,
55
publicDidKey,
56
privateHex,
57
privateMultikey,
+255
src/views/crypto/crypto-info.tsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js';
2
+
3
+
import {
4
+
type DidKeyString,
5
+
P256PrivateKeyExportable,
6
+
P256PublicKey,
7
+
parseDidKey,
8
+
parsePrivateMultikey,
9
+
parsePublicMultikey,
10
+
Secp256k1PrivateKeyExportable,
11
+
Secp256k1PublicKey,
12
+
} from '@atcute/crypto';
13
+
import { fromBase16 } from '@atcute/multibase';
14
+
15
+
import { useTitle } from '~/lib/navigation/router';
16
+
17
+
import Button from '~/components/inputs/button';
18
+
import RadioInput from '~/components/inputs/radio-input';
19
+
import TextInput from '~/components/inputs/text-input';
20
+
import PageHeader from '~/components/page-header';
21
+
22
+
type KeyType = 'p256' | 'secp256k1';
23
+
type KeyFormat = 'did:key' | 'multikey' | 'hex';
24
+
25
+
interface KeyInfo {
26
+
keyType: KeyType;
27
+
isPrivate: boolean;
28
+
inputFormat: KeyFormat;
29
+
publicDidKey: DidKeyString;
30
+
publicMultikey: string;
31
+
privateHex?: string;
32
+
privateMultikey?: string;
33
+
}
34
+
35
+
const DID_KEY_REGEX = /^did:key:z[a-km-zA-HJ-NP-Z1-9]+$/;
36
+
const MULTIKEY_REGEX = /^z[a-km-zA-HJ-NP-Z1-9]+$/;
37
+
const HEX_REGEX = /^[0-9a-f]+$/;
38
+
39
+
const CryptoInfoPage = () => {
40
+
const [input, setInput] = createSignal('');
41
+
const [hexKeyType, setHexKeyType] = createSignal<KeyType>();
42
+
const [result, setResult] = createSignal<KeyInfo>();
43
+
const [error, setError] = createSignal<string>();
44
+
45
+
const detectedFormat = createMemo((): KeyFormat | undefined => {
46
+
const $input = input().trim();
47
+
48
+
if (DID_KEY_REGEX.test($input)) {
49
+
return 'did:key';
50
+
}
51
+
if (MULTIKEY_REGEX.test($input)) {
52
+
return 'multikey';
53
+
}
54
+
if (HEX_REGEX.test($input)) {
55
+
return 'hex';
56
+
}
57
+
});
58
+
59
+
const canSubmit = createMemo(() => {
60
+
const format = detectedFormat();
61
+
if (!format) {
62
+
return false;
63
+
}
64
+
if (format === 'hex' && !hexKeyType()) {
65
+
return false;
66
+
}
67
+
return true;
68
+
});
69
+
70
+
useTitle(() => `View crypto key info — boat`);
71
+
72
+
return (
73
+
<>
74
+
<PageHeader title="View crypto key info" subtitle="Show basic metadata about a public or private key" />
75
+
76
+
<form
77
+
onSubmit={async (ev) => {
78
+
ev.preventDefault();
79
+
80
+
const $input = input().trim();
81
+
const format = detectedFormat();
82
+
83
+
setResult();
84
+
setError();
85
+
86
+
try {
87
+
let info: KeyInfo;
88
+
89
+
if (format === 'did:key') {
90
+
const parsed = parseDidKey($input);
91
+
const pubKey =
92
+
parsed.type === 'p256'
93
+
? await P256PublicKey.importRaw(parsed.publicKeyBytes)
94
+
: await Secp256k1PublicKey.importRaw(parsed.publicKeyBytes);
95
+
96
+
info = {
97
+
keyType: parsed.type,
98
+
isPrivate: false,
99
+
inputFormat: 'did:key',
100
+
publicDidKey: await pubKey.exportPublicKey('did'),
101
+
publicMultikey: await pubKey.exportPublicKey('multikey'),
102
+
};
103
+
} else if (format === 'multikey') {
104
+
// try parsing as private key first
105
+
try {
106
+
const parsed = parsePrivateMultikey($input);
107
+
const privKey =
108
+
parsed.type === 'p256'
109
+
? await P256PrivateKeyExportable.importRaw(parsed.privateKeyBytes)
110
+
: await Secp256k1PrivateKeyExportable.importRaw(parsed.privateKeyBytes);
111
+
112
+
info = {
113
+
keyType: parsed.type,
114
+
isPrivate: true,
115
+
inputFormat: 'multikey',
116
+
publicDidKey: await privKey.exportPublicKey('did'),
117
+
publicMultikey: await privKey.exportPublicKey('multikey'),
118
+
privateHex: await privKey.exportPrivateKey('rawHex'),
119
+
privateMultikey: await privKey.exportPrivateKey('multikey'),
120
+
};
121
+
} catch {
122
+
// try parsing as public key
123
+
const parsed = parsePublicMultikey($input);
124
+
const pubKey =
125
+
parsed.type === 'p256'
126
+
? await P256PublicKey.importRaw(parsed.publicKeyBytes)
127
+
: await Secp256k1PublicKey.importRaw(parsed.publicKeyBytes);
128
+
129
+
info = {
130
+
keyType: parsed.type,
131
+
isPrivate: false,
132
+
inputFormat: 'multikey',
133
+
publicDidKey: await pubKey.exportPublicKey('did'),
134
+
publicMultikey: await pubKey.exportPublicKey('multikey'),
135
+
};
136
+
}
137
+
} else if (format === 'hex') {
138
+
const keyType = hexKeyType()!;
139
+
const privateKeyBytes = fromBase16($input);
140
+
141
+
const privKey =
142
+
keyType === 'p256'
143
+
? await P256PrivateKeyExportable.importRaw(privateKeyBytes)
144
+
: await Secp256k1PrivateKeyExportable.importRaw(privateKeyBytes);
145
+
146
+
info = {
147
+
keyType: keyType,
148
+
isPrivate: true,
149
+
inputFormat: 'hex',
150
+
publicDidKey: await privKey.exportPublicKey('did'),
151
+
publicMultikey: await privKey.exportPublicKey('multikey'),
152
+
privateHex: await privKey.exportPrivateKey('rawHex'),
153
+
privateMultikey: await privKey.exportPrivateKey('multikey'),
154
+
};
155
+
} else {
156
+
throw new Error('Unknown key format');
157
+
}
158
+
159
+
setResult(info);
160
+
} catch (err) {
161
+
console.error(err);
162
+
setError(`Failed to parse key: ${err}`);
163
+
}
164
+
}}
165
+
class="flex flex-col gap-4 p-4"
166
+
>
167
+
<TextInput
168
+
label="Public or private key"
169
+
blurb="Accepts did:key, multikey, or hex format"
170
+
monospace
171
+
autocomplete="off"
172
+
autocorrect="off"
173
+
placeholder="did:key:z... or z... or a5973930f9d348..."
174
+
value={input()}
175
+
required
176
+
onChange={setInput}
177
+
/>
178
+
179
+
<Show when={detectedFormat() === 'hex'}>
180
+
<RadioInput
181
+
label="This is a..."
182
+
value={hexKeyType()}
183
+
required
184
+
options={[
185
+
{ value: 'secp256k1', label: `ES256K (secp256k1) private key` },
186
+
{ value: 'p256', label: `ES256 (p256) private key` },
187
+
]}
188
+
onChange={setHexKeyType}
189
+
/>
190
+
</Show>
191
+
192
+
<div>
193
+
<Button type="submit" disabled={!canSubmit()}>
194
+
Inspect
195
+
</Button>
196
+
</div>
197
+
</form>
198
+
199
+
<hr class="mx-4 border-gray-300" />
200
+
201
+
<Switch>
202
+
<Match when={error()}>
203
+
<div class="p-4 text-red-600">{error()}</div>
204
+
</Match>
205
+
206
+
<Match when={result()} keyed>
207
+
{(info) => (
208
+
<div class="flex flex-col gap-6 break-words p-4 text-gray-900">
209
+
<div>
210
+
<p class="font-semibold text-gray-600">Key type</p>
211
+
<span>
212
+
{/* @once */ info.keyType === 'p256'
213
+
? 'ES256 (p256)'
214
+
: 'ES256K (secp256k1)'}{' '}
215
+
{/* @once */ info.isPrivate ? 'private' : 'public'} key
216
+
</span>
217
+
</div>
218
+
219
+
<div>
220
+
<p class="font-semibold text-gray-600">Input encoding</p>
221
+
<span>{/* @once */ info.inputFormat}</span>
222
+
</div>
223
+
224
+
<div>
225
+
<p class="font-semibold text-gray-600">Public key (did:key)</p>
226
+
<span class="font-mono">{/* @once */ info.publicDidKey}</span>
227
+
</div>
228
+
229
+
<div>
230
+
<p class="font-semibold text-gray-600">Public key (multikey)</p>
231
+
<span class="font-mono">{/* @once */ info.publicMultikey}</span>
232
+
</div>
233
+
234
+
<Show when={info.privateHex}>
235
+
<div>
236
+
<p class="font-semibold text-gray-600">Private key (hex)</p>
237
+
<span class="font-mono">{/* @once */ info.privateHex}</span>
238
+
</div>
239
+
</Show>
240
+
241
+
<Show when={info.privateMultikey}>
242
+
<div>
243
+
<p class="font-semibold text-gray-600">Private key (multikey)</p>
244
+
<span class="font-mono">{/* @once */ info.privateMultikey}</span>
245
+
</div>
246
+
</Show>
247
+
</div>
248
+
)}
249
+
</Match>
250
+
</Switch>
251
+
</>
252
+
);
253
+
};
254
+
255
+
export default CryptoInfoPage;
+1
-1
src/views/frontpage.tsx
···
121
{
122
name: `View crypto key info`,
123
description: `Show basic metadata about a public or private key`,
124
-
href: null,
125
icon: KeyVisualizerIcon,
126
},
127
],
···
121
{
122
name: `View crypto key info`,
123
description: `Show basic metadata about a public or private key`,
124
+
href: `/crypto-info`,
125
icon: KeyVisualizerIcon,
126
},
127
],