Neuron®
The Neuron® is the basis for the creation of open and secure federated networks for smart societies.
Loading...
Searching...
No Matches
ExternalCredential.cs
1using System;
3using System.Net;
4using System.Text;
5using System.Threading.Tasks;
6using System.Xml;
7using Waher.Content;
10using Waher.Events;
16
18{
23 {
27 HOTP,
28
32 TOTP,
33
37 UTF8
38 }
39
43 [CollectionName("ExternalCredentials")]
44 [TypeName(TypeNameSerialization.None)]
45 [ArchivingTime]
46 [Index("Type", "Endpoint")]
47 [Index("Endpoint", "Type")]
49 {
50 private static ICallStackCheck[] approvedSources = null;
51
52 private HashFunction? hashFunction;
53 private byte[] secret;
54 private string account;
55 private string issuer;
56 private string label;
57 private string description;
58 private string image;
59 private long? counter;
60 private int? nrDigits = null;
61 private int? timeStepSeconds = null;
62
66 [ObjectId]
67 public string ObjectId { get; set; }
68
72 public CredentialAlgorithm Type { get; set; }
73
77 public string Endpoint { get; set; }
78
82 [Encrypted(32)]
83 public string Label
84 {
85 get
86 {
87 AssertAllowed();
88 return this.label;
89 }
90
91 set
92 {
93 if (!string.IsNullOrEmpty(this.label))
94 AssertAllowed();
95
96 this.label = value;
97 }
98 }
99
103 [Encrypted(32)]
104 public string Description
105 {
106 get
107 {
108 AssertAllowed();
109 return this.description;
110 }
111
112 set
113 {
114 if (!string.IsNullOrEmpty(this.description))
115 AssertAllowed();
116
117 this.description = value;
118 }
119 }
120
124 [Encrypted(32)]
125 public string Image
126 {
127 get
128 {
129 AssertAllowed();
130 return this.image;
131 }
132
133 set
134 {
135 if (!string.IsNullOrEmpty(this.image))
136 AssertAllowed();
137
138 this.image = value;
139 }
140 }
141
145 [Encrypted(32)]
146 public byte[] Secret
147 {
148 get
149 {
150 AssertAllowed();
151 return this.secret;
152 }
153
154 set
155 {
156 if (!(this.secret is null))
157 AssertAllowed();
158
159 this.secret = value;
160 }
161 }
162
166 [Encrypted(32)]
167 public string Issuer
168 {
169 get
170 {
171 AssertAllowed();
172 return this.issuer;
173 }
174
175 set
176 {
177 if (!string.IsNullOrEmpty(this.issuer))
178 AssertAllowed();
179
180 this.issuer = value;
181 }
182 }
183
187 [Encrypted(32)]
188 public string Account
189 {
190 get
191 {
192 AssertAllowed();
193 return this.account;
194 }
195
196 set
197 {
198 if (!string.IsNullOrEmpty(this.account))
199 AssertAllowed();
200
201 this.account = value;
202 }
203 }
204
208 [Encrypted(16)]
210 {
211 get
212 {
213 AssertAllowed();
214 return this.hashFunction;
215 }
216
217 set
218 {
219 if (this.hashFunction.HasValue)
220 AssertAllowed();
221
222 this.hashFunction = value;
223 }
224 }
225
229 [Encrypted(16)]
230 public int NrDigits
231 {
232 get
233 {
234 AssertAllowed();
235 return this.nrDigits ?? HotpCalculator.DefaultNrDigits;
236 }
237
238 set
239 {
240 if (this.nrDigits.HasValue)
241 AssertAllowed();
242
243 this.nrDigits = value;
244 }
245 }
246
250 [Encrypted(16)]
251 public long? Counter
252 {
253 get
254 {
255 AssertAllowed();
256 return this.counter;
257 }
258
259 set
260 {
261 if (this.counter.HasValue)
262 AssertAllowed();
263
264 this.counter = value;
265 }
266 }
267
271 [Encrypted(16)]
273 {
274 get
275 {
276 AssertAllowed();
277 return this.timeStepSeconds ?? TotpCalculator.DefaultTimeStepSeconds;
278 }
279
280 set
281 {
282 if (this.timeStepSeconds.HasValue)
283 AssertAllowed();
284
285 this.timeStepSeconds = value;
286 }
287 }
288
292 public string[] EncryptedProperties => new string[]
293 {
294 nameof(this.Label),
295 nameof(this.Description),
296 nameof(this.Secret),
297 nameof(this.Secret),
298 nameof(this.Issuer),
299 nameof(this.Account),
300 nameof(this.HashFunction),
301 nameof(this.NrDigits),
302 nameof(this.Counter),
303 nameof(this.TimeStepSeconds),
304 nameof(this.Image)
305 };
306
312 public static void SetAllowedSources(ICallStackCheck[] ApprovedSources)
313 {
314 if (!(approvedSources is null))
315 throw new NotSupportedException("Changing approved sources not permitted.");
316
317 approvedSources = ApprovedSources;
318 }
319
320 private static void AssertAllowed()
321 {
322 if (!(approvedSources is null))
323 Assert.CallFromSource(approvedSources);
324 }
325
332 internal static async Task<ExternalCredential> GetSecret(string Endpoint, CredentialAlgorithm Type)
333 {
334 return await Database.FindFirstIgnoreRest<ExternalCredential>(new FilterAnd(
336 new FilterFieldEqualTo(nameof(Type), Type)));
337 }
338
348 public static ExternalCredential TryParse(string OtpAuthUri)
349 {
350 if (!Uri.TryCreate(OtpAuthUri, UriKind.Absolute, out Uri ParsedUri))
351 return null;
352
353 return TryParse(ParsedUri);
354 }
355
365 public static ExternalCredential TryParse(Uri OtpAuthUri)
366 {
367 if (!OtpAuthUri.Scheme.Equals("otpauth", StringComparison.OrdinalIgnoreCase))
368 return null;
369
371 string Label = OtpAuthUri.AbsolutePath;
373 string Key, Value;
374 string Issuer = null;
375 string Account = null;
376 string Image = null;
377 byte[] Secret = null;
380 long? Counter = null;
381 int i;
382
383 switch (OtpAuthUri.Authority.ToLower())
384 {
385 case "totp":
387 break;
388
389 case "hotp":
391 break;
392
393 default:
394 return null;
395 }
396
397 if (Label.StartsWith('/'))
398 Label = WebUtility.UrlDecode(Label[1..]);
399
400 i = Label.IndexOf(':');
401 if (i >= 0)
402 {
403 Issuer = WebUtility.UrlDecode(Label[..i]);
404 Account = WebUtility.UrlDecode(Label[(i + 1)..]);
405 }
406
407 Key = OtpAuthUri.Query;
408 if (Key.StartsWith('?'))
409 Key = Key[1..];
410
411 foreach (string Part in Key.Split('&'))
412 {
413 i = Part.IndexOf('=');
414 if (i < 0)
415 {
416 Key = WebUtility.UrlDecode(Part);
417 Value = string.Empty;
418 }
419 else
420 {
421 Key = WebUtility.UrlDecode(Part[..i]);
422 Value = WebUtility.UrlDecode(Part[(i + 1)..]);
423 }
424
425 switch (Key.ToLower())
426 {
427 case "secret":
428 try
429 {
430 Secret = Base32.Decode(Value);
431 }
432 catch (Exception ex)
433 {
434 Log.Exception(ex);
435 return null;
436 }
437 break;
438
439 case "issuer":
440 Issuer = Value;
441 break;
442
443 case "algorithm":
444 switch (Value.ToUpper())
445 {
446 case "SHA1":
447 case "SHA-1":
448 HashFunction = Security.HashFunction.SHA1;
449 break;
450
451 case "SHA256":
452 case "SHA-256":
453 case "SHA2-256":
454 HashFunction = Security.HashFunction.SHA256;
455 break;
456
457 case "SHA384":
458 case "SHA-384":
459 case "SHA2-384":
460 HashFunction = Security.HashFunction.SHA384;
461 break;
462
463 case "SHA512":
464 case "SHA-512":
465 case "SHA2-512":
466 HashFunction = Security.HashFunction.SHA512;
467 break;
468
469 default:
470 Log.Warning("Unsupported OTP Auth URI hash algorithm: " + Value,
471 new KeyValuePair<string, object>("Algorithm", Value));
472 return null;
473 }
474 break;
475
476 case "digits":
477 if (!int.TryParse(Value, out i) || i < 6 || i > 8)
478 return null;
479
480 NrDigits = i;
481 break;
482
483 case "counter":
484 if (Type != CredentialAlgorithm.HOTP)
485 return null;
486
487 if (!long.TryParse(Value, out long l) || l < 0)
488 return null;
489
490 Counter = l;
491 break;
492
493 case "period":
494 if (Type != CredentialAlgorithm.TOTP)
495 return null;
496
497 if (!int.TryParse(Value, out i) || i <= 0)
498 return null;
499
500 TimeStepSeconds = i;
501 break;
502
503 case "image":
504 Image = Value;
505 break;
506
507 default:
508 Log.Warning("Unsupported OTP Auth URI parameter: " + Key,
509 new KeyValuePair<string, object>("Key", Key),
510 new KeyValuePair<string, object>("Value", Value));
511 return null;
512 }
513 }
514
515 if (Secret is null)
516 return null;
517
518 if (Type == CredentialAlgorithm.HOTP && !Counter.HasValue)
519 return null;
520
521 return new ExternalCredential()
522 {
523 Endpoint = string.IsNullOrEmpty(Issuer) ? Label : Issuer,
524 Type = Type,
525 Label = Label,
526 Issuer = Issuer,
529 Secret = Secret,
533 Image = Image
534 };
535 }
536
541 public static async Task<IEnumerable<ExternalCredential>> GetCredentials()
542 {
543 AssertAllowed();
544 return await Database.Find<ExternalCredential>("Endpoint");
545 }
546
550 public string Current
551 {
552 get
553 {
554 AssertAllowed();
555
556 switch (this.Type)
557 {
558 case CredentialAlgorithm.HOTP:
559 if (!this.Counter.HasValue)
560 throw new InvalidOperationException("Counter not set for HOTP credential.");
561
562 int Code = HotpCalculator.Compute(this.NrDigits, this.secret,
563 this.hashFunction ?? HotpCalculator.DefaultHashFunction, this.counter.Value);
564
565 return Code.ToString("D" + this.nrDigits.ToString());
566
567 case CredentialAlgorithm.TOTP:
568 Code = TotpCalculator.Compute(this.NrDigits, this.Secret,
569 this.hashFunction ?? HotpCalculator.DefaultHashFunction,
570 this.TimeStepSeconds, DateTime.UtcNow, TotpCalculator.DefaultT0);
571
572 return Code.ToString("D" + this.nrDigits.ToString());
573
574 case CredentialAlgorithm.UTF8:
575 return Encoding.UTF8.GetString(this.Secret);
576
577 default:
578 throw new InvalidOperationException("Unknown credential algorithm type.");
579 }
580 }
581 }
582
586 public TimeSpan? Next
587 {
588 get
589 {
590 switch (this.Type)
591 {
592 case CredentialAlgorithm.TOTP:
593 if (!this.timeStepSeconds.HasValue || !this.timeStepSeconds.HasValue)
594 return null;
595
596 DateTime Now = DateTime.UtcNow;
597 long Counter = TotpCalculator.CalcCounter(Now, this.timeStepSeconds.Value, TotpCalculator.DefaultT0);
598 DateTime Next = TotpCalculator.UnixEpoch.AddSeconds(((Counter + 1) * this.timeStepSeconds.Value) + TotpCalculator.DefaultT0);
599
600 return Next.Subtract(Now);
601
602 default:
603 return null;
604 }
605 }
606 }
607
611 public string NextLabel
612 {
613 get
614 {
615 TimeSpan? Next = this.Next;
616 if (Next.HasValue)
617 return Math.Ceiling(Next.Value.TotalSeconds).ToString() + " s";
618 else
619 return string.Empty;
620 }
621 }
622
627 public static Task<ExternalCredential> CreateAsync(string OtpAuthUri)
628 {
629 ExternalCredential Credential = TryParse(OtpAuthUri)
630 ?? throw new ArgumentException("Invalid OTP Auth URI.", nameof(OtpAuthUri));
631
632 return CreateAsync(Credential, Credential.Type, Credential.Endpoint,
633 Credential.HashFunction, Credential.Secret, Credential.Issuer,
634 Credential.Account, Credential.Label, Credential.Description,
635 Credential.Counter, Credential.NrDigits, Credential.TimeStepSeconds,
636 Credential.Image);
637 }
638
655 public static Task<ExternalCredential> CreateAsync(ExternalCredential Parsed,
657 byte[] Secret, string Issuer, string Account, string Label, string Description,
658 long? Counter, int? NrDigits, int? TimeStepSeconds)
659 {
660 return CreateAsync(Parsed, Type, EndPoint, HashFunction, Secret, Issuer,
662 }
663
681 public static async Task<ExternalCredential> CreateAsync(ExternalCredential Parsed,
683 byte[] Secret, string Issuer, string Account, string Label, string Description,
684 long? Counter, int? NrDigits, int? TimeStepSeconds, string Image)
685 {
686 int i;
687
688 if (string.IsNullOrEmpty(Label))
689 Label = Issuer + ":" + Account;
690 else if (string.IsNullOrEmpty(Issuer) && string.IsNullOrEmpty(Account))
691 {
692 i = Label.IndexOf(':');
693 if (i > 0)
694 {
695 Issuer = Label[..i];
696 Account = Label[(i + 1)..];
697 }
698 else
699 Issuer = Label;
700 }
701
702 if (string.IsNullOrEmpty(EndPoint))
703 EndPoint = Label;
704
705 using Semaphore CredentialLock = await Semaphores.BeginWrite("External Credentials");
706
707 string Suffix = string.Empty;
708 i = 1;
709 ExternalCredential Result = await Database.FindFirstIgnoreRest<ExternalCredential>(new FilterAnd(
710 new FilterFieldEqualTo(nameof(Endpoint), EndPoint + Suffix),
711 new FilterFieldEqualTo(nameof(Type), Type)));
712
713 while (!(Result is null))
714 {
715 i++;
716 Suffix = " (" + i.ToString() + ")";
717
718 Result = await Database.FindFirstIgnoreRest<ExternalCredential>(new FilterAnd(
719 new FilterFieldEqualTo(nameof(Endpoint), EndPoint + Suffix),
720 new FilterFieldEqualTo(nameof(Type), Type)));
721 }
722
723 if (Parsed is null)
724 {
725 Result = new ExternalCredential()
726 {
727 Endpoint = EndPoint + Suffix,
728 Type = Type,
730 Secret = Secret,
731 Issuer = Issuer,
733 Label = Label,
736 Image = Image
737 };
738 }
739 else
740 {
741 Result = Parsed;
742
743 Result.Endpoint = EndPoint + Suffix;
744 Result.Type = Type;
745 Result.hashFunction = HashFunction;
746 Result.secret = Secret;
747 Result.issuer = Issuer;
748 Result.account = Account;
749 Result.label = Label;
750 Result.description = Description;
751 Result.counter = Counter;
752 Result.image = Image;
753 }
754
755 if (NrDigits.HasValue)
756 Result.nrDigits = NrDigits.Value;
757
758 if (TimeStepSeconds.HasValue)
759 Result.timeStepSeconds = TimeStepSeconds.Value;
760
761 await Database.Insert(Result);
762
763 return Result;
764 }
765
770 public static Task<ZipFile> ExportAsync(string Password)
771 {
772 return ExportAsync(null, Password);
773 }
774
780 public static async Task<ZipFile> ExportAsync(string CredentialFileName,
781 string Password)
782 {
783 StringBuilder sb = new StringBuilder();
784 XmlWriterSettings Settings = XML.WriterSettings(true, false);
785 Settings.Encoding = Encoding.UTF8;
786
787 using XmlWriter w = XmlWriter.Create(sb, Settings);
788
789 w.WriteStartDocument();
790 w.WriteStartElement("Credentials", "http://waher.se/schema/Credentials.xsd");
791
792 foreach (ExternalCredential Credential in await GetCredentials())
793 {
794 w.WriteStartElement("Credential");
795 w.WriteAttributeString("type", Credential.Type.ToString());
796 w.WriteAttributeString("endpoint", Credential.Endpoint);
797 w.WriteAttributeString("label", Credential.Label);
798 w.WriteAttributeString("description", Credential.Description);
799 w.WriteAttributeString("issuer", Credential.Issuer);
800 w.WriteAttributeString("account", Credential.Account);
801
802 if (Credential.hashFunction.HasValue)
803 w.WriteAttributeString("hashFunction", Credential.hashFunction.Value.ToString());
804
805 if (Credential.nrDigits.HasValue)
806 w.WriteAttributeString("nrDigits", Credential.nrDigits.Value.ToString());
807
808 if (Credential.counter.HasValue)
809 w.WriteAttributeString("counter", Credential.counter.Value.ToString());
810
811 if (Credential.timeStepSeconds.HasValue)
812 w.WriteAttributeString("timeStepSeconds", Credential.timeStepSeconds.Value.ToString());
813
814 if (!string.IsNullOrEmpty(Credential.Image))
815 w.WriteAttributeString("image", Credential.Image);
816
817 w.WriteAttributeString("secret", Convert.ToBase64String(Credential.Secret));
818 w.WriteEndElement();
819 }
820
821 w.WriteEndElement();
822 w.WriteEndDocument();
823 w.Flush();
824
825 if (string.IsNullOrEmpty(CredentialFileName))
826 CredentialFileName = "Credentials.xml";
827
828 string Xml = sb.ToString();
829 byte[] Bin = Encoding.UTF8.GetBytes(Xml);
830 byte[] Archive = await Zip.CreateZipFile(CredentialFileName, Bin, Password,
831 ZipEncryption.Aes256Ae2);
832
833 return new ZipFile(Archive);
834 }
835 }
836}
Static class that does BASE32 encoding and decoding as defined in RFC4648: https://datatracker....
Definition: Base32.cs:11
static byte[] Decode(string Base32)
Converts a Base32-encoded string to its binary representation.
Definition: Base32.cs:19
Helps with common XML-related tasks.
Definition: XML.cs:21
static XmlWriterSettings WriterSettings(bool Indent, bool OmitXmlDeclaration)
Gets an XML writer settings object.
Definition: XML.cs:1351
Encapsulates a ZIP File
Definition: ZipFile.cs:7
Static class for creating ZIP files.
Definition: Zip.cs:22
static Task CreateZipFile(string SourceFileName, string OutputFileName)
Creates a ZIP file containing a single file.
Definition: Zip.cs:33
Static class managing the application event log. Applications and services log events on this static ...
Definition: Log.cs:14
static void Exception(Exception Exception, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, params KeyValuePair< string, object >[] Tags)
Logs an exception. Event type will be determined by the severity of the exception.
Definition: Log.cs:1657
static void Warning(string Message, string Object, string Actor, string EventId, EventLevel Level, string Facility, string Module, string StackTrace, params KeyValuePair< string, object >[] Tags)
Logs a warning event.
Definition: Log.cs:576
Static interface for database persistence. In order to work, a database provider has to be assigned t...
Definition: Database.cs:21
static Task< IEnumerable< object > > Find(string Collection, params string[] SortOrder)
Finds objects in a given collection.
Definition: Database.cs:238
static async Task Insert(object Object)
Inserts an object into the default collection of the database.
Definition: Database.cs:97
This filter selects objects that conform to all child-filters provided.
Definition: FilterAnd.cs:10
This filter selects objects that have a named field equal to a given value.
Represents a named semaphore, i.e. an object, identified by a name, that allows single concurrent wri...
Definition: Semaphore.cs:19
Static class of application-wide semaphores that can be used to order access to editable objects.
Definition: Semaphores.cs:17
static async Task< Semaphore > BeginWrite(string Key)
Waits until the semaphore identified by Key is ready for writing. Each call to BeginWrite must be fo...
Definition: Semaphores.cs:91
Static class containing methods that can be used to make sure calls are made from appropriate locatio...
Definition: Assert.cs:15
static void CallFromSource(params string[] Sources)
Makes sure the call is made from one of the listed sources.
Definition: Assert.cs:54
Contains OTP secret information for an OTP endpoint.
TimeSpan? Next
When current pass code changes.
string Current
Gets the next password for the endpoint.
string[] EncryptedProperties
Array of properties that are encrypted.
string Description
Optional description of the endpoint.
string Label
Optional name of the endpoint.
static ExternalCredential TryParse(string OtpAuthUri)
Tries to parse an OTP Auth URI (otpauth://).
static Task< ExternalCredential > CreateAsync(ExternalCredential Parsed, CredentialAlgorithm Type, string EndPoint, HashFunction? HashFunction, byte[] Secret, string Issuer, string Account, string Label, string Description, long? Counter, int? NrDigits, int? TimeStepSeconds)
Creates a credential.
static async Task< ZipFile > ExportAsync(string CredentialFileName, string Password)
Exports credentials to a password-protected ZIP file.
string NextLabel
Label for when current pass code changes.
static void SetAllowedSources(ICallStackCheck[] ApprovedSources)
If access to sensitive methods is only accessible from a set of approved sources.
static Task< ExternalCredential > CreateAsync(string OtpAuthUri)
Creates a credential.
static ExternalCredential TryParse(Uri OtpAuthUri)
Tries to parse an OTP Auth URI (otpauth://).
static Task< ZipFile > ExportAsync(string Password)
Exports credentials to a password-protected ZIP file.
HashFunction? HashFunction
Hash function used for the endpoint.
string Image
Optional image URI of the endpoint.
static async Task< ExternalCredential > CreateAsync(ExternalCredential Parsed, CredentialAlgorithm Type, string EndPoint, HashFunction? HashFunction, byte[] Secret, string Issuer, string Account, string Label, string Description, long? Counter, int? NrDigits, int? TimeStepSeconds, string Image)
Creates a credential.
static async Task< IEnumerable< ExternalCredential > > GetCredentials()
Gets stored credentials.
CredentialAlgorithm Type
Credential Algorithm type.
Implements the HOTP calculator algorithm, as defined in RFC 4226: https://datatracker....
int Compute(long Counter)
Calculates the expected one-time-password for the given counter value.
const int DefaultNrDigits
Default number of digits (6).
const HashFunction DefaultHashFunction
Default Hash Function (SHA-1)
Implements the TOTP calculator algorithm, as defined in RFC 6238: https://datatracker....
static readonly DateTime UnixEpoch
Unix Date and Time epoch, starting at 1970-01-01T00:00:00Z
const long DefaultT0
Default time when starting counting steps (0).
int Compute()
Calculates the expected one-time-password for the given counter value.
static long CalcCounter(DateTime Timestamp, int TimeStepSeconds, long T0)
Calculates the counter number for use with the HOTP algorithm.
const int DefaultTimeStepSeconds
Default time-step, in seconds (30).
Interface for objects containing encrypted properties. Mark the properties that are encrypted with th...
Interface for call stack checks.
ZipEncryption
Enumeration containing ZIP Encryption methods.
Definition: ZipEncryption.cs:9
TypeNameSerialization
How the type name should be serialized.
CredentialAlgorithm
Specifies the type of external credential algorithm to use.
HashFunction
Hash method enumeration.
Definition: Hashes.cs:26