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=="}