Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
ChooseProviderViewModel.cs
1using System.Collections.ObjectModel;
2using System.ComponentModel;
3using System.Globalization;
4using System.Security.Cryptography;
5using System.Text;
6using System.Xml;
7using CommunityToolkit.Mvvm.ComponentModel;
8using CommunityToolkit.Mvvm.Input;
9using Microsoft.Maui.Controls.Shapes;
15using Waher.Content;
19
21{
22 public partial class ChooseProviderViewModel : BaseRegistrationViewModel
23 {
25 : base(RegistrationStep.ChooseProvider)
26 {
27 }
28
30 protected override async Task OnInitialize()
31 {
32 await base.OnInitialize();
33
34 ServiceRef.TagProfile.Changed += this.TagProfile_Changed;
35 LocalizationManager.Current.PropertyChanged += this.Localization_Changed;
36 }
37
39 protected override async Task OnDispose()
40 {
41 ServiceRef.TagProfile.Changed -= this.TagProfile_Changed;
42 LocalizationManager.Current.PropertyChanged -= this.Localization_Changed;
43
44 await base.OnDispose();
45 }
46
48 public override async Task DoAssignProperties()
49 {
50 await base.DoAssignProperties();
51
53 GoToRegistrationStep(RegistrationStep.CreateAccount);
54 }
55
56 protected override void OnPropertyChanged(PropertyChangedEventArgs e)
57 {
58 base.OnPropertyChanged(e);
59
60 switch (e.PropertyName)
61 {
62 /*
63 case nameof(this.SelectedButton):
64 if ((this.SelectedButton is not null) && (this.SelectedButton.Button == ButtonType.Change))
65 {
66 MainThread.BeginInvokeOnMainThread(() =>
67 {
68 if (this.ScanQrCodeCommand.CanExecute(null))
69 this.ScanQrCodeCommand.Execute(null);
70 });
71 }
72 break;
73 */
74 case nameof(this.IsBusy):
75 this.ContinueCommand.NotifyCanExecuteChanged();
76 this.ScanQrCodeCommand.NotifyCanExecuteChanged();
77 break;
78 }
79 }
80
84 [ObservableProperty]
85 private string domainName = string.Empty;
86
90 [ObservableProperty]
91 [NotifyPropertyChangedFor(nameof(HasLocalizedName))]
92 private string localizedName = string.Empty;
93
97 [ObservableProperty]
98 [NotifyPropertyChangedFor(nameof(HasLocalizedDescription))]
99 private string localizedDescription = string.Empty;
100
104 public bool HasLocalizedName => this.LocalizedName.Length > 0;
105
109 public bool HasLocalizedDescription => this.LocalizedDescription.Length > 0;
110
114 public static bool IsAccountCreated => !string.IsNullOrEmpty(ServiceRef.TagProfile.Account);
115
116 /*
120 public Collection<ButtonInfo> Buttons { get; } =
121 [
122 new(ButtonType.Approve),
123 new(ButtonType.Change),
124 ];
125
126
130 [ObservableProperty]
131 [NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
132 private ButtonInfo? selectedButton;
133 */
134 private async void TagProfile_Changed(object? Sender, PropertyChangedEventArgs e)
135 {
136 if (this.DomainName != ServiceRef.TagProfile.Domain)
137 await this.SetDomainName();
138 }
139
140 private async void Localization_Changed(object? Sender, PropertyChangedEventArgs e)
141 {
142 await this.SetDomainName();
143 }
144
145 private async Task SetDomainName()
146 {
147 if (string.IsNullOrEmpty(ServiceRef.TagProfile.Domain))
148 {
149 this.DomainName = string.Empty;
150 this.LocalizedName = string.Empty;
151 this.LocalizedDescription = string.Empty;
152 return;
153 }
154
155 this.DomainName = ServiceRef.TagProfile.Domain;
156
157 try
158 {
159 Uri DomainInfo = new("https://" + this.DomainName + "/Agent/Account/DomainInfo");
160 string AcceptLanguage = App.SelectedLanguage.TwoLetterISOLanguageName;
161
162 if (AcceptLanguage != "en")
163 AcceptLanguage += ";q=1,en;q=0.9";
164
165 object Result = await InternetContent.GetAsync(DomainInfo,
166 new KeyValuePair<string, string>("Accept", "application/json"),
167 new KeyValuePair<string, string>("Accept-Language", AcceptLanguage),
168 new KeyValuePair<string, string>("Accept-Encoding", "0"));
169 if (Result is Dictionary<string, object> Response)
170 {
171 if (Response.TryGetValue("humanReadableName", out object? Obj) && Obj is string LocalizedName)
172 this.LocalizedName = LocalizedName;
173
174 if (Response.TryGetValue("humanReadableDescription", out Obj) && Obj is string LocalizedDescription)
175 this.LocalizedDescription = LocalizedDescription;
176 }
177 }
178 catch (Exception ex)
179 {
180 ServiceRef.LogService.LogException(ex);
181 }
182 }
183
184 public bool CanScanQrCode => !this.IsBusy;
185
186 public bool CanContinue => !this.IsBusy;
187 //&& (this.SelectedButton is not null) && (this.SelectedButton.Button == ButtonType.Approve);
188
189 [RelayCommand]
190 private void Continue()
191 {
192 GoToRegistrationStep(RegistrationStep.ValidatePhone);
193 }
194
195 [RelayCommand]
196 private static async Task ServiceProviderInfo()
197 {
198 string title = ServiceRef.Localizer[nameof(AppResources.WhatIsAServiceProvider)];
199 string message = ServiceRef.Localizer[nameof(AppResources.ServiceProviderInfo)];
200 ShowInfoPopup infoPage = new(title, message);
201 await ServiceRef.UiService.PushAsync(infoPage);
202 }
203
204 [RelayCommand]
205 private async Task SelectedServiceProviderInfo()
206 {
207 string title = this.LocalizedName;
208 string message = this.LocalizedDescription;
209 ShowInfoPopup infoPage = new(title, message);
210 await ServiceRef.UiService.PushAsync(infoPage);
211 }
212
213 [RelayCommand]
214 private static void UndoSelection()
215 {
217 }
218
219 [RelayCommand(CanExecute = nameof(CanScanQrCode))]
220 private async Task ScanQrCode()
221 {
222 string? Url = await Services.UI.QR.QrCode.ScanQrCode(nameof(AppResources.QrPageTitleScanInvitation),
224
225 if (string.IsNullOrEmpty(Url))
226 return;
227
228 string Scheme = Constants.UriSchemes.GetScheme(Url) ?? string.Empty;
229
230 if (!string.Equals(Scheme, Constants.UriSchemes.Onboarding, StringComparison.OrdinalIgnoreCase))
231 return;
232
233 string[] Parts = Url.Split(':');
234
235 if (Parts.Length != 5)
236 {
237 await ServiceRef.UiService.DisplayAlert(
238 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)],
239 ServiceRef.Localizer[nameof(AppResources.InvalidInvitationCode)],
240 ServiceRef.Localizer[nameof(AppResources.Ok)]);
241
242 return;
243 }
244
245 string Domain = Parts[1];
246 string Code = Parts[2];
247 string KeyStr = Parts[3];
248 string IVStr = Parts[4];
249 string EncryptedStr;
250 Uri Uri;
251
252 try
253 {
254 Uri = new Uri("https://" + Domain + "/Onboarding/GetInfo");
255 }
256 catch (Exception ex)
257 {
258 ServiceRef.LogService.LogException(ex);
259
260 await ServiceRef.UiService.DisplayAlert(
261 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)],
262 ServiceRef.Localizer[nameof(AppResources.InvalidInvitationCode)],
263 ServiceRef.Localizer[nameof(AppResources.Ok)]);
264
265 return;
266 }
267
268 this.IsBusy = true;
269
270 try
271 {
272 try
273 {
274 KeyValuePair<byte[], string> P = await InternetContent.PostAsync(Uri, Encoding.ASCII.GetBytes(Code), "text/plain",
275 new KeyValuePair<string, string>("Accept", "text/plain"));
276
277 object Decoded = await InternetContent.DecodeAsync(P.Value, P.Key, Uri);
278
279 EncryptedStr = (string)Decoded;
280 }
281 catch (Exception ex)
282 {
283 ServiceRef.LogService.LogException(ex);
284
285 await ServiceRef.UiService.DisplayAlert(
286 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)],
287 ServiceRef.Localizer[nameof(AppResources.UnableToAccessInvitation)],
288 ServiceRef.Localizer[nameof(AppResources.Ok)]);
289 return;
290 }
291
292 try
293 {
294 byte[] Key = Convert.FromBase64String(KeyStr);
295 byte[] IV = Convert.FromBase64String(IVStr);
296 byte[] Encrypted = Convert.FromBase64String(EncryptedStr);
297
298 using Aes Aes = Aes.Create();
299 Aes.BlockSize = 128;
300 Aes.KeySize = 256;
301 Aes.Mode = CipherMode.CBC;
302 Aes.Padding = PaddingMode.PKCS7;
303
304 using ICryptoTransform Decryptor = Aes.CreateDecryptor(Key, IV);
305 byte[] Decrypted = Decryptor.TransformFinalBlock(Encrypted, 0, Encrypted.Length);
306 string Xml = Encoding.UTF8.GetString(Decrypted);
307
308 XmlDocument Doc = new()
309 {
310 PreserveWhitespace = true
311 };
312
313 Doc.LoadXml(Xml);
314
315 if ((Doc.DocumentElement is null) || (Doc.DocumentElement.NamespaceURI != ContractsClient.NamespaceOnboarding))
316 throw new Exception("Invalid Invitation XML");
317
318 LinkedList<XmlElement> ToProcess = new();
319 ToProcess.AddLast(Doc.DocumentElement);
320
321 bool AccountDone = false;
322 XmlElement? LegalIdDefinition = null;
323 string? Pin = null;
324
325 while (ToProcess.First is not null)
326 {
327 XmlElement E = ToProcess.First.Value;
328 ToProcess.RemoveFirst();
329
330 switch (E.LocalName)
331 {
332 case "ApiKey":
333 KeyStr = XML.Attribute(E, "key");
334 string Secret = XML.Attribute(E, "secret");
335 Domain = XML.Attribute(E, "domain");
336
337 await SelectDomain(Domain, KeyStr, Secret);
338
339 await ServiceRef.UiService.DisplayAlert(
340 ServiceRef.Localizer[nameof(AppResources.InvitationAccepted)],
341 ServiceRef.Localizer[nameof(AppResources.InvitedToCreateAccountOnDomain), Domain],
342 ServiceRef.Localizer[nameof(AppResources.Ok)]);
343 break;
344
345 case "Account":
346 string UserName = XML.Attribute(E, "userName");
347 string Password = XML.Attribute(E, "password");
348 string PasswordMethod = XML.Attribute(E, "passwordMethod");
349 Domain = XML.Attribute(E, "domain");
350
351 string DomainBak = ServiceRef.TagProfile?.Domain ?? string.Empty;
352 bool DefaultConnectivityBak = ServiceRef.TagProfile?.DefaultXmppConnectivity ?? false;
353 string ApiKeyBak = ServiceRef.TagProfile?.ApiKey ?? string.Empty;
354 string ApiSecretBak = ServiceRef.TagProfile?.ApiSecret ?? string.Empty;
355
356 await SelectDomain(Domain, string.Empty, string.Empty);
357
358 if (!await this.ConnectToAccount(UserName, Password, PasswordMethod, string.Empty, LegalIdDefinition, Pin ?? string.Empty))
359 {
360 ServiceRef.TagProfile?.SetDomain(DomainBak, DefaultConnectivityBak, ApiKeyBak, ApiSecretBak);
361 throw new Exception("Invalid account.");
362 }
363
364 LegalIdDefinition = null;
365 AccountDone = true;
366 break;
367
368 case "LegalId":
369 LegalIdDefinition = E;
370 break;
371
372 case "Pin":
373 Pin = XML.Attribute(E, "pin");
374 break;
375
376 case "Transfer":
377 foreach (XmlNode N in E.ChildNodes)
378 {
379 if (N is XmlElement E2)
380 {
381 ToProcess.AddLast(E2);
382 }
383 }
384 break;
385
386 default:
387 throw new Exception("Invalid Invitation XML");
388 }
389 }
390
391 if (LegalIdDefinition is not null)
392 await ServiceRef.XmppService.ImportSigningKeys(LegalIdDefinition);
393
394 if (AccountDone)
395 GoToRegistrationStep(RegistrationStep.CreateAccount);
396 }
397 catch (Exception ex)
398 {
399 ServiceRef.LogService.LogException(ex);
400
401 await ServiceRef.UiService.DisplayAlert(
402 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)],
403 ServiceRef.Localizer[nameof(AppResources.InvalidInvitationCode)],
404 ServiceRef.Localizer[nameof(AppResources.Ok)]);
405
406 return;
407 }
408 }
409 finally
410 {
411 this.IsBusy = false;
412 }
413 }
414
415 private static async Task SelectDomain(string Domain, string Key, string Secret)
416 {
417 bool DefaultConnectivity;
418
419 try
420 {
421 (string HostName, int PortNumber, bool IsIpAddress) = await ServiceRef.NetworkService.LookupXmppHostnameAndPort(Domain);
422 DefaultConnectivity = HostName == Domain && PortNumber == XmppCredentials.DefaultPort;
423 }
424 catch (Exception ex)
425 {
426 ServiceRef.LogService.LogException(ex);
427 DefaultConnectivity = false;
428 }
429
430 ServiceRef.TagProfile.SetDomain(Domain, DefaultConnectivity, Key, Secret);
431 }
432
433
434 private async Task<bool> ConnectToAccount(string AccountName, string Password, string PasswordMethod, string LegalIdentityJid, XmlElement? LegalIdDefinition, string Pin)
435 {
436 try
437 {
438 async Task OnConnected(XmppClient client)
439 {
440 DateTime now = DateTime.Now;
441 LegalIdentity? createdIdentity = null;
442 LegalIdentity? approvedIdentity = null;
443
444 bool serviceDiscoverySucceeded;
445
447 {
448 serviceDiscoverySucceeded = await ServiceRef.XmppService.DiscoverServices(client);
449 }
450 else
451 {
452 serviceDiscoverySucceeded = true;
453 }
454
455 if (serviceDiscoverySucceeded && !string.IsNullOrEmpty(ServiceRef.TagProfile.LegalJid))
456 {
457 bool DestroyContractsClient = false;
458
459 if (!client.TryGetExtension(typeof(ContractsClient), out IXmppExtension Extension) ||
460 Extension is not ContractsClient ContractsClient)
461 {
463 DestroyContractsClient = true;
464 }
465
466 try
467 {
468 if (LegalIdDefinition is not null)
469 await ContractsClient.ImportKeys(LegalIdDefinition);
470
472
473 foreach (LegalIdentity Identity in Identities)
474 {
475 try
476 {
477 if ((string.IsNullOrEmpty(LegalIdentityJid) || string.Compare(LegalIdentityJid, Identity.Id, StringComparison.OrdinalIgnoreCase) == 0) &&
478 Identity.HasClientSignature &&
479 Identity.HasClientPublicKey &&
480 Identity.From <= now &&
481 Identity.To >= now &&
482 (Identity.State == IdentityState.Approved || Identity.State == IdentityState.Created) &&
483 Identity.ValidateClientSignature() &&
484 await ContractsClient.HasPrivateKey(Identity))
485 {
486 if (Identity.State == IdentityState.Approved)
487 {
488 approvedIdentity = Identity;
489 break;
490 }
491
492 createdIdentity ??= Identity;
493 }
494 }
495 catch (Exception)
496 {
497 // Keys might not be available. Ignore at this point. Keys will be generated later.
498 }
499 }
500
501 /*
502 if (approvedIdentity is not null)
503 {
504 this.LegalIdentity = approvedIdentity;
505 }
506 else if (createdIdentity is not null)
507 {
508 this.LegalIdentity = createdIdentity;
509 }*/
510 LegalIdentity? selectedIdentity = approvedIdentity ?? createdIdentity;
511
512 string SelectedId;
513
514 if (selectedIdentity is not null)
515 {
516 await ServiceRef.TagProfile.SetAccountAndLegalIdentity(AccountName, client.PasswordHash, client.PasswordHashMethod, selectedIdentity);
517 SelectedId = selectedIdentity.Id;
518 }
519 else
520 {
522 SelectedId = string.Empty;
523 }
524
525 if (!string.IsNullOrEmpty(Pin))
526 {
527 ServiceRef.TagProfile.LocalPassword = Pin;
528 }
529
530 foreach (LegalIdentity Identity in Identities)
531 {
532 if (Identity.Id == SelectedId)
533 {
534 continue;
535 }
536
537 switch (Identity.State)
538 {
539 case IdentityState.Approved:
540 case IdentityState.Created:
542 break;
543 }
544 }
545 }
546 finally
547 {
548 if (DestroyContractsClient)
549 {
551 }
552 }
553 }
554 }
555
556 (string hostName, int portNumber, bool isIpAddress) = await ServiceRef.NetworkService.LookupXmppHostnameAndPort(
557 ServiceRef.TagProfile?.Domain ?? string.Empty);
558
559 (bool succeeded, string? errorMessage, string[]? alternatives) = await ServiceRef.XmppService.TryConnectAndConnectToAccount(
560 ServiceRef.TagProfile?.Domain ?? string.Empty,
561 isIpAddress, hostName, portNumber, AccountName, Password, PasswordMethod, Constants.LanguageCodes.Default,
562 typeof(App).Assembly, OnConnected);
563
564 if (!succeeded)
565 {
566 await ServiceRef.UiService.DisplayAlert(
567 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)],
568 errorMessage ?? string.Empty,
569 ServiceRef.Localizer[nameof(AppResources.Ok)]);
570 }
571
572 return succeeded;
573 }
574 catch (Exception ex)
575 {
576 ServiceRef.LogService.LogException(ex);
577
578 await ServiceRef.UiService.DisplayAlert(
579 ServiceRef.Localizer[nameof(AppResources.ErrorTitle)],
580 ServiceRef.Localizer[nameof(AppResources.UnableToConnectTo), ServiceRef.TagProfile?.Domain ?? string.Empty],
581 ServiceRef.Localizer[nameof(AppResources.Ok)]);
582 }
583
584 return false;
585 }
586 }
587 /*
588 public enum ButtonType
589 {
590 Approve = 0,
591 Change = 1,
592 }
593
594 public partial class ButtonInfo : ObservableObject
595 {
596 public ButtonInfo(ButtonType Button)
597 {
598 this.Button = Button;
599
600 LocalizationManager.CurrentCultureChanged += this.OnCurrentCultureChanged;
601 }
602
603 ~ButtonInfo()
604 {
605 LocalizationManager.CurrentCultureChanged -= this.OnCurrentCultureChanged;
606 }
607
608 private void OnCurrentCultureChanged(object? Sender, CultureInfo Culture)
609 {
610 this.OnPropertyChanged(nameof(this.LocalizedName));
612 // this.OnPropertyChanged(nameof(this.LocalizedDescription));
613 }
614
615 public ButtonType Button { get; set; }
616
617 public string LocalizedName
618 {
619 get
620 {
621 return this.Button switch
622 {
623 ButtonType.Approve => ServiceRef.Localizer[nameof(AppResources.ProviderSectionApproveOption)],
624 ButtonType.Change => ServiceRef.Localizer[nameof(AppResources.ProviderSectionChangeOption)],
625 _ => throw new NotImplementedException(),
626 };
627 }
628 }
629
630 public Geometry ImageData
631 {
632 get
633 {
634 return this.Button switch
635 {
636 ButtonType.Approve => Geometries.ApproveProviderIconPath,
637 ButtonType.Change => Geometries.ChangeProviderIconPath,
638 _ => throw new NotImplementedException(),
639 };
640 }
641 }
642 }
643 */
644}
The Application class, representing an instance of the Neuro-Access app.
Definition: App.xaml.cs:69
static LanguageInfo SelectedLanguage
Selected language.
Definition: App.xaml.cs:196
const string Default
The default language code.
Definition: Constants.cs:83
const string Onboarding
Onboarding URI Scheme (obinfo)
Definition: Constants.cs:129
static ? string GetScheme(string Url)
Gets the predefined scheme from an IoT Code
Definition: Constants.cs:146
A set of never changing property constants and helpful values.
Definition: Constants.cs:7
Base class that references services in the app.
Definition: ServiceRef.cs:31
static ILogService LogService
Log service.
Definition: ServiceRef.cs:91
static INetworkService NetworkService
Network service.
Definition: ServiceRef.cs:103
static IUiService UiService
Service serializing and managing UI-related tasks.
Definition: ServiceRef.cs:55
static ITagProfile TagProfile
TAG Profile service.
Definition: ServiceRef.cs:79
static IStringLocalizer Localizer
Localization service
Definition: ServiceRef.cs:235
static IXmppService XmppService
The XMPP service for XMPP communication.
Definition: ServiceRef.cs:67
bool HasLocalizedDescription
The localized intro text to display to the user for explaining what 'choose account' is for.
bool HasLocalizedName
The localized intro text to display to the user for explaining what 'choose account' is for.
Static class managing encoding and decoding of internet content.
static Task< object > DecodeAsync(string ContentType, byte[] Data, Encoding Encoding, KeyValuePair< string, string >[] Fields, Uri BaseUri)
Decodes an object.
static Task< object > GetAsync(Uri Uri, params KeyValuePair< string, string >[] Headers)
Gets a resource, given its URI.
static Task< object > PostAsync(Uri Uri, object Data, params KeyValuePair< string, string >[] Headers)
Posts to a resource, using a Uniform Resource Identifier (or Locator).
Helps with common XML-related tasks.
Definition: XML.cs:19
static string Attribute(XmlElement E, string Name)
Gets the value of an XML attribute.
Definition: XML.cs:914
Adds support for legal identities, smart contracts and signatures to an XMPP client.
Task< bool > ImportKeys(string Xml)
Imports keys
Task< LegalIdentity > ObsoleteLegalIdentityAsync(string LegalIdentityId)
Obsoletes one of the legal identities of the account, given its ID.
Task< bool > HasPrivateKey(LegalIdentity Identity)
Checks if the private key of a legal identity is available. Private keys are required to be able to s...
const string NamespaceOnboarding
http://waher.se/schema/Onboarding/v1.xsd
Task< LegalIdentity[]> GetLegalIdentitiesAsync()
Gets legal identities registered with the account.
override void Dispose()
Disposes of the extension.
Manages an XMPP client connection. Implements XMPP, as defined in https://tools.ietf....
Definition: XmppClient.cs:59
string PasswordHashMethod
Password hash method.
Definition: XmppClient.cs:3445
bool TryGetExtension(Type Type, out IXmppExtension Extension)
Tries to get a registered extension of a specific type from the client.
Definition: XmppClient.cs:7318
string PasswordHash
Hash value of password. Depends on method used to authenticate user.
Definition: XmppClient.cs:3436
Class containing credentials for an XMPP client connection.
const int DefaultPort
Default XMPP Server port.
void UndoDomainSelection()
Reverses the SetDomain to the Initial* values.
string? Account
The account name for this profile
Definition: ITagProfile.cs:101
void SetDomain(string DomainName, bool DefaultXmppConnectivity, string Key, string Secret)
Set the domain name to connect to.
string? ApiKey
API Key, for creating new account.
Definition: ITagProfile.cs:61
string? LegalJid
The Jabber Legal JID for this user/profile.
Definition: ITagProfile.cs:116
string? ApiSecret
API Secret, for creating new account.
Definition: ITagProfile.cs:66
bool DefaultXmppConnectivity
If connecting to the domain can be done using default parameters (host=domain, default c2s port).
Definition: ITagProfile.cs:56
bool NeedsUpdating()
Returns true if the current ITagProfile needs to have its values updated, false otherwise.
void SetAccount(string AccountName, string ClientPasswordHash, string ClientPasswordHashMethod)
Set the account name and password for a new account.
string? Domain
The domain this profile is connected to.
Definition: ITagProfile.cs:51
Task SetAccountAndLegalIdentity(string AccountName, string ClientPasswordHash, string ClientPasswordHashMethod, LegalIdentity Identity)
Set the account name and password for an existing account.
RegistrationStep
The different steps of a TAG Profile registration journey.
IdentityState
Lists recognized legal identity states.