Browse Source

Working on initial encryption key generation & authentication

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
00e3cc3620
17 changed files with 1294 additions and 104 deletions
  1. +107
    -0
      Backend/Api/Auth/Login.go
  2. +79
    -0
      Backend/Api/Auth/Session.go
  3. +38
    -16
      Backend/Api/Auth/Signup.go
  4. +1
    -1
      Backend/Api/Routes.go
  5. +2
    -2
      Backend/Models/Users.go
  6. +2
    -1
      mobile/android/app/src/main/AndroidManifest.xml
  7. +0
    -75
      mobile/lib/authentication/unauthenticated_landing.dart
  8. +2
    -2
      mobile/lib/main.dart
  9. +138
    -0
      mobile/lib/utils/encryption/aes_helper.dart
  10. +257
    -0
      mobile/lib/utils/encryption/rsa_key_helper.dart
  11. +22
    -0
      mobile/lib/utils/storage/encryption_keys.dart
  12. +217
    -0
      mobile/lib/views/authentication/login.dart
  13. +74
    -5
      mobile/lib/views/authentication/signup.dart
  14. +85
    -0
      mobile/lib/views/authentication/unauthenticated_landing.dart
  15. +112
    -0
      mobile/lib/views/main/conversations_list.dart
  16. +154
    -1
      mobile/pubspec.lock
  17. +4
    -1
      mobile/pubspec.yaml

+ 107
- 0
Backend/Api/Auth/Login.go View File

@ -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,
)
}

+ 79
- 0
Backend/Api/Auth/Session.go View File

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

+ 38
- 16
Backend/Api/Auth/Signup.go View File

@ -11,18 +11,48 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
) )
type signupResponse struct {
Status string `json:"status"`
Message string `json:"message"`
}
func makeSignupResponse(w http.ResponseWriter, code int, message string) {
var (
status string = "error"
returnJson []byte
err error
)
if code > 200 && code < 300 {
status = "success"
}
returnJson, err = json.MarshalIndent(signupResponse{
Status: status,
Message: message,
}, "", " ")
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
return
}
// Return updated json
w.WriteHeader(code)
w.Write(returnJson)
}
func Signup(w http.ResponseWriter, r *http.Request) { func Signup(w http.ResponseWriter, r *http.Request) {
var ( var (
userData Models.User userData Models.User
requestBody []byte requestBody []byte
returnJson []byte
err error err error
) )
requestBody, err = ioutil.ReadAll(r.Body) requestBody, err = ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
log.Printf("Error encountered reading POST body: %s\n", err.Error()) log.Printf("Error encountered reading POST body: %s\n", err.Error())
http.Error(w, "Error", http.StatusInternalServerError)
makeSignupResponse(w, http.StatusInternalServerError, "An error occurred")
return return
} }
@ -31,7 +61,7 @@ func Signup(w http.ResponseWriter, r *http.Request) {
}, false) }, false)
if err != nil { if err != nil {
log.Printf("Invalid data provided to Signup: %s\n", err.Error()) log.Printf("Invalid data provided to Signup: %s\n", err.Error())
http.Error(w, "Invalid Data", http.StatusUnprocessableEntity)
makeSignupResponse(w, http.StatusUnprocessableEntity, "Invalid data provided")
return return
} }
@ -40,35 +70,27 @@ func Signup(w http.ResponseWriter, r *http.Request) {
userData.ConfirmPassword == "" || userData.ConfirmPassword == "" ||
len(userData.AsymmetricPrivateKey) == 0 || len(userData.AsymmetricPrivateKey) == 0 ||
len(userData.AsymmetricPublicKey) == 0 { len(userData.AsymmetricPublicKey) == 0 {
http.Error(w, "Invalid Data", http.StatusUnprocessableEntity)
makeSignupResponse(w, http.StatusUnprocessableEntity, "Invalid data provided")
return return
} }
err = Database.CheckUniqueUsername(userData.Username) err = Database.CheckUniqueUsername(userData.Username)
if err != nil { if err != nil {
http.Error(w, "Invalid Data", http.StatusUnprocessableEntity)
makeSignupResponse(w, http.StatusUnprocessableEntity, "Invalid data provided")
return return
} }
userData.Password, err = HashPassword(userData.Password) userData.Password, err = HashPassword(userData.Password)
if err != nil { if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
makeSignupResponse(w, http.StatusInternalServerError, "An error occurred")
return return
} }
err = Database.CreateUser(&userData) err = Database.CreateUser(&userData)
if err != nil { if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
makeSignupResponse(w, http.StatusInternalServerError, "An error occurred")
return return
} }
returnJson, err = json.MarshalIndent(userData, "", " ")
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
// Return updated json
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
makeSignupResponse(w, http.StatusCreated, "Successfully signed up")
} }

+ 1
- 1
Backend/Api/Routes.go View File

@ -66,7 +66,7 @@ func InitApiEndpoints(router *mux.Router) {
// Define routes for authentication // Define routes for authentication
api.HandleFunc("/signup", Auth.Signup).Methods("POST") api.HandleFunc("/signup", Auth.Signup).Methods("POST")
// api.HandleFunc("/login", Auth.Login).Methods("POST")
api.HandleFunc("/login", Auth.Login).Methods("POST")
// api.HandleFunc("/logout", Auth.Logout).Methods("GET") // api.HandleFunc("/logout", Auth.Logout).Methods("GET")
adminApi = api.PathPrefix("/message/").Subrouter() adminApi = api.PathPrefix("/message/").Subrouter()


+ 2
- 2
Backend/Models/Users.go View File

@ -18,6 +18,6 @@ type User struct {
Username string `gorm:"not null;unique" json:"username"` Username string `gorm:"not null;unique" json:"username"`
Password string `gorm:"not null" json:"password"` Password string `gorm:"not null" json:"password"`
ConfirmPassword string `gorm:"-" json:"confirm_password"` ConfirmPassword string `gorm:"-" json:"confirm_password"`
AsymmetricPrivateKey []byte `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted
AsymmetricPublicKey []byte `gorm:"not null" json:"asymmetric_public_key"`
AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted
AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"`
} }

+ 2
- 1
mobile/android/app/src/main/AndroidManifest.xml View File

@ -1,7 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mobile"> package="com.example.mobile">
<uses-permission android:name="android.permission.INTERNET"/>
<application <application
android:label="mobile"
android:label="Envelope"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity


+ 0
- 75
mobile/lib/authentication/unauthenticated_landing.dart View File

@ -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() {
}

+ 2
- 2
mobile/lib/main.dart View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import './authentication/unauthenticated_landing.dart';
import '/views/main/conversations_list.dart';
void main() { void main() {
runApp(const Home()); runApp(const Home());
@ -17,7 +17,7 @@ class Home extends StatelessWidget {
home: Scaffold( home: Scaffold(
backgroundColor: Colors.cyan, backgroundColor: Colors.cyan,
body: SafeArea( body: SafeArea(
child: UnauthenticatedLandingWidget(),
child: ConversationsList(),
) )
) )
); );


+ 138
- 0
mobile/lib/utils/encryption/aes_helper.dart View File

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

+ 257
- 0
mobile/lib/utils/encryption/rsa_key_helper.dart View File

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

+ 22
- 0
mobile/lib/utils/storage/encryption_keys.dart View File

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

+ 217
- 0
mobile/lib/views/authentication/login.dart View File

@ -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'),
),
],
)
)
)
)
);
}
}

mobile/lib/authentication/signup.dart → mobile/lib/views/authentication/signup.dart View File


+ 85
- 0
mobile/lib/views/authentication/unauthenticated_landing.dart View File

@ -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,
),
]
),
),
],
),
),
),
),
);
}
}

+ 112
- 0
mobile/lib/views/main/conversations_list.dart View File

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

+ 154
- 1
mobile/pubspec.lock View File

@ -1,6 +1,13 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
asn1lib:
dependency: "direct main"
description:
name: asn1lib
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -64,6 +71,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -81,6 +102,11 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
font_awesome_flutter: font_awesome_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -88,13 +114,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "10.1.0" version: "10.1.0"
http:
dependency: "direct main"
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.4"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
js: js:
dependency: transitive dependency: transitive
description: description:
name: js name: js
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.4"
version: "0.6.3"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -130,6 +170,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.6"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
pointycastle: pointycastle:
dependency: "direct main" dependency: "direct main"
description: description:
@ -137,6 +212,69 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.5.2" version: "3.5.2"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.4"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.15"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
shared_preferences_ios:
dependency: transitive
description:
name: shared_preferences_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
shared_preferences_macos:
dependency: transitive
description:
name: shared_preferences_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -198,5 +336,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.2"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+1"
sdks: sdks:
dart: ">=2.16.2 <3.0.0" dart: ">=2.16.2 <3.0.0"
flutter: ">=2.8.0"

+ 4
- 1
mobile/pubspec.yaml View File

@ -1,4 +1,4 @@
name: mobile
name: Envelope
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
@ -15,6 +15,9 @@ dependencies:
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
font_awesome_flutter: ^10.1.0 font_awesome_flutter: ^10.1.0
pointycastle: ^3.5.2 pointycastle: ^3.5.2
asn1lib: ^1.1.0
http: ^0.13.4
shared_preferences: ^2.0.15
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:


Loading…
Cancel
Save