diff --git a/Backend/Api/Friends/Friends.go b/Backend/Api/Friends/Friends.go
index 07316af..3327e10 100644
--- a/Backend/Api/Friends/Friends.go
+++ b/Backend/Api/Friends/Friends.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"io/ioutil"
"net/http"
+ "time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
@@ -20,19 +21,25 @@ func CreateFriendRequest(w http.ResponseWriter, r *http.Request) {
requestBody, err = ioutil.ReadAll(r.Body)
if err != nil {
- panic(err)
+ http.Error(w, "Error", http.StatusInternalServerError)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
}
err = json.Unmarshal(requestBody, &friendRequest)
if err != nil {
- panic(err)
+ http.Error(w, "Error", http.StatusInternalServerError)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
}
friendRequest.AcceptedAt.Scan(nil)
err = Database.CreateFriendRequest(&friendRequest)
if err != nil {
- panic(err)
+ http.Error(w, "Error", http.StatusInternalServerError)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
}
returnJSON, err = json.MarshalIndent(friendRequest, "", " ")
@@ -46,3 +53,42 @@ func CreateFriendRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(returnJSON)
}
+
+// CreateFriendRequestQrCode creates a FriendRequest from post data from qr code scan
+func CreateFriendRequestQrCode(w http.ResponseWriter, r *http.Request) {
+ var (
+ friendRequests []Models.FriendRequest
+ requestBody []byte
+ i int
+ err error
+ )
+
+ requestBody, err = ioutil.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ err = json.Unmarshal(requestBody, &friendRequests)
+ if err != nil {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ for i = range friendRequests {
+ friendRequests[i].AcceptedAt.Time = time.Now()
+ friendRequests[i].AcceptedAt.Valid = true
+ }
+
+ err = Database.CreateFriendRequests(&friendRequests)
+ if err != nil {
+ http.Error(w, "Error", http.StatusInternalServerError)
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ // Return updated json
+ w.WriteHeader(http.StatusOK)
+}
diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go
index c9d76ee..50f4f01 100644
--- a/Backend/Api/Routes.go
+++ b/Backend/Api/Routes.go
@@ -65,12 +65,13 @@ func InitAPIEndpoints(router *mux.Router) {
authAPI.HandleFunc("/friend_requests", Friends.EncryptedFriendRequestList).Methods("GET")
authAPI.HandleFunc("/friend_request", Friends.CreateFriendRequest).Methods("POST")
+ authAPI.HandleFunc("/friend_request/qr_code", Friends.CreateFriendRequestQrCode).Methods("POST")
authAPI.HandleFunc("/friend_request/{requestID}", Friends.AcceptFriendRequest).Methods("POST")
authAPI.HandleFunc("/friend_request/{requestID}", Friends.RejectFriendRequest).Methods("DELETE")
authAPI.HandleFunc("/conversations", Messages.EncryptedConversationList).Methods("GET")
authAPI.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET")
- authAPI.HandleFunc("/conversations", Messages.reateConversation).Methods("POST")
+ authAPI.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST")
authAPI.HandleFunc("/conversations", Messages.UpdateConversation).Methods("PUT")
authAPI.HandleFunc("/message", Messages.CreateMessage).Methods("POST")
diff --git a/Backend/Database/FriendRequests.go b/Backend/Database/FriendRequests.go
index f6393d5..0f6e58a 100644
--- a/Backend/Database/FriendRequests.go
+++ b/Backend/Database/FriendRequests.go
@@ -42,6 +42,12 @@ func CreateFriendRequest(friendRequest *Models.FriendRequest) error {
Error
}
+// CreateFriendRequests creates multiple friend requests
+func CreateFriendRequests(friendRequest *[]Models.FriendRequest) error {
+ return DB.Create(friendRequest).
+ Error
+}
+
// UpdateFriendRequest Updates friend request
func UpdateFriendRequest(friendRequest *Models.FriendRequest) error {
return DB.Where("id = ?", friendRequest.ID).
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index 5536a1b..df5e935 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -44,7 +44,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.mobile"
- minSdkVersion flutter.minSdkVersion
+ minSdkVersion 20
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 7c170fc..d3ba628 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -43,5 +43,9 @@
UIViewControllerBasedStatusBarAppearance
+ io.flutter.embedded_views_preview
+
+ NSCameraUsageDescription
+ This app needs camera access to scan QR codes
diff --git a/mobile/lib/components/qr_reader.dart b/mobile/lib/components/qr_reader.dart
new file mode 100644
index 0000000..1ff79ed
--- /dev/null
+++ b/mobile/lib/components/qr_reader.dart
@@ -0,0 +1,163 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:Envelope/utils/storage/session_cookie.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
+import 'package:pointycastle/impl.dart';
+import 'package:qr_code_scanner/qr_code_scanner.dart';
+import 'package:sqflite/sqflite.dart';
+import 'package:uuid/uuid.dart';
+import 'package:http/http.dart' as http;
+
+import '/models/friends.dart';
+import '/models/my_profile.dart';
+import '/utils/encryption/aes_helper.dart';
+import '/utils/encryption/crypto_utils.dart';
+import '/utils/storage/database.dart';
+import '/utils/strings.dart';
+import 'flash_message.dart';
+
+class QrReader extends StatefulWidget {
+ const QrReader({
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ State createState() => _QrReaderState();
+}
+
+class _QrReaderState extends State {
+ final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
+ Barcode? result;
+ QRViewController? controller;
+
+ // In order to get hot reload to work we need to pause the camera if the platform
+ // is android, or resume the camera if the platform is iOS.
+ @override
+ void reassemble() {
+ super.reassemble();
+ if (Platform.isAndroid) {
+ controller!.pauseCamera();
+ } else if (Platform.isIOS) {
+ controller!.resumeCamera();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Column(
+ children: [
+ Expanded(
+ flex: 5,
+ child: QRView(
+ key: qrKey,
+ onQRViewCreated: _onQRViewCreated,
+ formatsAllowed: const [BarcodeFormat.qrcode],
+ overlay: QrScannerOverlayShape(),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _onQRViewCreated(QRViewController controller) {
+ this.controller = controller;
+ controller.scannedDataStream.listen((scanData) {
+ addFriend(scanData)
+ .then((dynamic ret) {
+ if (ret) {
+ // Delay exit to prevent exit mid way through rendering
+ Future.delayed(Duration.zero, () {
+ Navigator.of(context).pop();
+ });
+ }
+ });
+ });
+ }
+
+ @override
+ void dispose() {
+ controller?.dispose();
+ super.dispose();
+ }
+
+ Future addFriend(Barcode scanData) async {
+ Map friendJson = jsonDecode(scanData.code!);
+
+ RSAPublicKey publicKey = CryptoUtils.rsaPublicKeyFromPem(
+ String.fromCharCodes(
+ base64.decode(
+ friendJson['k']
+ )
+ )
+ );
+
+ MyProfile profile = await MyProfile.getProfile();
+
+ var uuid = const Uuid();
+
+ final symmetricKey1 = AesHelper.deriveKey(generateRandomString(32));
+ final symmetricKey2 = AesHelper.deriveKey(generateRandomString(32));
+
+ Friend request1 = Friend(
+ id: uuid.v4(),
+ userId: friendJson['i'],
+ username: profile.username,
+ friendId: profile.id,
+ friendSymmetricKey: base64.encode(symmetricKey1),
+ publicKey: profile.publicKey!,
+ acceptedAt: DateTime.now(),
+ );
+
+ Friend request2 = Friend(
+ id: uuid.v4(),
+ userId: profile.id,
+ friendId: friendJson['i'],
+ username: friendJson['u'],
+ friendSymmetricKey: base64.encode(symmetricKey2),
+ publicKey: publicKey,
+ acceptedAt: DateTime.now(),
+ );
+
+ String payload = jsonEncode([
+ request1.payloadJson(),
+ request2.payloadJson(),
+ ]);
+
+ var resp = await http.post(
+ Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friend_request/qr_code'),
+ headers: {
+ 'Content-Type': 'application/json; charset=UTF-8',
+ 'cookie': await getSessionCookie(),
+ },
+ body: payload,
+ );
+
+ if (resp.statusCode != 200) {
+ showMessage(
+ 'Failed to add friend, please try again later',
+ context
+ );
+ return false;
+ }
+
+ final db = await getDatabaseConnection();
+
+ await db.insert(
+ 'friends',
+ request1.toMap(),
+ conflictAlgorithm: ConflictAlgorithm.replace,
+ );
+
+ await db.insert(
+ 'friends',
+ request2.toMap(),
+ conflictAlgorithm: ConflictAlgorithm.replace,
+ );
+
+ return true;
+ }
+}
diff --git a/mobile/lib/models/friends.dart b/mobile/lib/models/friends.dart
index 269a8ad..c435697 100644
--- a/mobile/lib/models/friends.dart
+++ b/mobile/lib/models/friends.dart
@@ -129,6 +129,41 @@ class Friend{
);
}
+ Map payloadJson() {
+
+ Uint8List friendIdEncrypted = CryptoUtils.rsaEncrypt(
+ Uint8List.fromList(friendId.codeUnits),
+ publicKey,
+ );
+
+ Uint8List usernameEncrypted = CryptoUtils.rsaEncrypt(
+ Uint8List.fromList(username.codeUnits),
+ publicKey,
+ );
+
+ Uint8List symmetricKeyEncrypted = CryptoUtils.rsaEncrypt(
+ Uint8List.fromList(
+ base64.decode(friendSymmetricKey),
+ ),
+ publicKey,
+ );
+
+ var publicKeyEncrypted = AesHelper.aesEncrypt(
+ base64.decode(friendSymmetricKey),
+ Uint8List.fromList(CryptoUtils.encodeRSAPublicKeyToPem(publicKey).codeUnits),
+ );
+
+ return {
+ 'id': id,
+ 'user_id': userId,
+ 'friend_id': base64.encode(friendIdEncrypted),
+ 'friend_username': base64.encode(usernameEncrypted),
+ 'symmetric_key': base64.encode(symmetricKeyEncrypted),
+ 'asymmetric_public_key': publicKeyEncrypted,
+ 'accepted_at': null,
+ };
+ }
+
String publicKeyPem() {
return CryptoUtils.encodeRSAPublicKeyToPem(publicKey);
}
@@ -139,7 +174,7 @@ class Friend{
'user_id': userId,
'username': username,
'friend_id': friendId,
- 'symmetric_key': friendSymmetricKey,
+ 'symmetric_key': base64.encode(friendSymmetricKey.codeUnits),
'asymmetric_public_key': publicKeyPem(),
'accepted_at': acceptedAt?.toIso8601String(),
};
diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart
index 9ed72e5..fc65477 100644
--- a/mobile/lib/utils/storage/conversations.dart
+++ b/mobile/lib/utils/storage/conversations.dart
@@ -149,7 +149,7 @@ Future uploadConversation(Conversation conversation, BuildContext context)
Map conversationJson = await conversation.payloadJson();
- var x = await http.post(
+ var resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
@@ -158,7 +158,7 @@ Future uploadConversation(Conversation conversation, BuildContext context)
body: jsonEncode(conversationJson),
);
- if (x.statusCode != 200) {
+ if (resp.statusCode != 200) {
showMessage('Failed to create conversation', context);
}
}
diff --git a/mobile/lib/views/main/friend/list.dart b/mobile/lib/views/main/friend/list.dart
index ded6225..8f19a61 100644
--- a/mobile/lib/views/main/friend/list.dart
+++ b/mobile/lib/views/main/friend/list.dart
@@ -1,4 +1,5 @@
import 'package:Envelope/components/custom_title_bar.dart';
+import 'package:Envelope/components/qr_reader.dart';
import 'package:Envelope/views/main/friend/add_search.dart';
import 'package:Envelope/views/main/friend/request_list_item.dart';
import 'package:flutter/material.dart';
@@ -74,7 +75,11 @@ class _FriendListState extends State {
distance: 90.0,
children: [
ActionButton(
- onPressed: () {},
+ onPressed: () {
+ Navigator.of(context).push(
+ MaterialPageRoute(builder: (context) => const QrReader())
+ );//.then(onGoBack); // TODO
+ },
icon: const Icon(Icons.qr_code_2, size: 25),
),
ActionButton(
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index cfa6a60..917ab66 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -247,6 +247,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
+ qr_code_scanner:
+ dependency: "direct main"
+ description:
+ name: qr_code_scanner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
qr_flutter:
dependency: "direct main"
description:
@@ -414,5 +421,5 @@ packages:
source: hosted
version: "0.2.0+1"
sdks:
- dart: ">=2.17.0-0 <3.0.0"
+ dart: ">=2.17.0 <3.0.0"
flutter: ">=2.8.0"
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index ea62a83..6007c51 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -23,6 +23,7 @@ dependencies:
intl: ^0.17.0
uuid: ^3.0.6
qr_flutter: ^4.0.0
+ qr_code_scanner: ^1.0.1
dev_dependencies:
flutter_test:
diff --git a/mobile/test/pajamasenergy_qr_code.png b/mobile/test/pajamasenergy_qr_code.png
new file mode 100644
index 0000000..84584a2
Binary files /dev/null and b/mobile/test/pajamasenergy_qr_code.png differ
diff --git a/mobile/test/qr_payload.json b/mobile/test/qr_payload.json
new file mode 100644
index 0000000..4ed2a36
--- /dev/null
+++ b/mobile/test/qr_payload.json
@@ -0,0 +1 @@
+{"i": "deffd741-d67f-469e-975f-b2272404a43e", "u": "pajamasenergy","k": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KICBNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXlVbkVFQ2NWc1NzS254Wmx4K3VFCiAgSjBRVmNsdTNGTnVSZmp2V2N2ZW5hazQyMWVLN2xxODIrUHJjWml0dEdUMEx6TVF5M240Uk0wMTFteDJWZFQvMQogIDhZSmhLV2dWUjhCNTVJV2o4OHdvVG12eHRmQWc2QWphNE1sYzRlV3Q5VHFMVXdyaHBVdFcwcEVlZHhNVDEwS3YKICBKenlTcWpkYlhjQUxKYStIRSt0YzhxU2twbWJDV3ZkZlNHWGh5L2FkZjFERjVKMzA0WEVmYzk2MGVKWmVqdS9uCiAgWnEyYzJnM1NjOS9TQXQvQ3VjaWJFNFdydVlaM1hhYkxNTytwT0syZlNoUlpXMWlVaHhiMWlBSWJMWlFsZEpBcwogIEhRSmp6VlRXcjgwSDcwOFZuamRjZmJpVkxMVlpZT3RBMFFwa1lZd3ZQQ3UrN0ZQdFk2ZUhLZjFML0draGthY3IKICBaUUlEQVFBQgogIC0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ=="}