| @ -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, | |||||
| ) | |||||
| } | |||||
| @ -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 | |||||
| } | |||||
| @ -1,75 +0,0 @@ | |||||
| import 'package:flutter/material.dart'; | |||||
| import 'package:font_awesome_flutter/font_awesome_flutter.dart'; | |||||
| import './signup.dart'; | |||||
| class UnauthenticatedLandingWidget extends StatefulWidget { | |||||
| const UnauthenticatedLandingWidget({Key? key}) : super(key: key); | |||||
| @override | |||||
| State<UnauthenticatedLandingWidget> createState() => _UnauthenticatedLandingWidgetState(); | |||||
| } | |||||
| class _UnauthenticatedLandingWidgetState extends State<UnauthenticatedLandingWidget> { | |||||
| @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: <Widget>[ | |||||
| 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() { | |||||
| } | |||||
| @ -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; | |||||
| } | |||||
| } | |||||
| @ -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<int> 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<int> 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<RSAPublicKey, RSAPrivateKey> 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<RSAPublicKey, RSAPrivateKey>(myPublic, myPrivate); | |||||
| } | |||||
| // Encrypt data using RSA key | |||||
| Uint8List rsaEncrypt(RSAPublicKey myPublic, Uint8List dataToEncrypt) { | |||||
| final encryptor = OAEPEncoding(RSAEngine()) | |||||
| ..init(true, PublicKeyParameter<RSAPublicKey>(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<RSAPrivateKey>(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<int> 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<int> 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; | |||||
| } | |||||
| } | |||||
| @ -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<RSAPrivateKey> 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); | |||||
| } | |||||
| @ -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<String, dynamic> json) { | |||||
| return LoginResponse( | |||||
| status: json['status'], | |||||
| message: json['message'], | |||||
| asymmetricPublicKey: json['asymmetric_public_key'], | |||||
| asymmetricPrivateKey: json['asymmetric_private_key'], | |||||
| ); | |||||
| } | |||||
| } | |||||
| Future<LoginResponse> login(context, String username, String password) async { | |||||
| final resp = await http.post( | |||||
| Uri.parse('http://192.168.1.5:8080/api/v1/login'), | |||||
| headers: <String, String>{ | |||||
| 'Content-Type': 'application/json; charset=UTF-8', | |||||
| }, | |||||
| body: jsonEncode(<String, String>{ | |||||
| '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<LoginWidget> createState() => _LoginWidgetState(); | |||||
| } | |||||
| class _LoginWidgetState extends State<LoginWidget> { | |||||
| final _formKey = GlobalKey<FormState>(); | |||||
| 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'), | |||||
| ), | |||||
| ], | |||||
| ) | |||||
| ) | |||||
| ) | |||||
| ) | |||||
| ); | |||||
| } | |||||
| } | |||||
| @ -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<UnauthenticatedLandingWidget> createState() => _UnauthenticatedLandingWidgetState(); | |||||
| } | |||||
| class _UnauthenticatedLandingWidgetState extends State<UnauthenticatedLandingWidget> { | |||||
| @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: <Widget>[ | |||||
| 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, | |||||
| ), | |||||
| ] | |||||
| ), | |||||
| ), | |||||
| ], | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ), | |||||
| ); | |||||
| } | |||||
| } | |||||
| @ -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<ConversationsList> createState() => _ConversationsListState(); | |||||
| } | |||||
| class _ConversationsListState extends State<ConversationsList> { | |||||
| @override | |||||
| void initState() { | |||||
| checkLogin(); | |||||
| super.initState(); | |||||
| } | |||||
| final _suggestions = <String>[]; | |||||
| 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: <Widget>[ | |||||
| PopupMenuButton( | |||||
| icon: const FaIcon(FontAwesomeIcons.ellipsisVertical, color: Colors.white, size: 40), | |||||
| itemBuilder: (context) => [ | |||||
| const PopupMenuItem<int>( | |||||
| value: 0, | |||||
| child: Text("Settings"), | |||||
| ), | |||||
| const PopupMenuItem<int>( | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||