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