From 00e3cc3620adf80cb7628e7d841d3734bc035496 Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Sat, 28 May 2022 16:12:47 +0930 Subject: [PATCH] Working on initial encryption key generation & authentication --- Backend/Api/Auth/Login.go | 107 ++++++++ Backend/Api/Auth/Session.go | 79 ++++++ Backend/Api/Auth/Signup.go | 54 ++-- Backend/Api/Routes.go | 2 +- Backend/Models/Users.go | 4 +- .../android/app/src/main/AndroidManifest.xml | 3 +- .../unauthenticated_landing.dart | 75 ----- mobile/lib/main.dart | 4 +- mobile/lib/utils/encryption/aes_helper.dart | 138 ++++++++++ .../lib/utils/encryption/rsa_key_helper.dart | 257 ++++++++++++++++++ mobile/lib/utils/storage/encryption_keys.dart | 22 ++ mobile/lib/views/authentication/login.dart | 217 +++++++++++++++ .../{ => views}/authentication/signup.dart | 79 +++++- .../unauthenticated_landing.dart | 85 ++++++ mobile/lib/views/main/conversations_list.dart | 112 ++++++++ mobile/pubspec.lock | 155 ++++++++++- mobile/pubspec.yaml | 5 +- 17 files changed, 1294 insertions(+), 104 deletions(-) create mode 100644 Backend/Api/Auth/Login.go create mode 100644 Backend/Api/Auth/Session.go delete mode 100644 mobile/lib/authentication/unauthenticated_landing.dart create mode 100644 mobile/lib/utils/encryption/aes_helper.dart create mode 100644 mobile/lib/utils/encryption/rsa_key_helper.dart create mode 100644 mobile/lib/utils/storage/encryption_keys.dart create mode 100644 mobile/lib/views/authentication/login.dart rename mobile/lib/{ => views}/authentication/signup.dart (76%) create mode 100644 mobile/lib/views/authentication/unauthenticated_landing.dart create mode 100644 mobile/lib/views/main/conversations_list.dart diff --git a/Backend/Api/Auth/Login.go b/Backend/Api/Auth/Login.go new file mode 100644 index 0000000..4d3e3f2 --- /dev/null +++ b/Backend/Api/Auth/Login.go @@ -0,0 +1,107 @@ +package Auth + +import ( + "encoding/json" + "net/http" + "time" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "github.com/gofrs/uuid" +) + +type Credentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type loginResponse struct { + Status string `json:"status"` + Message string `json:"message"` + AsymmetricPublicKey string `json:"asymmetric_public_key"` + AsymmetricPrivateKey string `json:"asymmetric_private_key"` +} + +func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey string) { + var ( + status string = "error" + returnJson []byte + err error + ) + if code > 200 && code < 300 { + status = "success" + } + + returnJson, err = json.MarshalIndent(loginResponse{ + Status: status, + Message: message, + AsymmetricPublicKey: pubKey, + AsymmetricPrivateKey: privKey, + }, "", " ") + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Return updated json + w.WriteHeader(code) + w.Write(returnJson) + +} + +func Login(w http.ResponseWriter, r *http.Request) { + var ( + creds Credentials + userData Models.User + sessionToken uuid.UUID + expiresAt time.Time + err error + ) + + err = json.NewDecoder(r.Body).Decode(&creds) + if err != nil { + makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "") + return + } + + userData, err = Database.GetUserByUsername(creds.Username) + if err != nil { + makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "") + return + } + + if !CheckPasswordHash(creds.Password, userData.Password) { + makeLoginResponse(w, http.StatusUnauthorized, "An error occurred", "", "") + return + } + + sessionToken, err = uuid.NewV4() + if err != nil { + makeLoginResponse(w, http.StatusInternalServerError, "An error occurred", "", "") + return + } + + expiresAt = time.Now().Add(1 * time.Hour) + + Sessions[sessionToken.String()] = Session{ + UserID: userData.ID.String(), + Username: userData.Username, + Expiry: expiresAt, + } + + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: sessionToken.String(), + Expires: expiresAt, + }) + + makeLoginResponse( + w, + http.StatusOK, + "Successfully logged in", + userData.AsymmetricPublicKey, + userData.AsymmetricPrivateKey, + ) +} diff --git a/Backend/Api/Auth/Session.go b/Backend/Api/Auth/Session.go new file mode 100644 index 0000000..f97bbaf --- /dev/null +++ b/Backend/Api/Auth/Session.go @@ -0,0 +1,79 @@ +package Auth + +import ( + "errors" + "net/http" + "time" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" +) + +var ( + Sessions = map[string]Session{} +) + +type Session struct { + UserID string + Username string + Expiry time.Time +} + +func (s Session) IsExpired() bool { + return s.Expiry.Before(time.Now()) +} + +func CheckCookie(r *http.Request) (Session, error) { + var ( + c *http.Cookie + sessionToken string + userSession Session + exists bool + err error + ) + + c, err = r.Cookie("session_token") + if err != nil { + return userSession, err + } + sessionToken = c.Value + + // We then get the session from our session map + userSession, exists = Sessions[sessionToken] + if !exists { + return userSession, errors.New("Cookie not found") + } + + // If the session is present, but has expired, we can delete the session, and return + // an unauthorized status + if userSession.IsExpired() { + delete(Sessions, sessionToken) + return userSession, errors.New("Cookie expired") + } + + return userSession, nil +} + +func CheckCookieCurrentUser(w http.ResponseWriter, r *http.Request) (Models.User, error) { + var ( + userSession Session + userData Models.User + err error + ) + + userSession, err = CheckCookie(r) + if err != nil { + return userData, err + } + + userData, err = Database.GetUserById(userSession.UserID) + if err != nil { + return userData, err + } + + if userData.ID.String() != userSession.UserID { + return userData, errors.New("Is not current user") + } + + return userData, nil +} diff --git a/Backend/Api/Auth/Signup.go b/Backend/Api/Auth/Signup.go index 4c6ac5b..232611f 100644 --- a/Backend/Api/Auth/Signup.go +++ b/Backend/Api/Auth/Signup.go @@ -11,18 +11,48 @@ import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" ) +type signupResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + +func makeSignupResponse(w http.ResponseWriter, code int, message string) { + var ( + status string = "error" + returnJson []byte + err error + ) + if code > 200 && code < 300 { + status = "success" + } + + returnJson, err = json.MarshalIndent(signupResponse{ + Status: status, + Message: message, + }, "", " ") + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Return updated json + w.WriteHeader(code) + w.Write(returnJson) + +} + func Signup(w http.ResponseWriter, r *http.Request) { var ( userData Models.User requestBody []byte - returnJson []byte err error ) requestBody, err = ioutil.ReadAll(r.Body) if err != nil { log.Printf("Error encountered reading POST body: %s\n", err.Error()) - http.Error(w, "Error", http.StatusInternalServerError) + makeSignupResponse(w, http.StatusInternalServerError, "An error occurred") return } @@ -31,7 +61,7 @@ func Signup(w http.ResponseWriter, r *http.Request) { }, false) if err != nil { log.Printf("Invalid data provided to Signup: %s\n", err.Error()) - http.Error(w, "Invalid Data", http.StatusUnprocessableEntity) + makeSignupResponse(w, http.StatusUnprocessableEntity, "Invalid data provided") return } @@ -40,35 +70,27 @@ func Signup(w http.ResponseWriter, r *http.Request) { userData.ConfirmPassword == "" || len(userData.AsymmetricPrivateKey) == 0 || len(userData.AsymmetricPublicKey) == 0 { - http.Error(w, "Invalid Data", http.StatusUnprocessableEntity) + makeSignupResponse(w, http.StatusUnprocessableEntity, "Invalid data provided") return } err = Database.CheckUniqueUsername(userData.Username) if err != nil { - http.Error(w, "Invalid Data", http.StatusUnprocessableEntity) + makeSignupResponse(w, http.StatusUnprocessableEntity, "Invalid data provided") return } userData.Password, err = HashPassword(userData.Password) if err != nil { - http.Error(w, "Error", http.StatusInternalServerError) + makeSignupResponse(w, http.StatusInternalServerError, "An error occurred") return } err = Database.CreateUser(&userData) if err != nil { - http.Error(w, "Error", http.StatusInternalServerError) + makeSignupResponse(w, http.StatusInternalServerError, "An error occurred") return } - returnJson, err = json.MarshalIndent(userData, "", " ") - if err != nil { - http.Error(w, "Error", http.StatusInternalServerError) - return - } - - // Return updated json - w.WriteHeader(http.StatusOK) - w.Write(returnJson) + makeSignupResponse(w, http.StatusCreated, "Successfully signed up") } diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index bb597bd..ad4d271 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -66,7 +66,7 @@ func InitApiEndpoints(router *mux.Router) { // Define routes for authentication api.HandleFunc("/signup", Auth.Signup).Methods("POST") - // api.HandleFunc("/login", Auth.Login).Methods("POST") + api.HandleFunc("/login", Auth.Login).Methods("POST") // api.HandleFunc("/logout", Auth.Logout).Methods("GET") adminApi = api.PathPrefix("/message/").Subrouter() diff --git a/Backend/Models/Users.go b/Backend/Models/Users.go index b267a27..1c688c3 100644 --- a/Backend/Models/Users.go +++ b/Backend/Models/Users.go @@ -18,6 +18,6 @@ type User struct { Username string `gorm:"not null;unique" json:"username"` Password string `gorm:"not null" json:"password"` ConfirmPassword string `gorm:"-" json:"confirm_password"` - AsymmetricPrivateKey []byte `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted - AsymmetricPublicKey []byte `gorm:"not null" json:"asymmetric_public_key"` + AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted + AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"` } diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index c655c32..c3bfaaa 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ + createState() => _UnauthenticatedLandingWidgetState(); -} - - -class _UnauthenticatedLandingWidgetState extends State { - @override - Widget build(BuildContext context) { - final ButtonStyle buttonStyle = ElevatedButton.styleFrom( - primary: Colors.white, - onPrimary: Colors.cyan, - minimumSize: const Size.fromHeight(50), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - textStyle: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.red, - ), - ); - - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: const [ - FaIcon(FontAwesomeIcons.envelope, color: Colors.white, size: 40), - SizedBox(width: 15), - Text('Envelope', style: TextStyle(fontSize: 40, color: Colors.white),) - ] - ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.all(15), - child: Column ( - children: [ - ElevatedButton( - child: const Text('Login'), - onPressed: loginButton, - style: buttonStyle, - ), - const SizedBox(height: 20), - ElevatedButton( - child: const Text('Sign Up'), - onPressed: () => { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const Signup()), - ), - }, - style: buttonStyle, - ), - ] - ), - ), - ], - ), - ); - } -} - -void loginButton() { -} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index cff251d..3eceed5 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import './authentication/unauthenticated_landing.dart'; +import '/views/main/conversations_list.dart'; void main() { runApp(const Home()); @@ -17,7 +17,7 @@ class Home extends StatelessWidget { home: Scaffold( backgroundColor: Colors.cyan, body: SafeArea( - child: UnauthenticatedLandingWidget(), + child: ConversationsList(), ) ) ); diff --git a/mobile/lib/utils/encryption/aes_helper.dart b/mobile/lib/utils/encryption/aes_helper.dart new file mode 100644 index 0000000..04f2ece --- /dev/null +++ b/mobile/lib/utils/encryption/aes_helper.dart @@ -0,0 +1,138 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import "package:pointycastle/export.dart"; + +Uint8List createUint8ListFromString(String s) { + var ret = Uint8List(s.length); + for (var i = 0; i < s.length; i++) { + ret[i] = s.codeUnitAt(i); + } + return ret; +} + +// AES key size +const keySize = 32; // 32 byte key for AES-256 +const iterationCount = 1000; + +class AesHelper { + static const cbcMode = 'CBC'; + static const cfbMode = 'CFB'; + + static Uint8List deriveKey(dynamic password, + {String salt = '', + int iterationCount = iterationCount, + int derivedKeyLength = keySize}) { + + if (password == null || password.isEmpty) { + throw ArgumentError('password must not be empty'); + } + + if (password is String) { + password = createUint8ListFromString(password); + } + + Uint8List saltBytes = createUint8ListFromString(salt); + Pbkdf2Parameters params = Pbkdf2Parameters(saltBytes, iterationCount, derivedKeyLength); + KeyDerivator keyDerivator = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64)); + keyDerivator.init(params); + + return keyDerivator.process(password); + } + + static Uint8List pad(Uint8List src, int blockSize) { + var pad = PKCS7Padding(); + pad.init(null); + + int padLength = blockSize - (src.length % blockSize); + var out = Uint8List(src.length + padLength)..setAll(0, src); + pad.addPadding(out, src.length); + + return out; + } + + static Uint8List unpad(Uint8List src) { + var pad = PKCS7Padding(); + pad.init(null); + + int padLength = pad.padCount(src); + int len = src.length - padLength; + + return Uint8List(len)..setRange(0, len, src); + } + + static String aesEncrypt(String password, Uint8List plaintext, + {String mode = cbcMode}) { + Uint8List derivedKey = deriveKey(password); + KeyParameter keyParam = KeyParameter(derivedKey); + BlockCipher aes = AESEngine(); + + var rnd = FortunaRandom(); + rnd.seed(keyParam); + Uint8List iv = rnd.nextBytes(aes.blockSize); + + BlockCipher cipher; + ParametersWithIV params = ParametersWithIV(keyParam, iv); + switch (mode) { + case cbcMode: + cipher = CBCBlockCipher(aes); + break; + case cfbMode: + cipher = CFBBlockCipher(aes, aes.blockSize); + break; + default: + throw ArgumentError('incorrect value of the "mode" parameter'); + } + cipher.init(true, params); + + Uint8List paddedText = pad(plaintext, aes.blockSize); + Uint8List cipherBytes = _processBlocks(cipher, paddedText); + Uint8List cipherIvBytes = Uint8List(cipherBytes.length + iv.length) + ..setAll(0, iv) + ..setAll(iv.length, cipherBytes); + + return base64.encode(cipherIvBytes); + } + + static String aesDecrypt(String password, Uint8List ciphertext, + {String mode = cbcMode}) { + Uint8List derivedKey = deriveKey(password); + KeyParameter keyParam = KeyParameter(derivedKey); + BlockCipher aes = AESEngine(); + + Uint8List iv = Uint8List(aes.blockSize) + ..setRange(0, aes.blockSize, ciphertext); + + BlockCipher cipher; + ParametersWithIV params = ParametersWithIV(keyParam, iv); + switch (mode) { + case cbcMode: + cipher = CBCBlockCipher(aes); + break; + case cfbMode: + cipher = CFBBlockCipher(aes, aes.blockSize); + break; + default: + throw ArgumentError('incorrect value of the "mode" parameter'); + } + cipher.init(false, params); + + int cipherLen = ciphertext.length - aes.blockSize; + Uint8List cipherBytes = Uint8List(cipherLen) + ..setRange(0, cipherLen, ciphertext, aes.blockSize); + Uint8List paddedText = _processBlocks(cipher, cipherBytes); + Uint8List textBytes = unpad(paddedText); + + return String.fromCharCodes(textBytes); + } + + static Uint8List _processBlocks(BlockCipher cipher, Uint8List inp) { + var out = Uint8List(inp.lengthInBytes); + + for (var offset = 0; offset < inp.lengthInBytes;) { + var len = cipher.processBlock(inp, offset, out, offset); + offset += len; + } + + return out; + } +} diff --git a/mobile/lib/utils/encryption/rsa_key_helper.dart b/mobile/lib/utils/encryption/rsa_key_helper.dart new file mode 100644 index 0000000..2d3510e --- /dev/null +++ b/mobile/lib/utils/encryption/rsa_key_helper.dart @@ -0,0 +1,257 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:pointycastle/src/platform_check/platform_check.dart'; +import "package:pointycastle/export.dart"; +import "package:asn1lib/asn1lib.dart"; + +/* +var rsaKeyHelper = RsaKeyHelper(); +var keyPair = rsaKeyHelper.generateRSAkeyPair(); +var rsaPub = keyPair.publicKey; +var rsaPriv = keyPair.privateKey; +var cipherText = rsaKeyHelper.rsaEncrypt(rsaPub, Uint8List.fromList('Test Data'.codeUnits)); +print(cipherText); +var plainText = rsaKeyHelper.rsaDecrypt(rsaPriv, cipherText); +print(String.fromCharCodes(plainText)); + */ + + +List decodePEM(String pem) { + var startsWith = [ + "-----BEGIN PUBLIC KEY-----", + "-----BEGIN PRIVATE KEY-----", + "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: React-Native-OpenPGP.js 0.1\r\nComment: http://openpgpjs.org\r\n\r\n", + "-----BEGIN PGP PRIVATE KEY BLOCK-----\r\nVersion: React-Native-OpenPGP.js 0.1\r\nComment: http://openpgpjs.org\r\n\r\n", + ]; + var endsWith = [ + "-----END PUBLIC KEY-----", + "-----END PRIVATE KEY-----", + "-----END PGP PUBLIC KEY BLOCK-----", + "-----END PGP PRIVATE KEY BLOCK-----", + ]; + bool isOpenPgp = pem.contains('BEGIN PGP'); + + for (var s in startsWith) { + if (pem.startsWith(s)) { + pem = pem.substring(s.length); + } + } + + for (var s in endsWith) { + if (pem.endsWith(s)) { + pem = pem.substring(0, pem.length - s.length); + } + } + + if (isOpenPgp) { + var index = pem.indexOf('\r\n'); + pem = pem.substring(0, index); + } + + pem = pem.replaceAll('\n', ''); + pem = pem.replaceAll('\r', ''); + + return base64.decode(pem); +} + + +class RsaKeyHelper { + + // Generate secure random sequence for seed + SecureRandom secureRandom() { + var secureRandom = FortunaRandom(); + var random = Random.secure(); + List seeds = []; + for (int i = 0; i < 32; i++) { + seeds.add(random.nextInt(255)); + } + secureRandom.seed(KeyParameter(Uint8List.fromList(seeds))); + + return secureRandom; + } + + // Generate RSA key pair + AsymmetricKeyPair generateRSAkeyPair({int bitLength = 2048}) { + var secureRandom = this.secureRandom(); + + // final keyGen = KeyGenerator('RSA'); // Get using registry + final keyGen = RSAKeyGenerator(); + + keyGen.init(ParametersWithRandom( + RSAKeyGeneratorParameters(BigInt.parse('65537'), bitLength, 64), + secureRandom)); + + // Use the generator + + final pair = keyGen.generateKeyPair(); + + // Cast the generated key pair into the RSA key types + + final myPublic = pair.publicKey as RSAPublicKey; + final myPrivate = pair.privateKey as RSAPrivateKey; + + return AsymmetricKeyPair(myPublic, myPrivate); + } + + + // Encrypt data using RSA key + Uint8List rsaEncrypt(RSAPublicKey myPublic, Uint8List dataToEncrypt) { + final encryptor = OAEPEncoding(RSAEngine()) + ..init(true, PublicKeyParameter(myPublic)); // true=encrypt + + return _processInBlocks(encryptor, dataToEncrypt); + } + + + // Decrypt data using RSA key + Uint8List rsaDecrypt(RSAPrivateKey myPrivate, Uint8List cipherText) { + final decryptor = OAEPEncoding(RSAEngine()) + ..init(false, PrivateKeyParameter(myPrivate)); // false=decrypt + + return _processInBlocks(decryptor, cipherText); + } + + + // Process blocks after encryption/descryption + Uint8List _processInBlocks(AsymmetricBlockCipher engine, Uint8List input) { + final numBlocks = input.length ~/ engine.inputBlockSize + + ((input.length % engine.inputBlockSize != 0) ? 1 : 0); + + final output = Uint8List(numBlocks * engine.outputBlockSize); + + var inputOffset = 0; + var outputOffset = 0; + while (inputOffset < input.length) { + final chunkSize = (inputOffset + engine.inputBlockSize <= input.length) + ? engine.inputBlockSize + : input.length - inputOffset; + + outputOffset += engine.processBlock( + input, inputOffset, chunkSize, output, outputOffset); + + inputOffset += chunkSize; + } + + return (output.length == outputOffset) + ? output + : output.sublist(0, outputOffset); + } + + + // Encode RSA public key to pem format + static encodePublicKeyToPem(RSAPublicKey publicKey) { + var algorithmSeq = ASN1Sequence(); + var algorithmAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1])); + var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0])); + algorithmSeq.add(algorithmAsn1Obj); + algorithmSeq.add(paramsAsn1Obj); + + var publicKeySeq = ASN1Sequence(); + publicKeySeq.add(ASN1Integer(publicKey.modulus!)); + publicKeySeq.add(ASN1Integer(publicKey.exponent!)); + var publicKeySeqBitString = ASN1BitString(Uint8List.fromList(publicKeySeq.encodedBytes)); + + var topLevelSeq = ASN1Sequence(); + topLevelSeq.add(algorithmSeq); + topLevelSeq.add(publicKeySeqBitString); + var dataBase64 = base64.encode(topLevelSeq.encodedBytes); + + return """-----BEGIN PUBLIC KEY-----\r\n$dataBase64\r\n-----END PUBLIC KEY-----"""; + } + + // Parse public key PEM file + static parsePublicKeyFromPem(pemString) { + List publicKeyDER = decodePEM(pemString); + var asn1Parser = ASN1Parser(Uint8List.fromList(publicKeyDER)); + var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; + var publicKeyBitString = topLevelSeq.elements[1]; + + var publicKeyAsn = ASN1Parser(publicKeyBitString.contentBytes()!, relaxedParsing: true); + var publicKeySeq = publicKeyAsn.nextObject() as ASN1Sequence; + var modulus = publicKeySeq.elements[0] as ASN1Integer; + var exponent = publicKeySeq.elements[1] as ASN1Integer; + + RSAPublicKey rsaPublicKey = RSAPublicKey( + modulus.valueAsBigInteger!, + exponent.valueAsBigInteger! + ); + + return rsaPublicKey; + } + + + // Encode RSA private key to pem format + static encodePrivateKeyToPem(RSAPrivateKey privateKey) { + var version = ASN1Integer(BigInt.from(0)); + + var algorithmSeq = ASN1Sequence(); + var algorithmAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1])); + var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0])); + algorithmSeq.add(algorithmAsn1Obj); + algorithmSeq.add(paramsAsn1Obj); + + var privateKeySeq = ASN1Sequence(); + var modulus = ASN1Integer(privateKey.n!); + var publicExponent = ASN1Integer(BigInt.parse('65537')); + var privateExponent = ASN1Integer(privateKey.privateExponent!); + var p = ASN1Integer(privateKey.p!); + var q = ASN1Integer(privateKey.q!); + var dP = privateKey.privateExponent! % (privateKey.p! - BigInt.from(1)); + var exp1 = ASN1Integer(dP); + var dQ = privateKey.privateExponent! % (privateKey.q! - BigInt.from(1)); + var exp2 = ASN1Integer(dQ); + var iQ = privateKey.q?.modInverse(privateKey.p!); + var co = ASN1Integer(iQ!); + + privateKeySeq.add(version); + privateKeySeq.add(modulus); + privateKeySeq.add(publicExponent); + privateKeySeq.add(privateExponent); + privateKeySeq.add(p); + privateKeySeq.add(q); + privateKeySeq.add(exp1); + privateKeySeq.add(exp2); + privateKeySeq.add(co); + var publicKeySeqOctetString = ASN1OctetString(Uint8List.fromList(privateKeySeq.encodedBytes)); + + var topLevelSeq = ASN1Sequence(); + topLevelSeq.add(version); + topLevelSeq.add(algorithmSeq); + topLevelSeq.add(publicKeySeqOctetString); + var dataBase64 = base64.encode(topLevelSeq.encodedBytes); + + return """-----BEGIN PRIVATE KEY-----\r\n$dataBase64\r\n-----END PRIVATE KEY-----"""; + } + + static parsePrivateKeyFromPem(pemString) { + List privateKeyDER = decodePEM(pemString); + var asn1Parser = ASN1Parser(Uint8List.fromList(privateKeyDER)); + var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; + var version = topLevelSeq.elements[0]; + var algorithm = topLevelSeq.elements[1]; + var privateKey = topLevelSeq.elements[2]; + + asn1Parser = ASN1Parser(privateKey.contentBytes()!); + var pkSeq = asn1Parser.nextObject() as ASN1Sequence; + + version = pkSeq.elements[0]; + var modulus = pkSeq.elements[1] as ASN1Integer; + //var publicExponent = pkSeq.elements[2] as ASN1Integer; + var privateExponent = pkSeq.elements[3] as ASN1Integer; + var p = pkSeq.elements[4] as ASN1Integer; + var q = pkSeq.elements[5] as ASN1Integer; + var exp1 = pkSeq.elements[6] as ASN1Integer; + var exp2 = pkSeq.elements[7] as ASN1Integer; + var co = pkSeq.elements[8] as ASN1Integer; + + RSAPrivateKey rsaPrivateKey = RSAPrivateKey( + modulus.valueAsBigInteger!, + privateExponent.valueAsBigInteger!, + p.valueAsBigInteger, + q.valueAsBigInteger + ); + + return rsaPrivateKey; + } +} diff --git a/mobile/lib/utils/storage/encryption_keys.dart b/mobile/lib/utils/storage/encryption_keys.dart new file mode 100644 index 0000000..d27bfc7 --- /dev/null +++ b/mobile/lib/utils/storage/encryption_keys.dart @@ -0,0 +1,22 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import "package:pointycastle/export.dart"; +import '/utils/encryption/rsa_key_helper.dart'; + +const rsaPrivateKeyName = 'rsaPrivateKey'; + +void setPrivateKey(RSAPrivateKey key) async { + String keyPem = RsaKeyHelper.encodePrivateKeyToPem(key); + + final prefs = await SharedPreferences.getInstance(); + prefs.setString(rsaPrivateKeyName, keyPem); +} + +Future getPrivateKey() async { + final prefs = await SharedPreferences.getInstance(); + String? keyPem = prefs.getString(rsaPrivateKeyName); + if (keyPem == null) { + throw Exception('No RSA private key set'); + } + + return RsaKeyHelper.parsePrivateKeyFromPem(keyPem); +} diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart new file mode 100644 index 0000000..0a087f8 --- /dev/null +++ b/mobile/lib/views/authentication/login.dart @@ -0,0 +1,217 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +import '/views/main/conversations_list.dart'; + +import '/utils/encryption/rsa_key_helper.dart'; +import '/utils/encryption/aes_helper.dart'; +import '/utils/storage/encryption_keys.dart'; + +class LoginResponse { + final String status; + final String message; + final String asymmetricPublicKey; + final String asymmetricPrivateKey; + + const LoginResponse({ + required this.status, + required this.message, + required this.asymmetricPublicKey, + required this.asymmetricPrivateKey, + }); + + factory LoginResponse.fromJson(Map json) { + return LoginResponse( + status: json['status'], + message: json['message'], + asymmetricPublicKey: json['asymmetric_public_key'], + asymmetricPrivateKey: json['asymmetric_private_key'], + ); + } +} + +Future login(context, String username, String password) async { + final resp = await http.post( + Uri.parse('http://192.168.1.5:8080/api/v1/login'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: jsonEncode({ + 'username': username, + 'password': password, + }), + ); + + LoginResponse response = LoginResponse.fromJson(jsonDecode(resp.body)); + + if (resp.statusCode != 200) { + throw Exception(response.message); + } + + var rsaPrivPem = AesHelper.aesDecrypt(password, base64.decode(response.asymmetricPrivateKey)); + var rsaPriv = RsaKeyHelper.parsePrivateKeyFromPem(rsaPrivPem); + setPrivateKey(rsaPriv); + + final preferences = await SharedPreferences.getInstance(); + preferences.setBool('islogin', true); + + return response; +} + +class Login extends StatelessWidget { + const Login({Key? key}) : super(key: key); + + static const String _title = 'Envelope'; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: _title, + home: Scaffold( + backgroundColor: Colors.cyan, + appBar: AppBar( + title: null, + automaticallyImplyLeading: true, + //`true` if you want Flutter to automatically add Back Button when needed, + //or `false` if you want to force your own back button every where + leading: IconButton(icon: const Icon(Icons.arrow_back), + //onPressed:() => Navigator.pop(context, false), + onPressed:() => { + Navigator.pop(context) + } + ) + ), + body: const SafeArea( + child: LoginWidget(), + ) + ), + theme: ThemeData( + appBarTheme: const AppBarTheme( + backgroundColor: Colors.cyan, + elevation: 0, + ), + inputDecorationTheme: const InputDecorationTheme( + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder(), + labelStyle: TextStyle( + color: Colors.white, + fontSize: 30, + ), + filled: true, + fillColor: Colors.white, + ), + + ), + ); + } +} + +class LoginWidget extends StatefulWidget { + const LoginWidget({Key? key}) : super(key: key); + + @override + State createState() => _LoginWidgetState(); +} + +class _LoginWidgetState extends State { + final _formKey = GlobalKey(); + + TextEditingController usernameController = TextEditingController(); + TextEditingController passwordController = TextEditingController(); + + @override + Widget build(BuildContext context) { + const TextStyle _inputTextStyle = TextStyle(fontSize: 18, color: Colors.black); + + final ButtonStyle _buttonStyle = ElevatedButton.styleFrom( + primary: Colors.white, + onPrimary: Colors.cyan, + minimumSize: const Size.fromHeight(50), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + textStyle: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ); + + return Center( + child: Form( + key: _formKey, + child: Center( + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text('Login', style: TextStyle(fontSize: 35, color: Colors.white),), + const SizedBox(height: 30), + TextFormField( + controller: usernameController, + decoration: const InputDecoration( + hintText: 'Username', + ), + style: _inputTextStyle, + // The validator receives the text that the user has entered. + validator: (value) { + if (value == null || value.isEmpty) { + return 'Create a username'; + } + return null; + }, + ), + const SizedBox(height: 5), + TextFormField( + controller: passwordController, + obscureText: true, + enableSuggestions: false, + autocorrect: false, + decoration: const InputDecoration( + hintText: 'Password', + ), + style: _inputTextStyle, + // The validator receives the text that the user has entered. + validator: (value) { + if (value == null || value.isEmpty) { + return 'Enter a password'; + } + return null; + }, + ), + const SizedBox(height: 5), + ElevatedButton( + style: _buttonStyle, + onPressed: () { + if (_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Processing Data')), + ); + + login( + context, + usernameController.text, + passwordController.text, + ).then((value) { + Navigator.of(context).popUntil((route) { + print(route.isFirst); + return route.isFirst; + }); + }).catchError((error) { + print(error); // TODO: Show error on interface + }); + } + }, + child: const Text('Submit'), + ), + ], + ) + ) + ) + + ) + ); + } +} diff --git a/mobile/lib/authentication/signup.dart b/mobile/lib/views/authentication/signup.dart similarity index 76% rename from mobile/lib/authentication/signup.dart rename to mobile/lib/views/authentication/signup.dart index 2b3641a..99654cf 100644 --- a/mobile/lib/authentication/signup.dart +++ b/mobile/lib/views/authentication/signup.dart @@ -1,5 +1,68 @@ +import 'dart:typed_data'; +import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:pointycastle/api.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +import '/views/main/conversations_list.dart'; + +import '/utils/encryption/rsa_key_helper.dart'; +import '/utils/encryption/aes_helper.dart'; +import '/utils/storage/encryption_keys.dart'; + +class SignupResponse { + final String status; + final String message; + + const SignupResponse({ + required this.status, + required this.message, + }); + + factory SignupResponse.fromJson(Map json) { + return SignupResponse( + status: json['status'], + message: json['message'], + ); + } +} + +Future signUp(context, String username, String password, String confirmPassword) async { + var rsaKeyHelper = RsaKeyHelper(); + var keyPair = rsaKeyHelper.generateRSAkeyPair(); + + setPrivateKey(keyPair.privateKey); + + var rsaPubPem = RsaKeyHelper.encodePublicKeyToPem(keyPair.publicKey); + var rsaPrivPem = RsaKeyHelper.encodePrivateKeyToPem(keyPair.privateKey); + + var encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits)); + + final resp = await http.post( + Uri.parse('http://192.168.1.5:8080/api/v1/signup'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: jsonEncode({ + 'username': username, + 'password': password, + 'confirm_password': confirmPassword, + 'asymmetric_public_key': rsaPubPem, + 'asymmetric_private_key': encRsaPriv, + }), + ); + + SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body)); + + if (resp.statusCode != 201) { + throw Exception(response.message); + } + + final preferences = await SharedPreferences.getInstance(); + preferences.setBool('islogin', true); + + return response; +} class Signup extends StatelessWidget { const Signup({Key? key}) : super(key: key); @@ -153,7 +216,16 @@ class _SignupWidgetState extends State { const SnackBar(content: Text('Processing Data')), ); - signup(context); + signUp( + context, + usernameController.text, + passwordController.text, + passwordConfirmController.text + ).then((value) { + Navigator.of(context).popUntil((route) => route.isFirst); + }).catchError((error) { + print(error); // TODO: Show error on interface + }); } }, child: const Text('Submit'), @@ -167,6 +239,3 @@ class _SignupWidgetState extends State { ); } } - -void signup(context) { -} diff --git a/mobile/lib/views/authentication/unauthenticated_landing.dart b/mobile/lib/views/authentication/unauthenticated_landing.dart new file mode 100644 index 0000000..e17a06f --- /dev/null +++ b/mobile/lib/views/authentication/unauthenticated_landing.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import './login.dart'; +import './signup.dart'; + +class UnauthenticatedLandingWidget extends StatefulWidget { + const UnauthenticatedLandingWidget({Key? key}) : super(key: key); + + @override + State createState() => _UnauthenticatedLandingWidgetState(); +} + + +class _UnauthenticatedLandingWidgetState extends State { + @override + Widget build(BuildContext context) { + final ButtonStyle buttonStyle = ElevatedButton.styleFrom( + primary: Colors.white, + onPrimary: Colors.cyan, + minimumSize: const Size.fromHeight(50), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + textStyle: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ); + + return WillPopScope( + onWillPop: () async => false, + child: Scaffold( + backgroundColor: Colors.cyan, + body: SafeArea( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: const [ + FaIcon(FontAwesomeIcons.envelope, color: Colors.white, size: 40), + SizedBox(width: 15), + Text('Envelope', style: TextStyle(fontSize: 40, color: Colors.white),) + ] + ), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.all(15), + child: Column ( + children: [ + ElevatedButton( + child: const Text('Login'), + onPressed: () => { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const Login()), + ), + }, + + style: buttonStyle, + ), + const SizedBox(height: 20), + ElevatedButton( + child: const Text('Sign Up'), + onPressed: () => { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const Signup()), + ), + }, + style: buttonStyle, + ), + ] + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/mobile/lib/views/main/conversations_list.dart b/mobile/lib/views/main/conversations_list.dart new file mode 100644 index 0000000..29e6f2c --- /dev/null +++ b/mobile/lib/views/main/conversations_list.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +import '/views/authentication/unauthenticated_landing.dart'; + +class ConversationsList extends StatefulWidget { + const ConversationsList({Key? key}) : super(key: key); + + @override + State createState() => _ConversationsListState(); +} + +class _ConversationsListState extends State { + @override + void initState() { + checkLogin(); + + super.initState(); + } + + final _suggestions = []; + final _biggerFont = const TextStyle(fontSize: 18); + + Future checkLogin() async { + SharedPreferences preferences = await SharedPreferences.getInstance(); + print(preferences.getBool('islogin')); + if (preferences.getBool('islogin') != true) { + setState(() { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const UnauthenticatedLandingWidget(), + )); + }); + } + } + + Widget list() { + + if (_suggestions.isEmpty) { + return const Center( + child: Text('No Conversations'), + ); + } + + return ListView.builder( + itemCount: _suggestions.length, + padding: const EdgeInsets.all(16.0), + itemBuilder: /*1*/ (context, i) { + //if (i >= _suggestions.length) { + // TODO: Check for more conversations here. Remove the itemCount to use this section + //_suggestions.addAll(generateWordPairs().take(10)); /*4*/ + //} + return Column( + children: [ + ListTile( + title: Text( + _suggestions[i], + style: _biggerFont, + ), + ), + const Divider(), + ] + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async => false, + child: Scaffold( + appBar: AppBar( + title: Text('Envelope'), + actions: [ + PopupMenuButton( + icon: const FaIcon(FontAwesomeIcons.ellipsisVertical, color: Colors.white, size: 40), + itemBuilder: (context) => [ + const PopupMenuItem( + value: 0, + child: Text("Settings"), + ), + const PopupMenuItem( + value: 1, + child: Text("Logout"), + ), + + ], + onSelected: (item) => selectedMenuItem(context, item), + ), + ], + ), + body: list(), + ), + ); + } + + void selectedMenuItem(BuildContext context, item) async { + switch (item) { + case 0: + print("Settings"); + break; + case 1: + SharedPreferences preferences = await SharedPreferences.getInstance(); + preferences.setBool('islogin', false); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const UnauthenticatedLandingWidget(), + )); + break; + } + } +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 3c4fda5..1a1eab8 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + asn1lib: + dependency: "direct main" + description: + name: asn1lib + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" async: dependency: transitive description: @@ -64,6 +71,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" flutter: dependency: "direct main" description: flutter @@ -81,6 +102,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" font_awesome_flutter: dependency: "direct main" description: @@ -88,13 +114,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "10.1.0" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" js: dependency: transitive description: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.6.3" lints: dependency: transitive description: @@ -130,6 +170,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" pointycastle: dependency: "direct main" description: @@ -137,6 +212,69 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.5.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.15" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" sky_engine: dependency: transitive description: flutter @@ -198,5 +336,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" sdks: dart: ">=2.16.2 <3.0.0" + flutter: ">=2.8.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 4834078..1cba398 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -1,4 +1,4 @@ -name: mobile +name: Envelope description: A new Flutter project. publish_to: 'none' # Remove this line if you wish to publish to pub.dev @@ -15,6 +15,9 @@ dependencies: cupertino_icons: ^1.0.2 font_awesome_flutter: ^10.1.0 pointycastle: ^3.5.2 + asn1lib: ^1.1.0 + http: ^0.13.4 + shared_preferences: ^2.0.15 dev_dependencies: flutter_test: