| @ -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; | |||
| } | |||
| } | |||
| } | |||