-
Notifications
You must be signed in to change notification settings - Fork 0
/
passkey_signer.dart
385 lines (328 loc) · 12.2 KB
/
passkey_signer.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
part of '../web3_signers_base.dart';
typedef Hex = String;
typedef Bytes = Uint8List;
class AuthData {
final Hex hexCredential;
final Bytes rawCredential;
/// x and y coordinates of the public key
final Tuple<Uint256, Uint256> publicKey;
final String aaGUID;
AuthData(this.hexCredential, this.rawCredential, this.publicKey, this.aaGUID);
}
class PassKeyPair {
final AuthData authData;
final String username;
final String? displayname;
final DateTime? registrationTime;
PassKeyPair(
this.authData, this.username, this.displayname, this.registrationTime);
factory PassKeyPair.fromJson(String source) =>
PassKeyPair.fromMap(json.decode(source) as Map<String, dynamic>);
factory PassKeyPair.fromMap(Map<String, dynamic> map) {
final pKey = List<String>.from(map['publicKey'])
.map((e) => Uint256.fromHex(e))
.toList();
return PassKeyPair(
AuthData(
map['hexCredential'],
b64d(map['rawCredential']),
Tuple(pKey[0], pKey[1]),
map['aaGUID'],
),
map['username'],
map['displayname'],
map['registrationTime'] != null
? DateTime.fromMillisecondsSinceEpoch(map['registrationTime'])
: null,
);
}
String toJson() => json.encode(toMap());
Map<String, dynamic> toMap() {
return <String, dynamic>{
'hexCredential': authData.hexCredential,
'rawCredential': b64e(authData.rawCredential),
'publicKey':
authData.publicKey.toList().map((e) => e.toString()).toList(),
'username': username,
'displayname': displayname,
'aaGUID': authData.aaGUID,
'registrationTime': registrationTime?.millisecondsSinceEpoch,
};
}
}
class PassKeySignature {
final Hex hexCredential;
final Bytes rawCredential;
/// r and s values of the signature.
final Tuple<Uint256, Uint256> signature;
final Uint8List authData;
final String clientDataPrefix;
final String clientDataSuffix;
/// not decodable.
final String userId;
PassKeySignature(this.hexCredential, this.rawCredential, this.signature,
this.authData, this.clientDataPrefix, this.clientDataSuffix, this.userId);
/// Converts the `PassKeySignature` to a `Uint8List` using the specified ABI encoding.
///
/// Returns the encoded Uint8List.
///
/// Example:
/// ```dart
/// final Uint8List encodedSig = pkpSig.toUint8List();
/// ```
Uint8List toUint8List() {
return abi.encode([
'uint256',
'uint256',
'bytes',
'string',
'string'
], [
signature.item1.value,
signature.item2.value,
authData,
clientDataPrefix,
clientDataSuffix
]);
}
}
class PassKeySigner implements PasskeySignerInterface {
final PassKeysOptions _opts;
final PasskeyAuthenticator _auth;
final Set<Bytes> _knownCredentials;
@override
String dummySignature =
"0xe017c9b829f0d550c9a0f1d791d460485b774c5e157d2eaabdf690cba2a62726b3e3a3c5022dc5301d272a752c05053941b1ca608bf6bc8ec7c71dfe15d5305900000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000025205f5f63c4a6cebdc67844b75186367e6d2e4f19b976ab0affefb4e981c22435050000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d222c226f726967696e223a226170692e776562617574686e2e696f227d000000";
/// - [namespace] : the relying party entity id e.g "variance.space"
/// - [name] : the relying party entity name e.g "Variance"
/// - [origin] : the relying party entity origin. e.g "https://variance.space"
/// - [knownCredentials] : a set of known credentials. Defaults to an empty set.
PassKeySigner(String namespace, String name, String origin,
{Set<Bytes> knownCredentials = const {}})
: _opts = PassKeysOptions(
namespace: namespace,
name: name,
origin: origin,
),
_auth = PasskeyAuthenticator(),
_knownCredentials = knownCredentials;
@override
Set<Bytes> get credentialIds => _knownCredentials;
@override
PassKeysOptions get opts => _opts;
@override
Uint8List clientDataHash(PassKeysOptions options, [String? challenge]) {
options.challenge = challenge ?? randomBase64String();
final clientDataJson = jsonEncode({
"type": options.type,
"challenge": options.challenge,
"origin": options.origin,
});
final dataBuffer = utf8.encode(clientDataJson);
final hash = sha256Hash(dataBuffer);
return Uint8List.fromList(hash);
}
@override
String credentialIdToHex(List<int> credentialId) {
if (credentialId.length <= 32) {
while (credentialId.length < 32) {
credentialId.insert(0, 0);
}
return hexlify(credentialId);
}
Logger.error(
"Credential ID too long: ${credentialId.length}, hex operation skipped");
return "";
}
@override
String getAddress({int? index}) {
return base64Url.encode(_knownCredentials.elementAt(index ?? 0));
}
@override
Uint8List hexToCredentialId(String credentialHex) {
if (credentialHex.startsWith("0x")) {
credentialHex = credentialHex.substring(2);
}
List<int> credentialId = hexToBytes(credentialHex).toList();
while (credentialId.isNotEmpty && credentialId[0] == 0) {
credentialId.removeAt(0);
}
return Uint8List.fromList(credentialId);
}
@override
Future<Uint8List> personalSign(Uint8List hash, {int? index}) async {
final knownCredentials = _getKnownCredentials(index);
final signature =
await signToPasskeySignature(hash, knownCredentials: knownCredentials);
return signature.toUint8List();
}
List<CredentialType> _getKnownCredentials([int? index]) {
// Retrive known credentials if any
final List<Bytes> credentialIds;
if (index != null) {
credentialIds = _knownCredentials.elementAtOrNull(index) != null
? [_knownCredentials.elementAt(index)]
: _knownCredentials.toList();
} else {
credentialIds = _knownCredentials.toList();
}
// convert credentialIds to CredentialType
final List<CredentialType> credentials = credentialIds
.map((e) =>
CredentialType(type: "public-key", id: b64e(e), transports: []))
.toList();
return credentials;
}
@override
Future<PassKeyPair> register(String username, String displayname,
{String? challenge,
bool requiresResidentKey = true,
bool requiresUserVerification = true}) async {
final attestation = await _register(
username,
displayname,
challenge,
requiresResidentKey,
requiresUserVerification,
);
final authData = _decodeAttestation(attestation);
return PassKeyPair(
authData,
username,
displayname,
DateTime.now(),
);
}
@override
Future<MsgSignature> signToEc(Uint8List hash, {int? index}) async {
final knownCredentials = _getKnownCredentials(index);
final signature =
await signToPasskeySignature(hash, knownCredentials: knownCredentials);
return MsgSignature(
signature.signature.item1.value, signature.signature.item2.value, 0);
}
@override
Future<PassKeySignature> signToPasskeySignature(Uint8List hash,
{List<CredentialType>? knownCredentials}) async {
// Prepare hash
final hashBase64 = b64e(hash);
// Authenticate with passkey
final assertion = await _authenticate(hashBase64, knownCredentials, true);
// Extract signature from response
final sig = getMessagingSignature(b64d(assertion.signature));
// Prepare challenge for response
final clientDataJSON = utf8.decode(b64d(assertion.clientDataJSON));
int challengePos = clientDataJSON.indexOf(hashBase64);
String challengePrefix = clientDataJSON.substring(0, challengePos);
String challengeSuffix =
clientDataJSON.substring(challengePos + hashBase64.length);
return PassKeySignature(
credentialIdToHex(b64d(assertion.id).toList()),
b64d(assertion.rawId),
sig,
b64d(assertion.authenticatorData),
challengePrefix,
challengeSuffix,
assertion.userHandle);
}
Future<AuthenticateResponseType> _authenticate(String challenge,
[List<CredentialType>? allowedCredentials,
bool requiresUserVerification = true]) async {
final entity = AuthenticateRequestType(
relyingPartyId: _opts.namespace,
challenge: challenge,
timeout: 60000,
userVerification: requiresUserVerification ? 'required' : 'preferred',
allowCredentials: allowedCredentials,
mediation: MediationType.Conditional);
return await _auth.authenticate(entity);
}
AuthData _decode(List<int> authData) {
// Extract the length of the public key from the authentication data.
final l = (authData[53] << 8) + authData[54];
// Calculate the offset for the start of the public key data.
final publicKeyOffset = 55 + l;
// Extract the public key data from the authentication data.
final pKey = authData.sublist(publicKeyOffset);
// Extract the credential ID from the authentication data.
final List<int> credentialId = authData.sublist(55, publicKeyOffset);
// Extract and encode the aaGUID from the authentication data.
final aaGUID = base64Url.encode(authData.sublist(37, 53));
// Decode the CBOR-encoded public key and convert it to a map.
final decodedPubKey = CborObject.fromCbor(pKey) as CborMapValue;
final keyX = decodedPubKey.value.entries
.firstWhere((element) => element.key.value == -2);
final keyY = decodedPubKey.value.entries
.firstWhere((element) => element.key.value == -3);
// Calculate the hash of the credential ID.
String hexCredentialId = credentialIdToHex(credentialId);
// Extract x and y coordinates from the decoded public key.
final x = Uint256.fromHex(hexlify(keyX.value.value));
final y = Uint256.fromHex(hexlify(keyY.value.value));
return AuthData(
hexCredentialId, Uint8List.fromList(credentialId), Tuple(x, y), aaGUID);
}
AuthData _decodeAttestation(RegisterResponseType attestation) {
final attestationAsCbor = b64d(attestation.attestationObject);
final decodedAttestationAsCbor =
CborObject.fromCbor(attestationAsCbor) as CborMapValue;
final key = decodedAttestationAsCbor.value.entries
.firstWhere((element) => element.key.value == "authData");
final value = key.value.value;
final authData = List<int>.from(value);
return _decode(authData);
}
@override
String randomBase64String() {
final uuid = UUID.generateUUIDv4();
return b64e(UUID.toBuffer(uuid));
}
Future<RegisterResponseType> _register(String username, String displayname,
[String? challenge,
bool requiresResidentKey = true,
bool requiresUserVerification = true]) async {
final options = _opts;
options.type = "webauthn.create";
final entity = RegisterRequestType(
challenge: b64e(clientDataHash(options, challenge)),
relyingParty: RelyingPartyType(
id: options.namespace,
name: options.name,
),
user: UserType(
id: randomBase64String(),
displayName: displayname,
name: username,
),
authSelectionType: AuthenticatorSelectionType(
requireResidentKey: requiresResidentKey,
residentKey: requiresResidentKey ? 'preferred' : 'discouraged',
authenticatorAttachment: 'platform',
userVerification: requiresUserVerification ? 'required' : 'preferred',
),
pubKeyCredParams: [
PubKeyCredParamType(
type: 'public-key',
alg: -7,
),
],
timeout: 60000,
attestation: 'none',
excludeCredentials: [],
);
return await _auth.register(entity);
}
}
class PassKeysOptions {
final String namespace;
final String name;
final String origin;
String? challenge;
String? type;
PassKeysOptions(
{required this.namespace,
required this.name,
required this.origin,
this.challenge,
this.type});
}