From b9c73c2cf77f2445c2bd86ef6efdce8ad2377e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 18 Oct 2022 23:12:27 +0200 Subject: [PATCH 01/14] first draft --- docu/RoadMap_2022-2023.md | 137 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docu/RoadMap_2022-2023.md diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md new file mode 100644 index 000000000..8a29fcbb5 --- /dev/null +++ b/docu/RoadMap_2022-2023.md @@ -0,0 +1,137 @@ +# Roadmap 2022 / 2023 + +## unsortierte Sammlung von Themen + +1. backend access layer + + - Refactoring der Resolver-Klassen + - Daten-Zugriffschicht zur Kapselung der DB-Schicht + - Transfer-Datenmodel zum Austausch von Daten zwischen den Schichten + - technisches Transaktion-Handling und Lösung von Deadlocks + - Konzept in Arbeit +2. capturing alias + + - Konzept fertig + - Änderungen in Register- und Login-Prozess +3. Passwort-Verschlüsselung + + - Konzept fertig + - Unabhängigkeit von Email erzeugen + - Änderung der User-Email ermöglichen + - Versionierung der verwendeten Verschlüsselungslogik notwendig +4. Contribution-Categories + + - Bewertung und Kategorisierung von Schöpfungen: Was hat Wer für Wen geleistet? + - Regeln auf Categories ermöglichen + - Konzept in Arbeit +5. Statistics / Analysen +6. Subgruppierung / Subcommunities + + - **einfacher Ansatz:** innerhalb der existierenden Community gibt es Untergruppierungen, sprich SubCommunities + + - Einführung eine Community-Tabelle + - In der Community-Tabelle gibt es zunächst eine Haupt-Community, die mehrere Sub-Communities haben kann + - ein User ist in der Haupt-Community unique, kann aber in mehreren SubCommunities sein + - Eine SubCommunity dient zur einfachen Gruppierung gleichgesinnter oder örtlich gebundener User + - Eine SubCommunity hat eigene Moderatoren + - Motivation einer SubCommunity: kleine lokale Gruppen und jeder kennt jeden + - **ToDos**: + - DB-Migration für Community-Tabelle, User-SubCommunity-Zuordnungen, UserRights-Tabelle + - Berechtigungen für SubCommunities + - Register- und Login-Prozess für SubCommunity-Anmeldung anpassen + - Auswahl-Box einer SubCommunity + - createUser mit Zuordnung zur ausgewählten SubCommunity + - Schöpfungsprozess auf angemeldete SubCommunity anpassen + - "Beitrag einreichen"-Dialog auf angemeldete SubCommunity anpassen + - "meine Beiträge zum Gemeinwohl" mit Filter auf angemeldete SubCommunity anpassen + - "Gemeinschaft"-Dialog auf angemeldete SubCommunity anpassen + - "Mein Profil"-Dialog auf SubCommunities anpassen + - Umzug-Service in andere SubCommunity + - Löschen der Mitgliedschaft zu angemeldeter SubCommunity (Deaktivierung der Zuordnung "User-SubCommunity") + - "Senden"-Dialog mit SubCommunity-Auswahl + - "Transaktion"-Dialog mit Filter auf angemeldeter SubCommunity + - AdminInterface auf angemeldete SubCommunity anpassen + - "Übersicht"-Dialog mit Filter auf angemeldete SubCommunity + - "Nutzersuche"-Dialog mit Filter auf angemeldete SubCommunity + - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete SubComunity + - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete SubCommunity anpassen + - **komplexer Ansatz** + + - DB-Migration + + - Community-Tabelle mit Eintrag für Haupt-Community + - Community-User Zuordnungstabelle + - Account-Tabelle + - Account-User Zuordnungstabelle + - SubCommunity-Verwaltung + + - Neuanlage + - Änderungen + - Berechtigungen (Admin, Moderator, User, ...) + - Konten für 2te und 3te Schöpfung + - User-Community-Verwaltung + + - Zuordnung zu einer SubCommunity bei Register bzw Login + - Umzug in andere SubCommunity + - Eindeutigkeit Community-übergreifend? + - User-Account-Verwaltung + + - Berechtigung auf Accounts anderer User (Treuhander) + - Transaktions- und Schöpfungslogik auf Multi-Community anpassen +7. User-Beziehungen und Favoritenverwaltung + + - User-User-Zuordnung + - aus Tx-Liste die aktuellen Favoriten ermitteln + - Verwaltung von Zuordnungen + - Auswahl + - Berechtigungen + - Gruppierung + - Community-übergreifend + - User-Beziehungen +8. technische Ablösung der Email und Ersatz durch GradidoID +9. Zeitzone + + - User sieht immer seine Locale-Zeit und Monate + - Admin sieht immer UTC-Zeit und Monate + - wichtiges Kriterium für Schöpfung ist das TargetDate + - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! + - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? + - Contribution-Link aktiviert in Tokyo am Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung + - Gebucht wird die TX mit creationDate=31.10.2022 22:00:00 UTC, + - die Schöpfung hat creationDate=31.10.2022 22:00:00 UTC und contributionDate=01.11.2022 07:00:00 und neu contributionOffset=+09:00 + - **Prüfung auf -12h <= ClientRequestTime <= +12h** + - original ClientRequestTime in DB speichern + - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 + - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! + - 31.10.2022 22:00 +09:00 => 10.2022 + - 01.11.2022 07:00 +09:00 => 11.2022 +10. Layout +11. Manuelle User-Registrierung für Admin + + 1. 10.12.2022 Tag bei den Galliern +12. Dezentralisierung / Federation + + 1. Hyperswarm + 2. + +## Priorisierung + +1. capturing alias +2. Manuelle User-Registrierung für Admin (10.12.2022) **Konzeption!!**! +3. Zeitzone +4. User-Beziehungen und Favoritenverwaltung +5. Layout +6. Passwort-Verschlüsselung +7. +8. Subgruppierung / Subcommunities (einfacher Ansatz) +9. Contribution-Categories +10. +11. backend access layer +12. +13. Statistics / Analysen +14. +15. technische Ablösung der Email und Ersatz durch GradidoID +16. +17. Dezentralisierung / Federation + +## Zeitleiste From f0187c130119a4a9711e6b802c6ba47071d69bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 18 Oct 2022 23:55:22 +0200 Subject: [PATCH 02/14] first draft after meeting --- docu/RoadMap_2022-2023.md | 92 ++++++++++++-------------- docu/graphics/RoadMap2022-2023.drawio | 60 +++++++++++++++++ docu/graphics/RoadMap2022-2023.png | Bin 0 -> 64646 bytes 3 files changed, 103 insertions(+), 49 deletions(-) create mode 100644 docu/graphics/RoadMap2022-2023.drawio create mode 100644 docu/graphics/RoadMap2022-2023.png diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md index 8a29fcbb5..26a4ec41e 100644 --- a/docu/RoadMap_2022-2023.md +++ b/docu/RoadMap_2022-2023.md @@ -55,29 +55,6 @@ - "Nutzersuche"-Dialog mit Filter auf angemeldete SubCommunity - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete SubComunity - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete SubCommunity anpassen - - **komplexer Ansatz** - - - DB-Migration - - - Community-Tabelle mit Eintrag für Haupt-Community - - Community-User Zuordnungstabelle - - Account-Tabelle - - Account-User Zuordnungstabelle - - SubCommunity-Verwaltung - - - Neuanlage - - Änderungen - - Berechtigungen (Admin, Moderator, User, ...) - - Konten für 2te und 3te Schöpfung - - User-Community-Verwaltung - - - Zuordnung zu einer SubCommunity bei Register bzw Login - - Umzug in andere SubCommunity - - Eindeutigkeit Community-übergreifend? - - User-Account-Verwaltung - - - Berechtigung auf Accounts anderer User (Treuhander) - - Transaktions- und Schöpfungslogik auf Multi-Community anpassen 7. User-Beziehungen und Favoritenverwaltung - User-User-Zuordnung @@ -89,30 +66,47 @@ - Community-übergreifend - User-Beziehungen 8. technische Ablösung der Email und Ersatz durch GradidoID + + * APIs / Links / etc mit Email anpassen, so dass keine Email mehr verwendet wird + * Email soll aber im Aussen für User optional noch verwendbar bleiben + * Intern erfolgt aber auf jedenfall ein Mapping auf GradidoID egal ob per Email oder Alias angefragt wird 9. Zeitzone - User sieht immer seine Locale-Zeit und Monate - Admin sieht immer UTC-Zeit und Monate - - wichtiges Kriterium für Schöpfung ist das TargetDate - - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! - - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? - - Contribution-Link aktiviert in Tokyo am Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung - - Gebucht wird die TX mit creationDate=31.10.2022 22:00:00 UTC, - - die Schöpfung hat creationDate=31.10.2022 22:00:00 UTC und contributionDate=01.11.2022 07:00:00 und neu contributionOffset=+09:00 - - **Prüfung auf -12h <= ClientRequestTime <= +12h** - - original ClientRequestTime in DB speichern - - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 - - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! - - 31.10.2022 22:00 +09:00 => 10.2022 - - 01.11.2022 07:00 +09:00 => 11.2022 + - wichtiges Kriterium für Schöpfung ist das TargetDate ( heißt in DB contributionDate) + - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! **(Ist-Zustand)** + - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? Ja + - Beispiel: User in Tokyo Locale mit Offest +09:00 + + - aktiviert Contribution-Link mit Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung + - die Contribution wird gespeichert mit + + - creationDate=31.10.2022 22:00:00 UTC + - contributionDate=01.11.2022 07:00:00 + - (neu) clientRequestTime=01.11.2022 07:00:00+09:00 + - durch automatische Bestätigung und sofortiger Transaktion wird die TX gespeichert mit + + - creationDate=31.10.2022 22:00:00 UTC + - **zwingende Prüfung aller Requeste: auf -12h <= ClientRequestTime <= +12h** + - zur Analyse und Problemverfolgung von Contributions immer original ClientRequestTime mit Offset in DB speichern + - Beispiel für täglichen Contribution-Link während des Monats: + + - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 + - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! + - Beispiel für täglichen Contribution-Link am Monatswechsel: + + - 31.10.2022 22:00 +09:00 => 31.10.2022 UTC: 31.10.2022 15:00 UTC => 31.10.2022 + - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!! 10. Layout 11. Manuelle User-Registrierung für Admin - 1. 10.12.2022 Tag bei den Galliern + - soll am 10.12.2022 für den Tag bei den Galliern produktiv sein 12. Dezentralisierung / Federation - 1. Hyperswarm - 2. + - Hyperswarm + - Authentifizierungs- und Autorisierungs-Handshake + - Inter-Community-Communication ## Priorisierung @@ -122,16 +116,16 @@ 4. User-Beziehungen und Favoritenverwaltung 5. Layout 6. Passwort-Verschlüsselung -7. -8. Subgruppierung / Subcommunities (einfacher Ansatz) -9. Contribution-Categories -10. -11. backend access layer -12. -13. Statistics / Analysen -14. -15. technische Ablösung der Email und Ersatz durch GradidoID -16. -17. Dezentralisierung / Federation +7. Subgruppierung / Subcommunities (einfacher Ansatz) +8. Contribution-Categories +9. backend access layer +10. Statistics / Analysen +11. technische Ablösung der Email und Ersatz durch GradidoID +12. Dezentralisierung / Federation ## Zeitleiste + + + + +![img](./graphics/RoadMap2022-2023.png) diff --git a/docu/graphics/RoadMap2022-2023.drawio b/docu/graphics/RoadMap2022-2023.drawio new file mode 100644 index 000000000..58b8dec94 --- /dev/null +++ b/docu/graphics/RoadMap2022-2023.drawio @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/graphics/RoadMap2022-2023.png b/docu/graphics/RoadMap2022-2023.png new file mode 100644 index 0000000000000000000000000000000000000000..3ce8511a346c0cec5a9293a0da6b5ddc6b1d8509 GIT binary patch literal 64646 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 Date: Tue, 25 Oct 2022 22:10:58 +0200 Subject: [PATCH 03/14] Ergebnisse aus Architekur-Mtg vom 25.10.2022 --- docu/RoadMap_2022-2023.md | 201 ++++++++++++++++++++++---------------- 1 file changed, 118 insertions(+), 83 deletions(-) diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md index 26a4ec41e..be3e197bd 100644 --- a/docu/RoadMap_2022-2023.md +++ b/docu/RoadMap_2022-2023.md @@ -13,49 +13,47 @@ - Konzept fertig - Änderungen in Register- und Login-Prozess -3. Passwort-Verschlüsselung +3. Passwort-Verschlüsselung: Refactoring - - Konzept fertig - - Unabhängigkeit von Email erzeugen - - Änderung der User-Email ermöglichen - - Versionierung der verwendeten Verschlüsselungslogik notwendig -4. Contribution-Categories + - Konzept aufteilen in Ausbaustufen + - Altlasten entsorgen + - Versionierung/Typisierung der verwendeten Verschlüsselungslogik notwendig + - DB-Migration auf encryptionType=EMAIL +4. Passwort-Verschlüsselung: Login mit impliziter Neuverschlüsselung + + * Logik der Passwortverschlüsselung auf GradidoID einführen + * bei Login mit encryptionType=Email oder OneTime triggern einer Neuverschlüsselung per GradidoID + * Unabhängigkeit von Email erzeugen + * Änderung der User-Email ermöglichen +5. Contribution-Categories - Bewertung und Kategorisierung von Schöpfungen: Was hat Wer für Wen geleistet? - Regeln auf Categories ermöglichen - Konzept in Arbeit -5. Statistics / Analysen -6. Subgruppierung / Subcommunities +6. Statistics / Analysen +7. Contribution-Link editieren +8. User-Tagging - - **einfacher Ansatz:** innerhalb der existierenden Community gibt es Untergruppierungen, sprich SubCommunities + - Eine UserTag dient zur einfachen Gruppierung gleichgesinnter oder örtlich gebundener User + - Motivation des User-Taggings: bilden kleinerer lokaler User-Gruppen und jeder kennt jeden + - Einführung einer UserTaggings-Tabelle und eine User-UserTaggings-Zuordnungs-Tabelle + - Ein Moderator kann im AdminInterface die Liste der UserTags pflegen - - Einführung eine Community-Tabelle - - In der Community-Tabelle gibt es zunächst eine Haupt-Community, die mehrere Sub-Communities haben kann - - ein User ist in der Haupt-Community unique, kann aber in mehreren SubCommunities sein - - Eine SubCommunity dient zur einfachen Gruppierung gleichgesinnter oder örtlich gebundener User - - Eine SubCommunity hat eigene Moderatoren - - Motivation einer SubCommunity: kleine lokale Gruppen und jeder kennt jeden - - **ToDos**: - - DB-Migration für Community-Tabelle, User-SubCommunity-Zuordnungen, UserRights-Tabelle - - Berechtigungen für SubCommunities - - Register- und Login-Prozess für SubCommunity-Anmeldung anpassen - - Auswahl-Box einer SubCommunity - - createUser mit Zuordnung zur ausgewählten SubCommunity - - Schöpfungsprozess auf angemeldete SubCommunity anpassen - - "Beitrag einreichen"-Dialog auf angemeldete SubCommunity anpassen - - "meine Beiträge zum Gemeinwohl" mit Filter auf angemeldete SubCommunity anpassen - - "Gemeinschaft"-Dialog auf angemeldete SubCommunity anpassen - - "Mein Profil"-Dialog auf SubCommunities anpassen - - Umzug-Service in andere SubCommunity - - Löschen der Mitgliedschaft zu angemeldeter SubCommunity (Deaktivierung der Zuordnung "User-SubCommunity") - - "Senden"-Dialog mit SubCommunity-Auswahl - - "Transaktion"-Dialog mit Filter auf angemeldeter SubCommunity - - AdminInterface auf angemeldete SubCommunity anpassen - - "Übersicht"-Dialog mit Filter auf angemeldete SubCommunity - - "Nutzersuche"-Dialog mit Filter auf angemeldete SubCommunity - - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete SubComunity - - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete SubCommunity anpassen -7. User-Beziehungen und Favoritenverwaltung + - neues TAG anlegen + - vorhandenes TAG umbenennen + - ein TAG löschen, sofern kein User mehr diesem TAG zugeordnet ist + - Will ein User ein TAG zugeordnet werden, so kann dies nur ein Moderator im AdminInterface tun + - Ein Moderator kann im AdminInterface + + - ein TAG einem User zuordnen + - ein TAG von einem User entfernen + - wichtige UseCases: + + - Zuordnung eines Users zu einem TAG durch einen Moderator + - TAG spezifische Schöpfung + - User muss für seinen Beitrag ein TAG auswählen können, dem er zuvor zugeordnet wurde + - TAG-Moderator kann den Beitrag bestätigen, weil er den User mit dem TAG (persönlich) kennt +9. User-Beziehungen und Favoritenverwaltung - User-User-Zuordnung - aus Tx-Liste die aktuellen Favoriten ermitteln @@ -65,67 +63,104 @@ - Gruppierung - Community-übergreifend - User-Beziehungen -8. technische Ablösung der Email und Ersatz durch GradidoID +10. technische Ablösung der Email und Ersatz durch GradidoID - * APIs / Links / etc mit Email anpassen, so dass keine Email mehr verwendet wird - * Email soll aber im Aussen für User optional noch verwendbar bleiben - * Intern erfolgt aber auf jedenfall ein Mapping auf GradidoID egal ob per Email oder Alias angefragt wird -9. Zeitzone + * APIs / Links / etc mit Email anpassen, so dass keine Email mehr verwendet wird + * Email soll aber im Aussen für User optional noch verwendbar bleiben + * Intern erfolgt aber auf jedenfall ein Mapping auf GradidoID egal ob per Email oder Alias angefragt wird +11. Zeitzone - - User sieht immer seine Locale-Zeit und Monate - - Admin sieht immer UTC-Zeit und Monate - - wichtiges Kriterium für Schöpfung ist das TargetDate ( heißt in DB contributionDate) - - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! **(Ist-Zustand)** - - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? Ja - - Beispiel: User in Tokyo Locale mit Offest +09:00 + - User sieht immer seine Locale-Zeit und Monate + - Admin sieht immer UTC-Zeit und Monate + - wichtiges Kriterium für Schöpfung ist das TargetDate ( heißt in DB contributionDate) + - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! **(Ist-Zustand)** + - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? Ja + - Beispiel: User in Tokyo Locale mit Offest +09:00 - - aktiviert Contribution-Link mit Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung - - die Contribution wird gespeichert mit + - aktiviert Contribution-Link mit Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung + - die Contribution wird gespeichert mit - - creationDate=31.10.2022 22:00:00 UTC - - contributionDate=01.11.2022 07:00:00 - - (neu) clientRequestTime=01.11.2022 07:00:00+09:00 - - durch automatische Bestätigung und sofortiger Transaktion wird die TX gespeichert mit + - creationDate=31.10.2022 22:00:00 UTC + - contributionDate=01.11.2022 07:00:00 + - (neu) clientRequestTime=01.11.2022 07:00:00+09:00 + - durch automatische Bestätigung und sofortiger Transaktion wird die TX gespeichert mit - - creationDate=31.10.2022 22:00:00 UTC - - **zwingende Prüfung aller Requeste: auf -12h <= ClientRequestTime <= +12h** - - zur Analyse und Problemverfolgung von Contributions immer original ClientRequestTime mit Offset in DB speichern - - Beispiel für täglichen Contribution-Link während des Monats: + - creationDate=31.10.2022 22:00:00 UTC + - **zwingende Prüfung aller Requeste: auf -12h <= ClientRequestTime <= +12h** - - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 - - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! - - Beispiel für täglichen Contribution-Link am Monatswechsel: + - Prüfung auf Sommerzeiten und exotische Länder beachten + - + - zur Analyse und Problemverfolgung von Contributions immer original ClientRequestTime mit Offset in DB speichern + - Beispiel für täglichen Contribution-Link während des Monats: - - 31.10.2022 22:00 +09:00 => 31.10.2022 UTC: 31.10.2022 15:00 UTC => 31.10.2022 - - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!! -10. Layout -11. Manuelle User-Registrierung für Admin + - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 + - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! + - Beispiel für täglichen Contribution-Link am Monatswechsel: + + - 31.10.2022 22:00 +09:00 => 31.10.2022 UTC: 31.10.2022 15:00 UTC => 31.10.2022 + - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!! +12. Layout +13. Lastschriften-Link +14. Registrierung mit Redeem-Link: bei inaktivem Konto keine Buchung möglich + + 1. speichern des Links zusammen mit OptIn-Code + 2. +15. Manuelle User-Registrierung für Admin - soll am 10.12.2022 für den Tag bei den Galliern produktiv sein -12. Dezentralisierung / Federation +16. Dezentralisierung / Federation - Hyperswarm + + - funktioniert schon im Prototyp + - alle Instanzen finden sich gegenseitig + - ToDo: + - Infos aus HyperSwarm in der Community speichern + - Prüfung ob neue mir noch unbekannte Community hinzugekommen ist? + - Triggern der Authentifizierungs- und Autorisierungs-Handshake für neue Community - Authentifizierungs- und Autorisierungs-Handshake - Inter-Community-Communication + - **ToDos**: + + - DB-Migration für Community-Tabelle, User-Community-Zuordnungen, UserRights-Tabelle + - Berechtigungen für Communities + - Register- und Login-Prozess für Community-Anmeldung anpassen + + - Auswahl-Box einer Community + - createUser mit Zuordnung zur ausgewählten Community + - Schöpfungsprozess auf angemeldete Community anpassen + + - "Beitrag einreichen"-Dialog auf angemeldete Community anpassen + - "meine Beiträge zum Gemeinwohl" mit Filter auf angemeldete Community anpassen + - "Gemeinschaft"-Dialog auf angemeldete Community anpassen + - "Mein Profil"-Dialog auf Communities anpassen + + - Umzug-Service in andere Community + - Löschen der Mitgliedschaft zu angemeldeter Community (Deaktivierung der Zuordnung "User-Community") + - "Senden"-Dialog mit Community-Auswahl + - "Transaktion"-Dialog mit Filter auf angemeldeter Community + - AdminInterface auf angemeldete Community anpassen + + - "Übersicht"-Dialog mit Filter auf angemeldete Community + - "Nutzersuche"-Dialog mit Filter auf angemeldete Community + - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete Comunity + - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete Community anpassen ## Priorisierung -1. capturing alias -2. Manuelle User-Registrierung für Admin (10.12.2022) **Konzeption!!**! -3. Zeitzone -4. User-Beziehungen und Favoritenverwaltung +1. Contribution-Link editieren (vlt schon im vorherigen Bugfix-Release Ende Okt. 2022 fertig) +2. Passwort-Verschlüsselung: Refactoring **Konzeption fertig!!**! +3. Manuelle User-Registrierung für Admin (10.12.2022) **Konzeption ongoing!!**! +4. Passwort-Verschlüsselung: implizite Login-Neuverschlüsselung **Konzeption fertig!!**! 5. Layout -6. Passwort-Verschlüsselung -7. Subgruppierung / Subcommunities (einfacher Ansatz) -8. Contribution-Categories -9. backend access layer -10. Statistics / Analysen -11. technische Ablösung der Email und Ersatz durch GradidoID -12. Dezentralisierung / Federation - -## Zeitleiste - - - - -![img](./graphics/RoadMap2022-2023.png) +6. Zeitzone +7. Dezentralisierung / Federation +8. capturing alias **Konzeption fertig!!**! +9. Registrierung mit Redeem-Link: bei inaktivem Konto keine Buchung möglich +10. Subgruppierung / User-Tagging (einfacher Ansatz) +11. backend access layer +12. technische Ablösung der Email und Ersatz durch GradidoID +13. User-Beziehungen und Favoritenverwaltung +14. Lastschriften-Link +15. Contribution-Categories +16. Statistics / Analysen From 77227355d8760d8e481b04f66706c53c933d2256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 25 Oct 2022 22:27:30 +0200 Subject: [PATCH 04/14] =?UTF-8?q?kleine=20=C3=84nderungen=20u=20Erg=C3=A4n?= =?UTF-8?q?zungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docu/RoadMap_2022-2023.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md index be3e197bd..fa8573448 100644 --- a/docu/RoadMap_2022-2023.md +++ b/docu/RoadMap_2022-2023.md @@ -101,10 +101,11 @@ - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!! 12. Layout 13. Lastschriften-Link -14. Registrierung mit Redeem-Link: bei inaktivem Konto keine Buchung möglich +14. Registrierung mit Redeem-Link: - 1. speichern des Links zusammen mit OptIn-Code - 2. + * bei inaktivem Konto, sprich bisher noch keine Email-Bestätigung, keine Buchung möglich + * somit speichern des Links zusammen mit OptIn-Code + * damit kann in einem Resend der ConfirmationEmail der Link auch korrekt wieder mitgeliefert werden 15. Manuelle User-Registrierung für Admin - soll am 10.12.2022 für den Tag bei den Galliern produktiv sein From 73ced2291e0cff0e0c5263520854ebf7283d244a Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 8 Nov 2022 13:41:33 +0100 Subject: [PATCH 05/14] database migration added --- backend/src/config/index.ts | 2 +- .../0053-change_password_encryption/User.ts | 127 ++++++++++++++++++ .../UserContact.ts | 66 +++++++++ database/entity/User.ts | 2 +- database/entity/UserContact.ts | 2 +- .../0053-change_password_encryption | 38 ++++++ 6 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 database/entity/0053-change_password_encryption/User.ts create mode 100644 database/entity/0053-change_password_encryption/UserContact.ts create mode 100644 database/migrations/0053-change_password_encryption diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e7139033b..26227b90d 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0052-add_updated_at_to_contributions', + DB_VERSION: '0053-change_password_encryption', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts new file mode 100644 index 000000000..bf2d02268 --- /dev/null +++ b/database/entity/0053-change_password_encryption/User.ts @@ -0,0 +1,127 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, + OneToOne, +} from 'typeorm' +import { Contribution } from '../Contribution' +import { ContributionMessage } from '../ContributionMessage' +import { UserContact } from '../UserContact' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'gradido_id', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID: string + + @Column({ + name: 'alias', + length: 20, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + alias: string + + /* + @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) + pubKey: Buffer + @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) + privKey: Buffer + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string + */ + + @OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user) + @JoinColumn({ name: 'email_id' }) + emailContact: UserContact + + @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null }) + emailId: number | null + + @Column({ + name: 'first_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + firstName: string + + @Column({ + name: 'last_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + lastName: string + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ + name: 'password_encryption_type', + type: 'int', + unsigned: true, + nullable: false, + default: 1, + }) + passwordEncryptionType: number + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null }) + isAdmin: Date | null + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ + name: 'contribution_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + contributionLinkId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] + + @OneToMany(() => ContributionMessage, (message) => message.user) + @JoinColumn({ name: 'user_id' }) + messages?: ContributionMessage[] + + @OneToMany(() => UserContact, (userContact: UserContact) => userContact.user) + @JoinColumn({ name: 'user_id' }) + userContacts?: UserContact[] +} diff --git a/database/entity/0053-change_password_encryption/UserContact.ts b/database/entity/0053-change_password_encryption/UserContact.ts new file mode 100644 index 000000000..05bfdfffe --- /dev/null +++ b/database/entity/0053-change_password_encryption/UserContact.ts @@ -0,0 +1,66 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToOne, + JoinColumn, + ManyToOne, +} from 'typeorm' +import { User } from './User' + +@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class UserContact extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'type', + length: 100, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + type: string + + @OneToOne(() => User, (user) => user.emailContact) + user: User + + @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false }) + userId: number + + @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) + email: string + + @Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true }) + emailVerificationCode: BigInt + + @Column({ name: 'email_opt_in_type_id' }) + emailOptInTypeId: number + + @Column({ name: 'email_resend_count' }) + emailResendCount: number + + // @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) + // emailHash: Buffer + + @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false }) + emailChecked: boolean + + @Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' }) + phone: string + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' }) + updatedAt: Date | null + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null + + @ManyToOne(() => User, (user) => user.userContacts) + @JoinColumn({ name: 'user_id' }) + contactUser: User +} diff --git a/database/entity/User.ts b/database/entity/User.ts index d073f428a..b3c00a9b4 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0049-add_user_contacts_table/User' +export { User } from './0053-change_password_encryption/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index a368bb7ca..dd74e65c4 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0049-add_user_contacts_table/UserContact' +export { UserContact } from './0053-change_password_encryption/UserContact' diff --git a/database/migrations/0053-change_password_encryption b/database/migrations/0053-change_password_encryption new file mode 100644 index 000000000..1b87e2511 --- /dev/null +++ b/database/migrations/0053-change_password_encryption @@ -0,0 +1,38 @@ +/* MIGRATION TO ADD GRADIDO_ID + * + * This migration adds and renames columns to and in the table `users` + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE users RENAME COLUMN created TO created_at;') + await queryFn('ALTER TABLE users RENAME COLUMN deletedAt TO deleted_at;') + // alter table emp rename column emp_name to name + await queryFn( + 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 1 AFTER password;', + ) + + // TODO these steps comes after verification and test + /* + await queryFn('ALTER TABLE users DROP COLUMN public_key;') + await queryFn('ALTER TABLE users DROP COLUMN privkey;') + await queryFn('ALTER TABLE users DROP COLUMN email_hash;') + await queryFn('ALTER TABLE users DROP COLUMN passphrase;') + */ +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE users RENAME COLUMN created_at TO created;') + await queryFn('ALTER TABLE users RENAME COLUMN deleted_at TO deletedAt;') + await queryFn('ALTER TABLE users DROP COLUMN password_encryption_type;') + + // TODO these steps comes after verification and test + /* + await queryFn('ALTER TABLE users ADD COLUMN public_key binary(32) DEFAULT NULL;') + await queryFn('ALTER TABLE users ADD COLUMN privkey binary(80) DEFAULT NULL;') + await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;') + await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;') + */ +} \ No newline at end of file From ac6b8e666ff893d19786b51f20bff36b6c771038 Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 9 Nov 2022 10:46:01 +0100 Subject: [PATCH 06/14] encryption interface --- .../graphql/enum/PasswordEncryptionType.ts | 12 +++++ backend/src/password/EmailEncryptr.ts | 19 +++++++ backend/src/password/EncryptorUtils.ts | 52 +++++++++++++++++++ backend/src/password/GradidoIDEncryptr.ts | 19 +++++++ backend/src/password/PasswordEncryptr.ts | 6 +++ ...ion => 0053-change_password_encryption.ts} | 4 +- 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 backend/src/graphql/enum/PasswordEncryptionType.ts create mode 100644 backend/src/password/EmailEncryptr.ts create mode 100644 backend/src/password/EncryptorUtils.ts create mode 100644 backend/src/password/GradidoIDEncryptr.ts create mode 100644 backend/src/password/PasswordEncryptr.ts rename database/migrations/{0053-change_password_encryption => 0053-change_password_encryption.ts} (97%) diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts new file mode 100644 index 000000000..4f23aa693 --- /dev/null +++ b/backend/src/graphql/enum/PasswordEncryptionType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum PasswordEncryptionType { + EMAIL = 0, + ONE_TIME = 1, + GRADIDO_ID = 2, +} + +registerEnumType(PasswordEncryptionType, { + name: 'PasswordEncryptionType', // this one is mandatory + description: 'Type of the password encryption', // this one is optional +}) diff --git a/backend/src/password/EmailEncryptr.ts b/backend/src/password/EmailEncryptr.ts new file mode 100644 index 000000000..59098e207 --- /dev/null +++ b/backend/src/password/EmailEncryptr.ts @@ -0,0 +1,19 @@ +import { User } from '@entity/User' +import { PasswordEncryptr } from './PasswordEncryptr' +import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' + +export class EmailEncryptr implements PasswordEncryptr { + async encryptPassword(dbUser: User, password: string): Promise { + const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.emailContact.email, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + + return passwordHash + } + + async verifyPassword(dbUser: User, password: string): Promise { + if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { + return false + } + return true + } +} diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts new file mode 100644 index 000000000..6609ff075 --- /dev/null +++ b/backend/src/password/EncryptorUtils.ts @@ -0,0 +1,52 @@ +import CONFIG from '@/config' +import { backendLogger as logger } from '@/server/logger' + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const sodium = require('sodium-native') + +// We will reuse this for changePassword +export const isValidPassword = (password: string): boolean => { + return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/) +} + +export const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => { + logger.trace('SecretKeyCryptographyCreateKey...') + const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') + const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') + if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) { + logger.error( + `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, + ) + throw new Error( + `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, + ) + } + + const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES) + sodium.crypto_hash_sha512_init(state) + sodium.crypto_hash_sha512_update(state, Buffer.from(salt)) + sodium.crypto_hash_sha512_update(state, configLoginAppSecret) + const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES) + sodium.crypto_hash_sha512_final(state, hash) + + const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES) + const opsLimit = 10 + const memLimit = 33554432 + const algo = 2 + sodium.crypto_pwhash( + encryptionKey, + Buffer.from(password), + hash.slice(0, sodium.crypto_pwhash_SALTBYTES), + opsLimit, + memLimit, + algo, + ) + + const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) + sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) + + logger.debug( + `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`, + ) + return [encryptionKeyHash, encryptionKey] +} diff --git a/backend/src/password/GradidoIDEncryptr.ts b/backend/src/password/GradidoIDEncryptr.ts new file mode 100644 index 000000000..630bee056 --- /dev/null +++ b/backend/src/password/GradidoIDEncryptr.ts @@ -0,0 +1,19 @@ +import { User } from '@entity/User' +import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' +import { PasswordEncryptr } from './PasswordEncryptr' + +export class GradidoIDEncryptr implements PasswordEncryptr { + async encryptPassword(dbUser: User, password: string): Promise { + const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.gradidoID, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + + return passwordHash + } + + async verifyPassword(dbUser: User, password: string): Promise { + if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { + return false + } + return true + } +} diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts new file mode 100644 index 000000000..24d949be9 --- /dev/null +++ b/backend/src/password/PasswordEncryptr.ts @@ -0,0 +1,6 @@ +import { User } from '@entity/User' + +export interface PasswordEncryptr { + encryptPassword(dbUser: User, password: string): Promise + verifyPassword(dbUser: User, password: string): Promise +} diff --git a/database/migrations/0053-change_password_encryption b/database/migrations/0053-change_password_encryption.ts similarity index 97% rename from database/migrations/0053-change_password_encryption rename to database/migrations/0053-change_password_encryption.ts index 1b87e2511..5d880689f 100644 --- a/database/migrations/0053-change_password_encryption +++ b/database/migrations/0053-change_password_encryption.ts @@ -1,4 +1,4 @@ -/* MIGRATION TO ADD GRADIDO_ID +/* MIGRATION TO ADD ENCRYPTION TO PASSWORDS * * This migration adds and renames columns to and in the table `users` */ @@ -35,4 +35,4 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;') await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;') */ -} \ No newline at end of file +} From 107cc016ba0064462ccde429699a1fb711f61508 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 15 Nov 2022 20:30:09 +0100 Subject: [PATCH 07/14] set up completed and working --- .../graphql/enum/PasswordEncryptionType.ts | 4 +-- .../src/graphql/resolver/UserResolver.test.ts | 1 + backend/src/password/EmailEncryptr.ts | 19 ------------ backend/src/password/EncryptorUtils.ts | 14 +++++++++ backend/src/password/GradidoIDEncryptr.ts | 19 ------------ backend/src/password/PasswordEncryptr.ts | 30 +++++++++++++++++-- backend/src/util/communityUser.ts | 2 ++ .../0053-change_password_encryption/User.ts | 28 ++++++++--------- .../UserContact.ts | 6 ---- 9 files changed, 60 insertions(+), 63 deletions(-) delete mode 100644 backend/src/password/EmailEncryptr.ts delete mode 100644 backend/src/password/GradidoIDEncryptr.ts diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts index 4f23aa693..b3a00d748 100644 --- a/backend/src/graphql/enum/PasswordEncryptionType.ts +++ b/backend/src/graphql/enum/PasswordEncryptionType.ts @@ -1,8 +1,8 @@ import { registerEnumType } from 'type-graphql' export enum PasswordEncryptionType { - EMAIL = 0, - ONE_TIME = 1, + NO_PASSWORD = 0, + EMAIL = 1, GRADIDO_ID = 2, } diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index cf4ad8d4b..791ed4c8e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -146,6 +146,7 @@ describe('UserResolver', () => { publisherId: 1234, referrerId: null, contributionLinkId: null, + passwordEncryptionType: 1, }, ]) const valUUID = validateUUID(user[0].gradidoID) diff --git a/backend/src/password/EmailEncryptr.ts b/backend/src/password/EmailEncryptr.ts deleted file mode 100644 index 59098e207..000000000 --- a/backend/src/password/EmailEncryptr.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { User } from '@entity/User' -import { PasswordEncryptr } from './PasswordEncryptr' -import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' - -export class EmailEncryptr implements PasswordEncryptr { - async encryptPassword(dbUser: User, password: string): Promise { - const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.emailContact.email, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - - return passwordHash - } - - async verifyPassword(dbUser: User, password: string): Promise { - if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { - return false - } - return true - } -} diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 6609ff075..5f6f4b416 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -1,5 +1,7 @@ import CONFIG from '@/config' import { backendLogger as logger } from '@/server/logger' +import { User } from '@entity/User' +import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -50,3 +52,15 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): ) return [encryptionKeyHash, encryptionKey] } + +export const getBasicCryptographicKey = (dbUser: User): string | null => { + if (dbUser.passwordEncryptionType === PasswordEncryptionType.NO_PASSWORD) { + return null + } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.EMAIL) { + return dbUser.emailContact.email + } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.GRADIDO_ID) { + return dbUser.gradidoID + } + + throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) +} diff --git a/backend/src/password/GradidoIDEncryptr.ts b/backend/src/password/GradidoIDEncryptr.ts deleted file mode 100644 index 630bee056..000000000 --- a/backend/src/password/GradidoIDEncryptr.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { User } from '@entity/User' -import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' -import { PasswordEncryptr } from './PasswordEncryptr' - -export class GradidoIDEncryptr implements PasswordEncryptr { - async encryptPassword(dbUser: User, password: string): Promise { - const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.gradidoID, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - - return passwordHash - } - - async verifyPassword(dbUser: User, password: string): Promise { - if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { - return false - } - return true - } -} diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts index 24d949be9..24dc7f352 100644 --- a/backend/src/password/PasswordEncryptr.ts +++ b/backend/src/password/PasswordEncryptr.ts @@ -1,6 +1,30 @@ import { User } from '@entity/User' +import { logger } from '@test/testSetup' +import { getBasicCryptographicKey, SecretKeyCryptographyCreateKey } from './EncryptorUtils' -export interface PasswordEncryptr { - encryptPassword(dbUser: User, password: string): Promise - verifyPassword(dbUser: User, password: string): Promise +export class PasswordEncryptr { + async encryptPassword(dbUser: User, password: string): Promise { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) logger.error('Password not set for user ' + dbUser.id) + else { + const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + return passwordHash + } + + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } + + async verifyPassword(dbUser: User, password: string): Promise { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) logger.error('Password not set for user ' + dbUser.id) + else { + if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { + return false + } + return true + } + + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index e885b7043..87d0432dc 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -26,6 +26,8 @@ const communityDbUser: dbUser = { isAdmin: null, publisherId: 0, passphrase: '', + // default password encryption type + passwordEncryptionType: 2, hasId: function (): boolean { throw new Error('Function not implemented.') }, diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts index bf2d02268..fac4e1031 100644 --- a/database/entity/0053-change_password_encryption/User.ts +++ b/database/entity/0053-change_password_encryption/User.ts @@ -34,20 +34,20 @@ export class User extends BaseEntity { }) alias: string - /* - @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) - pubKey: Buffer - @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) - privKey: Buffer - @Column({ - type: 'text', - name: 'passphrase', - collation: 'utf8mb4_unicode_ci', - nullable: true, - default: null, - }) - passphrase: string - */ + @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) + pubKey: Buffer + + @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) + privKey: Buffer + + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string @OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user) @JoinColumn({ name: 'email_id' }) diff --git a/database/entity/0053-change_password_encryption/UserContact.ts b/database/entity/0053-change_password_encryption/UserContact.ts index 05bfdfffe..97b12d4cd 100644 --- a/database/entity/0053-change_password_encryption/UserContact.ts +++ b/database/entity/0053-change_password_encryption/UserContact.ts @@ -5,8 +5,6 @@ import { Column, DeleteDateColumn, OneToOne, - JoinColumn, - ManyToOne, } from 'typeorm' import { User } from './User' @@ -59,8 +57,4 @@ export class UserContact extends BaseEntity { @DeleteDateColumn({ name: 'deleted_at', nullable: true }) deletedAt: Date | null - - @ManyToOne(() => User, (user) => user.userContacts) - @JoinColumn({ name: 'user_id' }) - contactUser: User } From 47d8469eb3292edbeeebcc4bb85273058b48afaf Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 17 Nov 2022 13:38:16 +0100 Subject: [PATCH 08/14] new password implementation set up and test --- .../src/graphql/resolver/UserResolver.test.ts | 65 ++++++++++++++++- backend/src/graphql/resolver/UserResolver.ts | 71 +++++-------------- backend/src/password/PasswordEncryptr.ts | 46 ++++++------ backend/src/util/communityUser.ts | 2 +- 4 files changed, 103 insertions(+), 81 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 2d92e196c..d3bfb1c66 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -36,6 +36,10 @@ import { UserContact } from '@entity/UserContact' import { OptInType } from '../enum/OptInType' import { UserContactType } from '../enum/UserContactType' import { bobBaumeister } from '@/seeds/users/bob-baumeister' +import { encryptPassword } from '@/password/PasswordEncryptr' +import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' +import { find } from 'lodash' +import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -491,7 +495,8 @@ describe('UserResolver', () => { }) it('updates the password', () => { - expect(newUser.password).toEqual('3917921995996627700') + const encryptedPass = encryptPassword(newUser, 'Aa12345_') + expect(newUser.password.toString()).toEqual(encryptedPass.toString()) }) /* @@ -1159,6 +1164,64 @@ describe('UserResolver', () => { }) }) }) + + describe('password encryption type', () => { + describe('user just registered', () => { + let bibi: User + + it('password type should be gradido id', async () => { + const users = await User.find() + bibi = users[1] + + expect(bibi).toEqual( + expect.objectContaining({ + password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + .readBigUInt64LE() + .toString(), + passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, + }), + ) + }) + }) + + describe('user has encryption type email', () => { + const variables = { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + publisherId: 1234, + } + + let bibi: User + beforeAll(async () => { + const users = await User.find() + bibi = users[1] + + bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL + bibi.password = SecretKeyCryptographyCreateKey( + 'bibi@bloxberg.de', + 'Aa12345_', + )[0].readBigUInt64LE() + + await bibi.save() + }) + + it('changes to gradidoID on login', async () => { + await mutate({ mutation: login, variables: variables }) + + const users = await User.find() + bibi = users[0] + + expect(bibi).toEqual( + expect.objectContaining({ + password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + .readBigUInt64LE() + .toString(), + passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, + }), + ) + }) + }) + }) }) describe('printTimeDuration', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 2287ede98..78f8a96f8 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -39,17 +39,15 @@ import { SearchAdminUsersResult } from '@model/AdminUser' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { v4 as uuidv4 } from 'uuid' +import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' +import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptr' +import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') // eslint-disable-next-line @typescript-eslint/no-var-requires const random = require('random-bigint') -// We will reuse this for changePassword -const isPassword = (password: string): boolean => { - return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/) -} - const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] const DEFAULT_LANGUAGE = 'de' const isLanguage = (language: string): boolean => { @@ -106,48 +104,6 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => { return [pubKey, privKey] } -const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => { - logger.trace('SecretKeyCryptographyCreateKey...') - const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') - const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') - if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) { - logger.error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, - ) - throw new Error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, - ) - } - - const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES) - sodium.crypto_hash_sha512_init(state) - sodium.crypto_hash_sha512_update(state, Buffer.from(salt)) - sodium.crypto_hash_sha512_update(state, configLoginAppSecret) - const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES) - sodium.crypto_hash_sha512_final(state, hash) - - const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES) - const opsLimit = 10 - const memLimit = 33554432 - const algo = 2 - sodium.crypto_pwhash( - encryptionKey, - Buffer.from(password), - hash.slice(0, sodium.crypto_pwhash_SALTBYTES), - opsLimit, - memLimit, - algo, - ) - - const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) - sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) - - logger.debug( - `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`, - ) - return [encryptionKeyHash, encryptionKey] -} - /* const getEmailHash = (email: string): Buffer => { logger.trace('getEmailHash...') @@ -343,12 +299,16 @@ export class UserResolver { // TODO we want to catch this on the frontend and ask the user to check his emails or resend code throw new Error('User has no private or publicKey') } - const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash - const loginUserPassword = BigInt(dbUser.password.toString()) - if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) { + + if (!verifyPassword(dbUser, password)) { logger.error('The User has no valid credentials.') throw new Error('No user with this credentials') } + + if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) { + dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID + dbUser.password = encryptPassword(dbUser, password) + } // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message logger.addContext('user', dbUser.id) logger.debug('validation of login credentials successful...') @@ -623,7 +583,7 @@ export class UserResolver { ): Promise { logger.info(`setPassword(${code}, ***)...`) // Validate Password - if (!isPassword(password)) { + if (!isValidPassword(password)) { logger.error('Password entered is lexically invalid') throw new Error( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', @@ -681,10 +641,11 @@ export class UserResolver { userContact.emailChecked = true // Update Password + user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) - user.password = passwordHash[0].readBigUInt64LE() // using the shorthash + user.password = encryptPassword(user, password) user.pubKey = keyPair[0] user.privKey = encryptedPrivkey logger.debug('User credentials updated ...') @@ -789,7 +750,7 @@ export class UserResolver { if (password && passwordNew) { // Validate Password - if (!isPassword(passwordNew)) { + if (!isValidPassword(passwordNew)) { logger.error('newPassword does not fullfil the rules') throw new Error( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', @@ -801,7 +762,7 @@ export class UserResolver { userEntity.emailContact.email, password, ) - if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) { + if (!verifyPassword(userEntity, password)) { logger.error(`Old password is invalid`) throw new Error(`Old password is invalid`) } @@ -817,7 +778,7 @@ export class UserResolver { logger.debug('PrivateKey encrypted...') // Save new password hash and newly encrypted private key - userEntity.password = newPasswordHash[0].readBigUInt64LE() + userEntity.password = encryptPassword(userEntity, passwordNew) userEntity.privKey = encryptedPrivkey } diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts index 24dc7f352..b8ef2de31 100644 --- a/backend/src/password/PasswordEncryptr.ts +++ b/backend/src/password/PasswordEncryptr.ts @@ -1,30 +1,28 @@ import { User } from '@entity/User' -import { logger } from '@test/testSetup' +// import { logger } from '@test/testSetup' getting error "jest is not defined" import { getBasicCryptographicKey, SecretKeyCryptographyCreateKey } from './EncryptorUtils' -export class PasswordEncryptr { - async encryptPassword(dbUser: User, password: string): Promise { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) logger.error('Password not set for user ' + dbUser.id) - else { - const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - return passwordHash - } - - throw new Error('Password not set for user ' + dbUser.id) // user has no password - } - - async verifyPassword(dbUser: User, password: string): Promise { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) logger.error('Password not set for user ' + dbUser.id) - else { - if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { - return false - } - return true - } - +export const encryptPassword = (dbUser: User, password: string): bigint => { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) { + // logger.error('Password not set for user ' + dbUser.id) throw new Error('Password not set for user ' + dbUser.id) // user has no password + } else { + const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + return passwordHash + } +} + +export const verifyPassword = (dbUser: User, password: string): boolean => { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) { + // logger.error('Password not set for user ' + dbUser.id) + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } else { + if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { + return false + } + return true } } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 87d0432dc..9913418fb 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -27,7 +27,7 @@ const communityDbUser: dbUser = { publisherId: 0, passphrase: '', // default password encryption type - passwordEncryptionType: 2, + passwordEncryptionType: 0, hasId: function (): boolean { throw new Error('Function not implemented.') }, From 39d006351d8e6c04bdaa6b6025c9e7898c724326 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 17 Nov 2022 14:10:17 +0100 Subject: [PATCH 09/14] unused import added wrongly --- backend/src/graphql/resolver/UserResolver.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d3bfb1c66..03aabfbb4 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -38,7 +38,6 @@ import { UserContactType } from '../enum/UserContactType' import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { encryptPassword } from '@/password/PasswordEncryptr' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' -import { find } from 'lodash' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' // import { klicktippSignIn } from '@/apis/KlicktippController' From fc23fc9e87768e0fc5f5f57235261c18b4518f54 Mon Sep 17 00:00:00 2001 From: joseji Date: Sun, 20 Nov 2022 18:14:43 +0100 Subject: [PATCH 10/14] fixes done --- .../src/graphql/resolver/UserResolver.test.ts | 6 ++-- backend/src/graphql/resolver/UserResolver.ts | 4 ++- backend/src/password/EncryptorUtils.ts | 27 ++++++++++-------- backend/src/password/PasswordEncryptor.ts | 17 +++++++++++ backend/src/password/PasswordEncryptr.ts | 28 ------------------- .../0053-change_password_encryption/User.ts | 2 +- .../0053-change_password_encryption.ts | 22 ++------------- 7 files changed, 42 insertions(+), 64 deletions(-) create mode 100644 backend/src/password/PasswordEncryptor.ts delete mode 100644 backend/src/password/PasswordEncryptr.ts diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 03aabfbb4..a3e2dbe21 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -36,7 +36,7 @@ import { UserContact } from '@entity/UserContact' import { OptInType } from '../enum/OptInType' import { UserContactType } from '../enum/UserContactType' import { bobBaumeister } from '@/seeds/users/bob-baumeister' -import { encryptPassword } from '@/password/PasswordEncryptr' +import { encryptPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' @@ -149,7 +149,7 @@ describe('UserResolver', () => { publisherId: 1234, referrerId: null, contributionLinkId: null, - passwordEncryptionType: 1, + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, }, ]) const valUUID = validateUUID(user[0].gradidoID) @@ -1168,7 +1168,7 @@ describe('UserResolver', () => { describe('user just registered', () => { let bibi: User - it('password type should be gradido id', async () => { + it('has password type gradido id', async () => { const users = await User.find() bibi = users[1] diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index ef5edd747..a9006bbb6 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -40,7 +40,7 @@ import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { v4 as uuidv4 } from 'uuid' import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' -import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptr' +import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -432,6 +432,7 @@ export class UserResolver { dbUser.lastName = lastName dbUser.language = language dbUser.publisherId = publisherId + dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD dbUser.passphrase = passphrase.join(' ') logger.debug('new dbUser=' + dbUser) if (redeemCode) { @@ -780,6 +781,7 @@ export class UserResolver { logger.debug('PrivateKey encrypted...') // Save new password hash and newly encrypted private key + userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID userEntity.password = encryptPassword(userEntity, passwordNew) userEntity.privKey = encryptedPrivkey } diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 5f6f4b416..2ca47109d 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -47,20 +47,23 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) - logger.debug( - `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`, - ) return [encryptionKeyHash, encryptionKey] } -export const getBasicCryptographicKey = (dbUser: User): string | null => { - if (dbUser.passwordEncryptionType === PasswordEncryptionType.NO_PASSWORD) { - return null - } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.EMAIL) { - return dbUser.emailContact.email - } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.GRADIDO_ID) { - return dbUser.gradidoID +export const getUserCryptographicSalt = (dbUser: User): string => { + switch (dbUser.passwordEncryptionType) { + case PasswordEncryptionType.NO_PASSWORD: { + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } + case PasswordEncryptionType.EMAIL: { + return dbUser.emailContact.email + break + } + case PasswordEncryptionType.GRADIDO_ID: { + return dbUser.gradidoID + break + } + default: + throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) } - - throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) } diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts new file mode 100644 index 000000000..2c6ebfb0f --- /dev/null +++ b/backend/src/password/PasswordEncryptor.ts @@ -0,0 +1,17 @@ +import { User } from '@entity/User' +// import { logger } from '@test/testSetup' getting error "jest is not defined" +import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils' + +export const encryptPassword = (dbUser: User, password: string): bigint => { + const basicKey = getUserCryptographicSalt(dbUser) + const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + return passwordHash +} + +export const verifyPassword = (dbUser: User, password: string): boolean => { + if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { + return false + } + return true +} diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts deleted file mode 100644 index b8ef2de31..000000000 --- a/backend/src/password/PasswordEncryptr.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { User } from '@entity/User' -// import { logger } from '@test/testSetup' getting error "jest is not defined" -import { getBasicCryptographicKey, SecretKeyCryptographyCreateKey } from './EncryptorUtils' - -export const encryptPassword = (dbUser: User, password: string): bigint => { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) { - // logger.error('Password not set for user ' + dbUser.id) - throw new Error('Password not set for user ' + dbUser.id) // user has no password - } else { - const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - return passwordHash - } -} - -export const verifyPassword = (dbUser: User, password: string): boolean => { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) { - // logger.error('Password not set for user ' + dbUser.id) - throw new Error('Password not set for user ' + dbUser.id) // user has no password - } else { - if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { - return false - } - return true - } -} diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts index fac4e1031..2a3332925 100644 --- a/database/entity/0053-change_password_encryption/User.ts +++ b/database/entity/0053-change_password_encryption/User.ts @@ -88,7 +88,7 @@ export class User extends BaseEntity { type: 'int', unsigned: true, nullable: false, - default: 1, + default: 0, }) passwordEncryptionType: number diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts index 5d880689f..0c8632186 100644 --- a/database/migrations/0053-change_password_encryption.ts +++ b/database/migrations/0053-change_password_encryption.ts @@ -1,6 +1,6 @@ -/* MIGRATION TO ADD ENCRYPTION TO PASSWORDS +/* MIGRATION TO ADD ENCRYPTION TYPE TO PASSWORDS * - * This migration adds and renames columns to and in the table `users` + * This migration adds and renames columns in the table `users` */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ @@ -11,28 +11,12 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis await queryFn('ALTER TABLE users RENAME COLUMN deletedAt TO deleted_at;') // alter table emp rename column emp_name to name await queryFn( - 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 1 AFTER password;', + 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 0 AFTER password;', ) - - // TODO these steps comes after verification and test - /* - await queryFn('ALTER TABLE users DROP COLUMN public_key;') - await queryFn('ALTER TABLE users DROP COLUMN privkey;') - await queryFn('ALTER TABLE users DROP COLUMN email_hash;') - await queryFn('ALTER TABLE users DROP COLUMN passphrase;') - */ } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { await queryFn('ALTER TABLE users RENAME COLUMN created_at TO created;') await queryFn('ALTER TABLE users RENAME COLUMN deleted_at TO deletedAt;') await queryFn('ALTER TABLE users DROP COLUMN password_encryption_type;') - - // TODO these steps comes after verification and test - /* - await queryFn('ALTER TABLE users ADD COLUMN public_key binary(32) DEFAULT NULL;') - await queryFn('ALTER TABLE users ADD COLUMN privkey binary(80) DEFAULT NULL;') - await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;') - await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;') - */ } From 3aeb9dd0f17e8abdaaec37258bdf6b6dd2b22950 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 22 Nov 2022 11:12:07 +0100 Subject: [PATCH 11/14] fixes added --- .../src/graphql/resolver/UserResolver.test.ts | 24 +++++++++++++++++++ backend/src/password/EncryptorUtils.ts | 2 ++ backend/src/password/PasswordEncryptor.ts | 4 ++-- .../0053-change_password_encryption.ts | 2 ++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 4e05aadd6..377dfa131 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -39,6 +39,7 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { encryptPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' +import { tokenToString } from 'typescript' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -1220,6 +1221,29 @@ describe('UserResolver', () => { }), ) }) + + it('can login after password change', async () => { + resetToken() + expect(await mutate({ mutation: login, variables: variables })).toEqual( + expect.objectContaining({ + data: { + login: { + email: 'bibi@bloxberg.de', + firstName: 'Bibi', + hasElopage: false, + id: expect.any(Number), + isAdmin: null, + klickTipp: { + newsletterState: false, + }, + language: 'de', + lastName: 'Bloxberg', + publisherId: 1234, + }, + }, + }), + ) + }) }) }) }) diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 2ca47109d..971b6a32e 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -53,6 +53,7 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): export const getUserCryptographicSalt = (dbUser: User): string => { switch (dbUser.passwordEncryptionType) { case PasswordEncryptionType.NO_PASSWORD: { + logger.error('Password not set for user ' + dbUser.id) throw new Error('Password not set for user ' + dbUser.id) // user has no password } case PasswordEncryptionType.EMAIL: { @@ -64,6 +65,7 @@ export const getUserCryptographicSalt = (dbUser: User): string => { break } default: + logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) } } diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts index 2c6ebfb0f..3dc0736df 100644 --- a/backend/src/password/PasswordEncryptor.ts +++ b/backend/src/password/PasswordEncryptor.ts @@ -3,8 +3,8 @@ import { User } from '@entity/User' import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils' export const encryptPassword = (dbUser: User, password: string): bigint => { - const basicKey = getUserCryptographicSalt(dbUser) - const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const salt = getUserCryptographicSalt(dbUser) + const keyBuffer = SecretKeyCryptographyCreateKey(salt, password) // return short and long hash const passwordHash = keyBuffer[0].readBigUInt64LE() return passwordHash } diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts index 0c8632186..635109430 100644 --- a/database/migrations/0053-change_password_encryption.ts +++ b/database/migrations/0053-change_password_encryption.ts @@ -13,6 +13,8 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis await queryFn( 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 0 AFTER password;', ) + await queryFn(`UPDATE users SET password_encryption_type = 1 WHERE id IN + (SELECT user_id FROM user_contacts WHERE email_checked = 1)`) } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { From e55f516c512019b15081ad476278884b35631587 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 22 Nov 2022 11:19:42 +0100 Subject: [PATCH 12/14] removed unused import --- backend/src/graphql/resolver/UserResolver.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 377dfa131..200ba8163 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -39,7 +39,6 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { encryptPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' -import { tokenToString } from 'typescript' // import { klicktippSignIn } from '@/apis/KlicktippController' From 60e3f628325b38cfd5798bf5ebbec00b6a95c99b Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 23 Nov 2022 11:32:27 +0100 Subject: [PATCH 13/14] enum use on community user initialization --- backend/src/util/communityUser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 9913418fb..298348f0f 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { PasswordEncryptionType } from '@/graphql/enum/PasswordEncryptionType' import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' import { User as dbUser } from '@entity/User' import { UserContact } from '@entity/UserContact' @@ -27,7 +28,7 @@ const communityDbUser: dbUser = { publisherId: 0, passphrase: '', // default password encryption type - passwordEncryptionType: 0, + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, hasId: function (): boolean { throw new Error('Function not implemented.') }, From 2a74b924721257b239a0148ec7bd8b28e424a8d5 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 24 Nov 2022 11:21:47 +0100 Subject: [PATCH 14/14] user saving on db --- .../src/graphql/resolver/UserResolver.test.ts | 16 +++++++++++----- backend/src/graphql/resolver/UserResolver.ts | 1 + backend/src/password/PasswordEncryptor.ts | 5 +---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 200ba8163..d8472fba9 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1193,9 +1193,11 @@ describe('UserResolver', () => { let bibi: User beforeAll(async () => { - const users = await User.find() - bibi = users[1] - + const usercontact = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + bibi = usercontact.user bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL bibi.password = SecretKeyCryptographyCreateKey( 'bibi@bloxberg.de', @@ -1208,11 +1210,15 @@ describe('UserResolver', () => { it('changes to gradidoID on login', async () => { await mutate({ mutation: login, variables: variables }) - const users = await User.find() - bibi = users[0] + const usercontact = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + bibi = usercontact.user expect(bibi).toEqual( expect.objectContaining({ + firstName: 'Bibi', password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] .readBigUInt64LE() .toString(), diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index b376c632f..752c585fd 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -311,6 +311,7 @@ export class UserResolver { if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) { dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID dbUser.password = encryptPassword(dbUser, password) + await dbUser.save() } // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message logger.addContext('user', dbUser.id) diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts index 3dc0736df..1735106c1 100644 --- a/backend/src/password/PasswordEncryptor.ts +++ b/backend/src/password/PasswordEncryptor.ts @@ -10,8 +10,5 @@ export const encryptPassword = (dbUser: User, password: string): bigint => { } export const verifyPassword = (dbUser: User, password: string): boolean => { - if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { - return false - } - return true + return dbUser.password.toString() === encryptPassword(dbUser, password).toString() }