Browse Source

Add friends through scanning qr code

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
bfa4f6b422
13 changed files with 279 additions and 10 deletions
  1. +49
    -3
      Backend/Api/Friends/Friends.go
  2. +2
    -1
      Backend/Api/Routes.go
  3. +6
    -0
      Backend/Database/FriendRequests.go
  4. +1
    -1
      mobile/android/app/build.gradle
  5. +4
    -0
      mobile/ios/Runner/Info.plist
  6. +163
    -0
      mobile/lib/components/qr_reader.dart
  7. +36
    -1
      mobile/lib/models/friends.dart
  8. +2
    -2
      mobile/lib/utils/storage/conversations.dart
  9. +6
    -1
      mobile/lib/views/main/friend/list.dart
  10. +8
    -1
      mobile/pubspec.lock
  11. +1
    -0
      mobile/pubspec.yaml
  12. BIN
      mobile/test/pajamasenergy_qr_code.png
  13. +1
    -0
      mobile/test/qr_payload.json

+ 49
- 3
Backend/Api/Friends/Friends.go View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"time"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "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) requestBody, err = ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
panic(err)
http.Error(w, "Error", http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
return
} }
err = json.Unmarshal(requestBody, &friendRequest) err = json.Unmarshal(requestBody, &friendRequest)
if err != nil { if err != nil {
panic(err)
http.Error(w, "Error", http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
return
} }
friendRequest.AcceptedAt.Scan(nil) friendRequest.AcceptedAt.Scan(nil)
err = Database.CreateFriendRequest(&friendRequest) err = Database.CreateFriendRequest(&friendRequest)
if err != nil { if err != nil {
panic(err)
http.Error(w, "Error", http.StatusInternalServerError)
w.WriteHeader(http.StatusInternalServerError)
return
} }
returnJSON, err = json.MarshalIndent(friendRequest, "", " ") returnJSON, err = json.MarshalIndent(friendRequest, "", " ")
@ -46,3 +53,42 @@ func CreateFriendRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(returnJSON) 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)
}

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

@ -65,12 +65,13 @@ func InitAPIEndpoints(router *mux.Router) {
authAPI.HandleFunc("/friend_requests", Friends.EncryptedFriendRequestList).Methods("GET") authAPI.HandleFunc("/friend_requests", Friends.EncryptedFriendRequestList).Methods("GET")
authAPI.HandleFunc("/friend_request", Friends.CreateFriendRequest).Methods("POST") 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.AcceptFriendRequest).Methods("POST")
authAPI.HandleFunc("/friend_request/{requestID}", Friends.RejectFriendRequest).Methods("DELETE") authAPI.HandleFunc("/friend_request/{requestID}", Friends.RejectFriendRequest).Methods("DELETE")
authAPI.HandleFunc("/conversations", Messages.EncryptedConversationList).Methods("GET") authAPI.HandleFunc("/conversations", Messages.EncryptedConversationList).Methods("GET")
authAPI.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).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("/conversations", Messages.UpdateConversation).Methods("PUT")
authAPI.HandleFunc("/message", Messages.CreateMessage).Methods("POST") authAPI.HandleFunc("/message", Messages.CreateMessage).Methods("POST")


+ 6
- 0
Backend/Database/FriendRequests.go View File

@ -42,6 +42,12 @@ func CreateFriendRequest(friendRequest *Models.FriendRequest) error {
Error Error
} }
// CreateFriendRequests creates multiple friend requests
func CreateFriendRequests(friendRequest *[]Models.FriendRequest) error {
return DB.Create(friendRequest).
Error
}
// UpdateFriendRequest Updates friend request // UpdateFriendRequest Updates friend request
func UpdateFriendRequest(friendRequest *Models.FriendRequest) error { func UpdateFriendRequest(friendRequest *Models.FriendRequest) error {
return DB.Where("id = ?", friendRequest.ID). return DB.Where("id = ?", friendRequest.ID).


+ 1
- 1
mobile/android/app/build.gradle View File

@ -44,7 +44,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.mobile" applicationId "com.example.mobile"
minSdkVersion flutter.minSdkVersion
minSdkVersion 20
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName


+ 4
- 0
mobile/ios/Runner/Info.plist View File

@ -43,5 +43,9 @@
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string>
</dict> </dict>
</plist> </plist>

+ 163
- 0
mobile/lib/components/qr_reader.dart View File

@ -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<QrReader> createState() => _QrReaderState();
}
class _QrReaderState extends State<QrReader> {
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: <Widget>[
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<bool> addFriend(Barcode scanData) async {
Map<String, dynamic> 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: <String, String>{
'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;
}
}

+ 36
- 1
mobile/lib/models/friends.dart View File

@ -129,6 +129,41 @@ class Friend{
); );
} }
Map<String, dynamic> 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() { String publicKeyPem() {
return CryptoUtils.encodeRSAPublicKeyToPem(publicKey); return CryptoUtils.encodeRSAPublicKeyToPem(publicKey);
} }
@ -139,7 +174,7 @@ class Friend{
'user_id': userId, 'user_id': userId,
'username': username, 'username': username,
'friend_id': friendId, 'friend_id': friendId,
'symmetric_key': friendSymmetricKey,
'symmetric_key': base64.encode(friendSymmetricKey.codeUnits),
'asymmetric_public_key': publicKeyPem(), 'asymmetric_public_key': publicKeyPem(),
'accepted_at': acceptedAt?.toIso8601String(), 'accepted_at': acceptedAt?.toIso8601String(),
}; };


+ 2
- 2
mobile/lib/utils/storage/conversations.dart View File

@ -149,7 +149,7 @@ Future<void> uploadConversation(Conversation conversation, BuildContext context)
Map<String, dynamic> conversationJson = await conversation.payloadJson(); Map<String, dynamic> conversationJson = await conversation.payloadJson();
var x = await http.post(
var resp = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
headers: <String, String>{ headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
@ -158,7 +158,7 @@ Future<void> uploadConversation(Conversation conversation, BuildContext context)
body: jsonEncode(conversationJson), body: jsonEncode(conversationJson),
); );
if (x.statusCode != 200) {
if (resp.statusCode != 200) {
showMessage('Failed to create conversation', context); showMessage('Failed to create conversation', context);
} }
} }


+ 6
- 1
mobile/lib/views/main/friend/list.dart View File

@ -1,4 +1,5 @@
import 'package:Envelope/components/custom_title_bar.dart'; 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/add_search.dart';
import 'package:Envelope/views/main/friend/request_list_item.dart'; import 'package:Envelope/views/main/friend/request_list_item.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -74,7 +75,11 @@ class _FriendListState extends State<FriendList> {
distance: 90.0, distance: 90.0,
children: [ children: [
ActionButton( ActionButton(
onPressed: () {},
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const QrReader())
);//.then(onGoBack); // TODO
},
icon: const Icon(Icons.qr_code_2, size: 25), icon: const Icon(Icons.qr_code_2, size: 25),
), ),
ActionButton( ActionButton(


+ 8
- 1
mobile/pubspec.lock View File

@ -247,6 +247,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" 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: qr_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -414,5 +421,5 @@ packages:
source: hosted source: hosted
version: "0.2.0+1" version: "0.2.0+1"
sdks: sdks:
dart: ">=2.17.0-0 <3.0.0"
dart: ">=2.17.0 <3.0.0"
flutter: ">=2.8.0" flutter: ">=2.8.0"

+ 1
- 0
mobile/pubspec.yaml View File

@ -23,6 +23,7 @@ dependencies:
intl: ^0.17.0 intl: ^0.17.0
uuid: ^3.0.6 uuid: ^3.0.6
qr_flutter: ^4.0.0 qr_flutter: ^4.0.0
qr_code_scanner: ^1.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:


BIN
mobile/test/pajamasenergy_qr_code.png View File

Before After
Width: 500  |  Height: 500  |  Size: 13 KiB

+ 1
- 0
mobile/test/qr_payload.json View File

@ -0,0 +1 @@
{"i": "deffd741-d67f-469e-975f-b2272404a43e", "u": "pajamasenergy","k": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KICBNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXlVbkVFQ2NWc1NzS254Wmx4K3VFCiAgSjBRVmNsdTNGTnVSZmp2V2N2ZW5hazQyMWVLN2xxODIrUHJjWml0dEdUMEx6TVF5M240Uk0wMTFteDJWZFQvMQogIDhZSmhLV2dWUjhCNTVJV2o4OHdvVG12eHRmQWc2QWphNE1sYzRlV3Q5VHFMVXdyaHBVdFcwcEVlZHhNVDEwS3YKICBKenlTcWpkYlhjQUxKYStIRSt0YzhxU2twbWJDV3ZkZlNHWGh5L2FkZjFERjVKMzA0WEVmYzk2MGVKWmVqdS9uCiAgWnEyYzJnM1NjOS9TQXQvQ3VjaWJFNFdydVlaM1hhYkxNTytwT0syZlNoUlpXMWlVaHhiMWlBSWJMWlFsZEpBcwogIEhRSmp6VlRXcjgwSDcwOFZuamRjZmJpVkxMVlpZT3RBMFFwa1lZd3ZQQ3UrN0ZQdFk2ZUhLZjFML0draGthY3IKICBaUUlEQVFBQgogIC0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ=="}

Loading…
Cancel
Save