diff --git a/Backend/Api/Friends/EncryptedFriendsList.go b/Backend/Api/Friends/EncryptedFriendsList.go index 5179c9a..2db0531 100644 --- a/Backend/Api/Friends/EncryptedFriendsList.go +++ b/Backend/Api/Friends/EncryptedFriendsList.go @@ -18,7 +18,6 @@ func EncryptedFriendList(w http.ResponseWriter, r *http.Request) { ) userSession, err = Auth.CheckCookie(r) - if err != nil { http.Error(w, "Forbidden", http.StatusUnauthorized) return diff --git a/Backend/Api/Friends/FriendRequest.go b/Backend/Api/Friends/FriendRequest.go index 078bdcb..4943f17 100644 --- a/Backend/Api/Friends/FriendRequest.go +++ b/Backend/Api/Friends/FriendRequest.go @@ -15,8 +15,9 @@ func FriendRequest(w http.ResponseWriter, r *http.Request) { user Models.User requestBody []byte requestJson map[string]interface{} - friendID []byte + friendID string friendRequest Models.Friend + ok bool err error ) @@ -38,8 +39,8 @@ func FriendRequest(w http.ResponseWriter, r *http.Request) { return } - friendID, err = Util.ToBytes(requestJson["id"]) - if requestJson["id"] == nil { + friendID, ok = requestJson["id"].(string) + if !ok { http.Error(w, "Error", http.StatusInternalServerError) return } diff --git a/Backend/Api/Friends/Friends.go b/Backend/Api/Friends/Friends.go index 0706e14..a991fb6 100644 --- a/Backend/Api/Friends/Friends.go +++ b/Backend/Api/Friends/Friends.go @@ -2,8 +2,10 @@ package Friends import ( "encoding/json" + "io/ioutil" "net/http" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Util" ) @@ -30,3 +32,38 @@ func Friend(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(returnJson) } + +func CreateFriendRequest(w http.ResponseWriter, r *http.Request) { + var ( + friendData Models.Friend + requestBody []byte + returnJson []byte + err error + ) + + requestBody, err = ioutil.ReadAll(r.Body) + if err != nil { + panic(err) + } + + err = json.Unmarshal(requestBody, &friendData) + if err != nil { + panic(err) + } + + err = Database.CreateFriendRequest(&friendData) + if err != nil { + panic(err) + } + + returnJson, err = json.MarshalIndent(friendData, "", " ") + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Return updated json + w.WriteHeader(http.StatusOK) + w.Write(returnJson) +} diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index adc2f4a..a362154 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -31,7 +31,6 @@ func authenticationMiddleware(next http.Handler) http.Handler { ) _, err = Auth.CheckCookie(r) - if err != nil { http.Error(w, "Forbidden", http.StatusUnauthorized) return @@ -61,6 +60,7 @@ func InitApiEndpoints(router *mux.Router) { authApi.Use(authenticationMiddleware) // Define routes for friends and friend requests + authApi.HandleFunc("/friend", Friends.CreateFriendRequest).Methods("POST") authApi.HandleFunc("/friends", Friends.EncryptedFriendList).Methods("GET") authApi.HandleFunc("/friend/{userID}", Friends.Friend).Methods("GET") authApi.HandleFunc("/friend/{userID}/request", Friends.FriendRequest).Methods("POST") diff --git a/Backend/Database/Seeder/FriendSeeder.go b/Backend/Database/Seeder/FriendSeeder.go index 771dcad..bcacf9d 100644 --- a/Backend/Database/Seeder/FriendSeeder.go +++ b/Backend/Database/Seeder/FriendSeeder.go @@ -1,6 +1,7 @@ package Seeder import ( + "encoding/base64" "time" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" @@ -15,7 +16,7 @@ func seedFriend(user, friendUser Models.User) error { friend = Models.Friend{ UserID: user.ID, - FriendID: encryptWithPublicKey(friendUser.ID.Bytes(), decodedPublicKey), + FriendID: base64.StdEncoding.EncodeToString(encryptWithPublicKey([]byte(friendUser.ID.String()), decodedPublicKey)), AcceptedAt: time.Now(), } @@ -26,7 +27,7 @@ func seedFriend(user, friendUser Models.User) error { friend = Models.Friend{ UserID: friendUser.ID, - FriendID: encryptWithPublicKey(user.ID.Bytes(), decodedPublicKey), + FriendID: base64.StdEncoding.EncodeToString(encryptWithPublicKey([]byte(user.ID.String()), decodedPublicKey)), AcceptedAt: time.Now(), } diff --git a/Backend/Database/Seeder/MessageSeeder.go b/Backend/Database/Seeder/MessageSeeder.go index 6a7504d..29de572 100644 --- a/Backend/Database/Seeder/MessageSeeder.go +++ b/Backend/Database/Seeder/MessageSeeder.go @@ -6,7 +6,7 @@ import ( "crypto/cipher" "crypto/rand" "crypto/rsa" - "crypto/sha512" + "crypto/sha256" "encoding/pem" "fmt" "hash" @@ -23,7 +23,7 @@ func encryptWithPublicKey(msg []byte, pub *rsa.PublicKey) []byte { hash hash.Hash ) - hash = sha512.New() + hash = sha256.New() ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil) if err != nil { panic(err) diff --git a/Backend/Database/Seeder/Seed.go b/Backend/Database/Seeder/Seed.go index 07b7248..28825f4 100644 --- a/Backend/Database/Seeder/Seed.go +++ b/Backend/Database/Seeder/Seed.go @@ -1,33 +1,80 @@ package Seeder import ( + "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/x509" "encoding/pem" + "errors" "log" ) const ( // Encrypted with "password" - encryptedPrivateKey string = ` -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 -` - - publicKey string = ` ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAidHsO9H0fMd6cu9dQiOwu/qtaDq+lG59kudoHGx8WZhucpOSp2df95UzjnXmMw1fz0Mx5fjgBaunG7u1MVBy+7IdYwXp+iz3xZulVy1Yv3e+GMzgQhAuxhz/8wsHkFkUTweDZCrCZPhkTXlCrxDIuKykQ0el+RnpSjyyljOsAWAdTmrz0a2Nh9FOmF1v49pWy3Z3aJG58xl1dmFkpXjT3m36GB4Z30iR1uOMnNdrtfwIfLQAc7nmle2LxCHeEMYzlA9y6JChm5kGI6FmglSKYTxvDm40jA5mYyDCPADeCodbIw4Mtm0nwrM3QqKWj+jlaL8BY7/jjaosmz7VK2do4wIDAQAB ------END PUBLIC KEY----- -` + encryptedPrivateKey string = `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` + + // Private key for testing server side + privateKey string = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJScQQJxWxKwqf +FmXH64QnRBVyW7cU25F+O9Zy96dqTjbV4ruWrzb4+txmK20ZPQvMxDLefhEzTXWb +HZV1P/XxgmEpaBVHwHnkhaPzzChOa/G18CDoCNrgyVzh5a31OotTCuGlS1bSkR53 +ExPXQq8nPJKqN1tdwAslr4cT61zypKSmZsJa919IZeHL9p1/UMXknfThcR9z3rR4 +ll6O7+dmrZzaDdJz39IC38K5yJsThau5hnddpssw76k4rZ9KFFlbWJSHFvWIAhst +lCV0kCwdAmPNVNavzQfvTxWeN1x9uJUstVlg60DRCmRhjC88K77sU+1jp4cp/Uv8 +aSGRpytlAgMBAAECggEBALFbJr8YwRs/EnfEU2AI24OBkOgXecSOBq9UaAsavU+E +pPpmceU+c1CEMUhwwQs457m/shaqu9sZSCOpuHP8LGdk+tlyFTYImR5KxoBdBbK7 +l9k4QLZSfxELO6TrLBDkSbic4N8098ZHCbHfhF7qKcyHqa8DYaTEPs4wz/M0Mcy0 +xziCxMUFh/LhSLDH8PMMXZ+HV3+zmxdEqmaZvk3FQOGD1O39I9TA8PnFa11whVbN +nMSjxgmK+byPIM4LFXNHk+TZsJm1FaYaGVdLetAPET7p6XMrMWy+z/4dcb4GbYjY +0i5Xv1lVlIRgDB9xj0MOW5hzQzTPHC4JN4nIoBFSc20CgYEA5IgymckwqKJJWXRn +AIJ3guuEp4vBtjmdVCJnFmbPEeW+WY+CNuwn9DK78Zavfn1HruryE/hkYLVNPm8y +KSf16+tIadUXcao1UIVDNSVC6jtFmRLgWuPXbNKFQwUor1ai9IK+F3JV8pfr36HE +8rk/LEM0DIgsTg+j+IKT39a7IucCgYEA4XtKGhvnGUdcveMPcrvuQlSnucSpw5Ly +4KuRsTySdMihhxX1GSyg6F2T4YKFRqKZERsYgYk6A32u53If+VkXacvOsInwuoBa +FTb3fOQpw1xBSI7R3RgiriY4cCsDetexEBbg7/SrodpQu254A8+5PKxrSR1U+idx +boX745k1gdMCgYEAuZ7CctTOV/pQ137LdseBqO4BNlE2yvrrBf5XewOQZzoTLQ16 +N4ADR765lxXMf1HkmnesnnnflglMr0yEEpepkLDvhT6WpzUXzsoe95jHTBdOhXGm +l0x+mp43rWMQU7Jr82wKWGL+2md5J5ButrOuUxZWvWMRkWn0xhHRaDsyjrsCgYAq +zNRMEG/VhI4+HROZm8KmJJuRz5rJ3OLtcqO9GNpUAKFomupjVO1WLi0b6UKTHdog +PRxxujKg5wKEPE2FbzvagS1CpWxkemifDkf8FPM4ehKKS1HavfIXTHn6ELAgaUDa +5Pzdj3vkxSP98AIn9w4aTkAvKLowobwOVrBxi2t0sQKBgHh2TrGSnlV3s1DijfNM +0JiwsHWz0hljybcZaZP45nsgGRiR15TcIiOLwkjaCws2tYtOSOT4sM7HV/s2mpPa +b0XvaLzh1iKG7HZ9tvPt/VhHlKKosNBK/j4fvgMZg7/bhRfHmaDQKoqlGbtyWjEQ +mj1b2/Gnbk3VYDR16BFfj7m2 +-----END PRIVATE KEY-----` + + publicKey string = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyUnEECcVsSsKnxZlx+uE +J0QVclu3FNuRfjvWcvenak421eK7lq82+PrcZittGT0LzMQy3n4RM011mx2VdT/1 +8YJhKWgVR8B55IWj88woTmvxtfAg6Aja4Mlc4eWt9TqLUwrhpUtW0pEedxMT10Kv +JzySqjdbXcALJa+HE+tc8qSkpmbCWvdfSGXhy/adf1DF5J304XEfc960eJZeju/n +Zq2c2g3Sc9/SAt/CucibE4WruYZ3XabLMO+pOK2fShRZW1iUhxb1iAIbLZQldJAs +HQJjzVTWr80H708VnjdcfbiVLLVZYOtA0QpkYYwvPCu+7FPtY6eHKf1L/Gkhkacr +ZQIDAQAB +-----END PUBLIC KEY-----` ) var ( decodedPublicKey *rsa.PublicKey + // decodedPrivateKey *rsa.PrivateKey ) +// DecryptWithPrivateKey decrypts data with private key +func decryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) []byte { + hash := sha256.New() + plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil) + if err != nil { + panic(err) + } + return plaintext +} + func Seed() { var ( block *pem.Block decKey any + ok bool err error ) @@ -36,7 +83,10 @@ func Seed() { if err != nil { panic(err) } - decodedPublicKey = decKey.(*rsa.PublicKey) + decodedPublicKey, ok = decKey.(*rsa.PublicKey) + if !ok { + panic(errors.New("Invalid decodedPublicKey")) + } log.Println("Seeding users...") SeedUsers() diff --git a/Backend/Models/Friends.go b/Backend/Models/Friends.go index b76d530..b435223 100644 --- a/Backend/Models/Friends.go +++ b/Backend/Models/Friends.go @@ -11,6 +11,6 @@ type Friend struct { Base UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` User User `json:"user"` - FriendID []byte `gorm:"not null" json:"friend_id"` // Stored encrypted + FriendID string `gorm:"not null" json:"friend_id"` // Stored encrypted AcceptedAt time.Time `json:"accepted_at"` } diff --git a/mobile/lib/models/friends.dart b/mobile/lib/models/friends.dart index 832d637..8afdcb4 100644 --- a/mobile/lib/models/friends.dart +++ b/mobile/lib/models/friends.dart @@ -1,9 +1,54 @@ +import 'dart:convert'; +import "package:pointycastle/export.dart"; +import '/utils/encryption/crypto_utils.dart'; class Friend{ String id; - String username; + String userId; + String friendId; + String friendIdDecrypted; + String acceptedAt; Friend({ required this.id, - required this.username, + required this.userId, + required this.friendId, + required this.friendIdDecrypted, + required this.acceptedAt, }); + + factory Friend.fromJson(Map json, RSAPrivateKey privKey) { + var friendIdDecrypted = CryptoUtils.rsaDecrypt( + base64.decode(json['friend_id']), + privKey, + ); + + return Friend( + id: json['id'], + userId: json['user_id'], + friendId: json['friend_id'], + friendIdDecrypted: String.fromCharCodes(friendIdDecrypted), + acceptedAt: json['accepted_at'], + ); + } + + @override + String toString() { + return ''' +id: $id +userId: $userId, +friendId: $friendId, +friendIdDecrypted: $friendIdDecrypted, +accepted_at: $acceptedAt, +'''; + } + + Map toMap() { + return { + 'id': id, + 'user_id': userId, + 'friend_id': friendId, + 'friend_id_decrypted': friendIdDecrypted, + 'accepted_at': acceptedAt, + }; + } } diff --git a/mobile/lib/utils/encryption/crypto_utils.dart b/mobile/lib/utils/encryption/crypto_utils.dart new file mode 100644 index 0000000..d580eda --- /dev/null +++ b/mobile/lib/utils/encryption/crypto_utils.dart @@ -0,0 +1,979 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:pointycastle/asn1/object_identifiers.dart'; +import 'package:pointycastle/export.dart'; +import 'package:pointycastle/pointycastle.dart'; +import 'package:pointycastle/src/utils.dart' as thing; +import 'package:pointycastle/ecc/ecc_fp.dart' as ecc_fp; + +import './string_utils.dart'; + +/// +/// Helper class for cryptographic operations +/// +class CryptoUtils { + static const BEGIN_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----'; + static const END_PRIVATE_KEY = '-----END PRIVATE KEY-----'; + + static const BEGIN_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----'; + static const END_PUBLIC_KEY = '-----END PUBLIC KEY-----'; + + static const BEGIN_EC_PRIVATE_KEY = '-----BEGIN EC PRIVATE KEY-----'; + static const END_EC_PRIVATE_KEY = '-----END EC PRIVATE KEY-----'; + + static const BEGIN_EC_PUBLIC_KEY = '-----BEGIN EC PUBLIC KEY-----'; + static const END_EC_PUBLIC_KEY = '-----END EC PUBLIC KEY-----'; + + static const BEGIN_RSA_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----'; + static const END_RSA_PRIVATE_KEY = '-----END RSA PRIVATE KEY-----'; + + static const BEGIN_RSA_PUBLIC_KEY = '-----BEGIN RSA PUBLIC KEY-----'; + static const END_RSA_PUBLIC_KEY = '-----END RSA PUBLIC KEY-----'; + + /// + /// Converts the [RSAPublicKey.modulus] from the given [publicKey] to a [Uint8List]. + /// + static Uint8List rsaPublicKeyModulusToBytes(RSAPublicKey publicKey) => + thing.encodeBigInt(publicKey.modulus); + + /// + /// Converts the [RSAPublicKey.exponent] from the given [publicKey] to a [Uint8List]. + /// + static Uint8List rsaPublicKeyExponentToBytes(RSAPublicKey publicKey) => + thing.encodeBigInt(publicKey.exponent); + + /// + /// Converts the [RSAPrivateKey.modulus] from the given [privateKey] to a [Uint8List]. + /// + static Uint8List rsaPrivateKeyModulusToBytes(RSAPrivateKey privateKey) => + thing.encodeBigInt(privateKey.modulus); + + /// + /// Converts the [RSAPrivateKey.exponent] from the given [privateKey] to a [Uint8List]. + /// + static Uint8List rsaPrivateKeyExponentToBytes(RSAPrivateKey privateKey) => + thing.encodeBigInt(privateKey.exponent); + + /// + /// Get a SHA1 Thumbprint for the given [bytes]. + /// + @Deprecated('Use [getHash]') + static String getSha1ThumbprintFromBytes(Uint8List bytes) { + return getHash(bytes, algorithmName: 'SHA-1'); + } + + /// + /// Get a SHA256 Thumbprint for the given [bytes]. + /// + @Deprecated('Use [getHash]') + static String getSha256ThumbprintFromBytes(Uint8List bytes) { + return getHash(bytes, algorithmName: 'SHA-256'); + } + + /// + /// Get a MD5 Thumbprint for the given [bytes]. + /// + @Deprecated('Use [getHash]') + static String getMd5ThumbprintFromBytes(Uint8List bytes) { + return getHash(bytes, algorithmName: 'MD5'); + } + + /// + /// Get a hash for the given [bytes] using the given [algorithm] + /// + /// The default [algorithm] used is **SHA-256**. All supported algorihms are : + /// + /// * SHA-1 + /// * SHA-224 + /// * SHA-256 + /// * SHA-384 + /// * SHA-512 + /// * SHA-512/224 + /// * SHA-512/256 + /// * MD5 + /// + static String getHash(Uint8List bytes, {String algorithmName = 'SHA-256'}) { + var hash = getHashPlain(bytes, algorithmName: algorithmName); + + const hexDigits = '0123456789abcdef'; + var charCodes = Uint8List(hash.length * 2); + for (var i = 0, j = 0; i < hash.length; i++) { + var byte = hash[i]; + charCodes[j++] = hexDigits.codeUnitAt((byte >> 4) & 0xF); + charCodes[j++] = hexDigits.codeUnitAt(byte & 0xF); + } + + return String.fromCharCodes(charCodes).toUpperCase(); + } + + /// + /// Get a hash for the given [bytes] using the given [algorithm] + /// + /// The default [algorithm] used is **SHA-256**. All supported algorihms are : + /// + /// * SHA-1 + /// * SHA-224 + /// * SHA-256 + /// * SHA-384 + /// * SHA-512 + /// * SHA-512/224 + /// * SHA-512/256 + /// * MD5 + /// + static Uint8List getHashPlain(Uint8List bytes, + {String algorithmName = 'SHA-256'}) { + Uint8List hash; + switch (algorithmName) { + case 'SHA-1': + hash = Digest('SHA-1').process(bytes); + break; + case 'SHA-224': + hash = Digest('SHA-224').process(bytes); + break; + case 'SHA-256': + hash = Digest('SHA-256').process(bytes); + break; + case 'SHA-384': + hash = Digest('SHA-384').process(bytes); + break; + case 'SHA-512': + hash = Digest('SHA-512').process(bytes); + break; + case 'SHA-512/224': + hash = Digest('SHA-512/224').process(bytes); + break; + case 'SHA-512/256': + hash = Digest('SHA-512/256').process(bytes); + break; + case 'MD5': + hash = Digest('MD5').process(bytes); + break; + default: + throw ArgumentError('Hash not supported'); + } + + return hash; + } + + /// + /// Generates a RSA [AsymmetricKeyPair] with the given [keySize]. + /// The default value for the [keySize] is 2048 bits. + /// + /// The following keySize is supported: + /// * 1024 + /// * 2048 + /// * 4096 + /// * 8192 + /// + static AsymmetricKeyPair generateRSAKeyPair({int keySize = 2048}) { + var keyParams = + RSAKeyGeneratorParameters(BigInt.parse('65537'), keySize, 12); + + var secureRandom = _getSecureRandom(); + + var rngParams = ParametersWithRandom(keyParams, secureRandom); + var generator = RSAKeyGenerator(); + generator.init(rngParams); + + final pair = generator.generateKeyPair(); + + final myPublic = pair.publicKey as RSAPublicKey; + final myPrivate = pair.privateKey as RSAPrivateKey; + + return AsymmetricKeyPair(myPublic, myPrivate); + } + + /// + /// Generates a elliptic curve [AsymmetricKeyPair]. + /// + /// The default curve is **prime256v1** + /// + /// The following curves are supported: + /// + /// * brainpoolp160r1 + /// * brainpoolp160t1 + /// * brainpoolp192r1 + /// * brainpoolp192t1 + /// * brainpoolp224r1 + /// * brainpoolp224t1 + /// * brainpoolp256r1 + /// * brainpoolp256t1 + /// * brainpoolp320r1 + /// * brainpoolp320t1 + /// * brainpoolp384r1 + /// * brainpoolp384t1 + /// * brainpoolp512r1 + /// * brainpoolp512t1 + /// * GostR3410-2001-CryptoPro-A + /// * GostR3410-2001-CryptoPro-B + /// * GostR3410-2001-CryptoPro-C + /// * GostR3410-2001-CryptoPro-XchA + /// * GostR3410-2001-CryptoPro-XchB + /// * prime192v1 + /// * prime192v2 + /// * prime192v3 + /// * prime239v1 + /// * prime239v2 + /// * prime239v3 + /// * prime256v1 + /// * secp112r1 + /// * secp112r2 + /// * secp128r1 + /// * secp128r2 + /// * secp160k1 + /// * secp160r1 + /// * secp160r2 + /// * secp192k1 + /// * secp192r1 + /// * secp224k1 + /// * secp224r1 + /// * secp256k1 + /// * secp256r1 + /// * secp384r1 + /// * secp521r1 + /// + static AsymmetricKeyPair generateEcKeyPair({String curve = 'prime256v1'}) { + var ecDomainParameters = ECDomainParameters(curve); + var keyParams = ECKeyGeneratorParameters(ecDomainParameters); + + var secureRandom = _getSecureRandom(); + + var rngParams = ParametersWithRandom(keyParams, secureRandom); + var generator = ECKeyGenerator(); + generator.init(rngParams); + + return generator.generateKeyPair(); + } + + /// + /// Generates a secure [FortunaRandom] + /// + static SecureRandom _getSecureRandom() { + var secureRandom = FortunaRandom(); + var random = Random.secure(); + var seeds = []; + for (var i = 0; i < 32; i++) { + seeds.add(random.nextInt(255)); + } + secureRandom.seed(KeyParameter(Uint8List.fromList(seeds))); + return secureRandom; + } + + /// + /// Enode the given [publicKey] to PEM format using the PKCS#8 standard. + /// + static String encodeRSAPublicKeyToPem(RSAPublicKey publicKey) { + var algorithmSeq = ASN1Sequence(); + var paramsAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x5, 0x0])); + algorithmSeq.add(ASN1ObjectIdentifier.fromName('rsaEncryption')); + algorithmSeq.add(paramsAsn1Obj); + + var publicKeySeq = ASN1Sequence(); + publicKeySeq.add(ASN1Integer(publicKey.modulus)); + publicKeySeq.add(ASN1Integer(publicKey.exponent)); + var publicKeySeqBitString = + ASN1BitString(stringValues: Uint8List.fromList(publicKeySeq.encode())); + + var topLevelSeq = ASN1Sequence(); + topLevelSeq.add(algorithmSeq); + topLevelSeq.add(publicKeySeqBitString); + var dataBase64 = base64.encode(topLevelSeq.encode()); + var chunks = StringUtils.chunk(dataBase64, 64); + + return '$BEGIN_PUBLIC_KEY\n${chunks.join('\n')}\n$END_PUBLIC_KEY'; + } + + /// + /// Enode the given [rsaPublicKey] to PEM format using the PKCS#1 standard. + /// + /// The ASN1 structure is decripted at . + /// + /// ``` + /// RSAPublicKey ::= SEQUENCE { + /// modulus INTEGER, -- n + /// publicExponent INTEGER -- e + /// } + /// ``` + /// + static String encodeRSAPublicKeyToPemPkcs1(RSAPublicKey rsaPublicKey) { + var topLevelSeq = ASN1Sequence(); + topLevelSeq.add(ASN1Integer(rsaPublicKey.modulus)); + topLevelSeq.add(ASN1Integer(rsaPublicKey.exponent)); + + var dataBase64 = base64.encode(topLevelSeq.encode()); + var chunks = StringUtils.chunk(dataBase64, 64); + + return '$BEGIN_RSA_PUBLIC_KEY\n${chunks.join('\n')}\n$END_RSA_PUBLIC_KEY'; + } + + /// + /// Enode the given [rsaPrivateKey] to PEM format using the PKCS#1 standard. + /// + /// The ASN1 structure is decripted at . + /// + /// ``` + /// RSAPrivateKey ::= SEQUENCE { + /// version Version, + /// modulus INTEGER, -- n + /// publicExponent INTEGER, -- e + /// privateExponent INTEGER, -- d + /// prime1 INTEGER, -- p + /// prime2 INTEGER, -- q + /// exponent1 INTEGER, -- d mod (p-1) + /// exponent2 INTEGER, -- d mod (q-1) + /// coefficient INTEGER, -- (inverse of q) mod p + /// otherPrimeInfos OtherPrimeInfos OPTIONAL + /// } + /// ``` + static String encodeRSAPrivateKeyToPemPkcs1(RSAPrivateKey rsaPrivateKey) { + var version = ASN1Integer(BigInt.from(0)); + var modulus = ASN1Integer(rsaPrivateKey.n); + var publicExponent = ASN1Integer(BigInt.parse('65537')); + var privateExponent = ASN1Integer(rsaPrivateKey.privateExponent); + + var p = ASN1Integer(rsaPrivateKey.p); + var q = ASN1Integer(rsaPrivateKey.q); + var dP = + rsaPrivateKey.privateExponent! % (rsaPrivateKey.p! - BigInt.from(1)); + var exp1 = ASN1Integer(dP); + var dQ = + rsaPrivateKey.privateExponent! % (rsaPrivateKey.q! - BigInt.from(1)); + var exp2 = ASN1Integer(dQ); + var iQ = rsaPrivateKey.q!.modInverse(rsaPrivateKey.p!); + var co = ASN1Integer(iQ); + + var topLevelSeq = ASN1Sequence(); + topLevelSeq.add(version); + topLevelSeq.add(modulus); + topLevelSeq.add(publicExponent); + topLevelSeq.add(privateExponent); + topLevelSeq.add(p); + topLevelSeq.add(q); + topLevelSeq.add(exp1); + topLevelSeq.add(exp2); + topLevelSeq.add(co); + var dataBase64 = base64.encode(topLevelSeq.encode()); + var chunks = StringUtils.chunk(dataBase64, 64); + return '$BEGIN_RSA_PRIVATE_KEY\n${chunks.join('\n')}\n$END_RSA_PRIVATE_KEY'; + } + + /// + /// Enode the given [rsaPrivateKey] to PEM format using the PKCS#8 standard. + /// + /// The ASN1 structure is decripted at . + /// ``` + /// PrivateKeyInfo ::= SEQUENCE { + /// version Version, + /// algorithm AlgorithmIdentifier, + /// PrivateKey BIT STRING + /// } + /// ``` + /// + static String encodeRSAPrivateKeyToPem(RSAPrivateKey rsaPrivateKey) { + 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(rsaPrivateKey.n); + var publicExponent = ASN1Integer(BigInt.parse('65537')); + var privateExponent = ASN1Integer(rsaPrivateKey.privateExponent); + var p = ASN1Integer(rsaPrivateKey.p); + var q = ASN1Integer(rsaPrivateKey.q); + var dP = + rsaPrivateKey.privateExponent! % (rsaPrivateKey.p! - BigInt.from(1)); + var exp1 = ASN1Integer(dP); + var dQ = + rsaPrivateKey.privateExponent! % (rsaPrivateKey.q! - BigInt.from(1)); + var exp2 = ASN1Integer(dQ); + var iQ = rsaPrivateKey.q!.modInverse(rsaPrivateKey.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(octets: Uint8List.fromList(privateKeySeq.encode())); + + var topLevelSeq = ASN1Sequence(); + topLevelSeq.add(version); + topLevelSeq.add(algorithmSeq); + topLevelSeq.add(publicKeySeqOctetString); + var dataBase64 = base64.encode(topLevelSeq.encode()); + var chunks = StringUtils.chunk(dataBase64, 64); + return '$BEGIN_PRIVATE_KEY\n${chunks.join('\n')}\n$END_PRIVATE_KEY'; + } + + /// + /// Decode a [RSAPrivateKey] from the given [pem] String. + /// + static RSAPrivateKey rsaPrivateKeyFromPem(String pem) { + var bytes = getBytesFromPEMString(pem); + return rsaPrivateKeyFromDERBytes(bytes); + } + + /// + /// Decode the given [bytes] into an [RSAPrivateKey]. + /// + static RSAPrivateKey rsaPrivateKeyFromDERBytes(Uint8List bytes) { + var asn1Parser = ASN1Parser(bytes); + var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; + //ASN1Object version = topLevelSeq.elements[0]; + //ASN1Object algorithm = topLevelSeq.elements[1]; + var privateKey = topLevelSeq.elements![2]; + + asn1Parser = ASN1Parser(privateKey.valueBytes); + var pkSeq = asn1Parser.nextObject() as ASN1Sequence; + + var modulus = pkSeq.elements![1] as ASN1Integer; + //ASN1Integer 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; + //ASN1Integer exp1 = pkSeq.elements[6] as ASN1Integer; + //ASN1Integer exp2 = pkSeq.elements[7] as ASN1Integer; + //ASN1Integer co = pkSeq.elements[8] as ASN1Integer; + + var rsaPrivateKey = RSAPrivateKey( + modulus.integer!, privateExponent.integer!, p.integer, q.integer); + + return rsaPrivateKey; + } + + /// + /// Decode a [RSAPrivateKey] from the given [pem] string formated in the pkcs1 standard. + /// + static RSAPrivateKey rsaPrivateKeyFromPemPkcs1(String pem) { + var bytes = getBytesFromPEMString(pem); + return rsaPrivateKeyFromDERBytesPkcs1(bytes); + } + + /// + /// Decode the given [bytes] into an [RSAPrivateKey]. + /// + /// The [bytes] need to follow the the pkcs1 standard + /// + static RSAPrivateKey rsaPrivateKeyFromDERBytesPkcs1(Uint8List bytes) { + var asn1Parser = ASN1Parser(bytes); + var pkSeq = asn1Parser.nextObject() as ASN1Sequence; + + var modulus = pkSeq.elements![1] as ASN1Integer; + //ASN1Integer 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; + //ASN1Integer exp1 = pkSeq.elements[6] as ASN1Integer; + //ASN1Integer exp2 = pkSeq.elements[7] as ASN1Integer; + //ASN1Integer co = pkSeq.elements[8] as ASN1Integer; + + var rsaPrivateKey = RSAPrivateKey( + modulus.integer!, privateExponent.integer!, p.integer, q.integer); + + return rsaPrivateKey; + } + + /// + /// Helper function for decoding the base64 in [pem]. + /// + /// Throws an ArgumentError if the given [pem] is not sourounded by begin marker -----BEGIN and + /// endmarker -----END or the [pem] consists of less than two lines. + /// + /// The PEM header check can be skipped by setting the optional paramter [checkHeader] to false. + /// + static Uint8List getBytesFromPEMString(String pem, + {bool checkHeader = true}) { + var lines = LineSplitter.split(pem) + .map((line) => line.trim()) + .where((line) => line.isNotEmpty) + .toList(); + var base64; + if (checkHeader) { + if (lines.length < 2 || + !lines.first.startsWith('-----BEGIN') || + !lines.last.startsWith('-----END')) { + throw ArgumentError('The given string does not have the correct ' + 'begin/end markers expected in a PEM file.'); + } + base64 = lines.sublist(1, lines.length - 1).join(''); + } else { + base64 = lines.join(''); + } + + return Uint8List.fromList(base64Decode(base64)); + } + + /// + /// Decode a [RSAPublicKey] from the given [pem] String. + /// + static RSAPublicKey rsaPublicKeyFromPem(String pem) { + var bytes = CryptoUtils.getBytesFromPEMString(pem); + return rsaPublicKeyFromDERBytes(bytes); + } + + /// + /// Decode the given [bytes] into an [RSAPublicKey]. + /// + static RSAPublicKey rsaPublicKeyFromDERBytes(Uint8List bytes) { + var asn1Parser = ASN1Parser(bytes); + var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; + var publicKeySeq; + if (topLevelSeq.elements![1].runtimeType == ASN1BitString) { + var publicKeyBitString = topLevelSeq.elements![1] as ASN1BitString; + + var publicKeyAsn = + ASN1Parser(publicKeyBitString.stringValues as Uint8List?); + publicKeySeq = publicKeyAsn.nextObject() as ASN1Sequence; + } else { + publicKeySeq = topLevelSeq; + } + var modulus = publicKeySeq.elements![0] as ASN1Integer; + var exponent = publicKeySeq.elements![1] as ASN1Integer; + + var rsaPublicKey = RSAPublicKey(modulus.integer!, exponent.integer!); + + return rsaPublicKey; + } + + /// + /// Decode a [RSAPublicKey] from the given [pem] string formated in the pkcs1 standard. + /// + static RSAPublicKey rsaPublicKeyFromPemPkcs1(String pem) { + var bytes = CryptoUtils.getBytesFromPEMString(pem); + return rsaPublicKeyFromDERBytesPkcs1(bytes); + } + + /// + /// Decode the given [bytes] into an [RSAPublicKey]. + /// + /// The [bytes] need to follow the the pkcs1 standard + /// + static RSAPublicKey rsaPublicKeyFromDERBytesPkcs1(Uint8List bytes) { + var publicKeyAsn = ASN1Parser(bytes); + var publicKeySeq = publicKeyAsn.nextObject() as ASN1Sequence; + var modulus = publicKeySeq.elements![0] as ASN1Integer; + var exponent = publicKeySeq.elements![1] as ASN1Integer; + + var rsaPublicKey = RSAPublicKey(modulus.integer!, exponent.integer!); + return rsaPublicKey; + } + + /// + /// Enode the given elliptic curve [publicKey] to PEM format. + /// + /// This is descripted in + /// + /// ```ASN1 + /// ECPrivateKey ::= SEQUENCE { + /// version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + /// privateKey OCTET STRING + /// parameters [0] ECParameters {{ NamedCurve }} OPTIONAL + /// publicKey [1] BIT STRING OPTIONAL + /// } + /// + /// ``` + /// + /// As descripted in the mentioned RFC, all optional values will always be set. + /// + static String encodeEcPrivateKeyToPem(ECPrivateKey ecPrivateKey) { + var outer = ASN1Sequence(); + + var version = ASN1Integer(BigInt.from(1)); + var privateKeyAsBytes = thing.encodeBigInt(ecPrivateKey.d); + var privateKey = ASN1OctetString(octets: privateKeyAsBytes); + var choice = ASN1Sequence(tag: 0xA0); + + choice.add( + ASN1ObjectIdentifier.fromName(ecPrivateKey.parameters!.domainName)); + + var publicKey = ASN1Sequence(tag: 0xA1); + + var subjectPublicKey = ASN1BitString( + stringValues: ecPrivateKey.parameters!.G.getEncoded(false)); + publicKey.add(subjectPublicKey); + + outer.add(version); + outer.add(privateKey); + outer.add(choice); + outer.add(publicKey); + var dataBase64 = base64.encode(outer.encode()); + var chunks = StringUtils.chunk(dataBase64, 64); + + return '$BEGIN_EC_PRIVATE_KEY\n${chunks.join('\n')}\n$END_EC_PRIVATE_KEY'; + } + + /// + /// Enode the given elliptic curve [publicKey] to PEM format. + /// + /// This is descripted in + /// + /// ```ASN1 + /// SubjectPublicKeyInfo ::= SEQUENCE { + /// algorithm AlgorithmIdentifier, + /// subjectPublicKey BIT STRING + /// } + /// ``` + /// + static String encodeEcPublicKeyToPem(ECPublicKey publicKey) { + var outer = ASN1Sequence(); + var algorithm = ASN1Sequence(); + algorithm.add(ASN1ObjectIdentifier.fromName('ecPublicKey')); + algorithm.add(ASN1ObjectIdentifier.fromName('prime256v1')); + var encodedBytes = publicKey.Q!.getEncoded(false); + + var subjectPublicKey = ASN1BitString(stringValues: encodedBytes); + + outer.add(algorithm); + outer.add(subjectPublicKey); + var dataBase64 = base64.encode(outer.encode()); + var chunks = StringUtils.chunk(dataBase64, 64); + + return '$BEGIN_EC_PUBLIC_KEY\n${chunks.join('\n')}\n$END_EC_PUBLIC_KEY'; + } + + /// + /// Decode a [ECPublicKey] from the given [pem] String. + /// + /// Throws an ArgumentError if the given string [pem] is null or empty. + /// + static ECPublicKey ecPublicKeyFromPem(String pem) { + if (pem.isEmpty) { + throw ArgumentError('Argument must not be null.'); + } + var bytes = CryptoUtils.getBytesFromPEMString(pem); + return ecPublicKeyFromDerBytes(bytes); + } + + /// + /// Decode a [ECPrivateKey] from the given [pem] String. + /// + /// Throws an ArgumentError if the given string [pem] is null or empty. + /// + static ECPrivateKey ecPrivateKeyFromPem(String pem) { + if (pem.isEmpty) { + throw ArgumentError('Argument must not be null.'); + } + var bytes = CryptoUtils.getBytesFromPEMString(pem); + return ecPrivateKeyFromDerBytes( + bytes, + pkcs8: pem.startsWith(BEGIN_PRIVATE_KEY), + ); + } + + /// + /// Decode the given [bytes] into an [ECPrivateKey]. + /// + /// [pkcs8] defines the ASN1 format of the given [bytes]. The default is false, so SEC1 is assumed. + /// + /// Supports SEC1 () and PKCS8 () + /// + static ECPrivateKey ecPrivateKeyFromDerBytes(Uint8List bytes, + {bool pkcs8 = false}) { + var asn1Parser = ASN1Parser(bytes); + var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; + var curveName; + var x; + if (pkcs8) { + // Parse the PKCS8 format + var innerSeq = topLevelSeq.elements!.elementAt(1) as ASN1Sequence; + var b2 = innerSeq.elements!.elementAt(1) as ASN1ObjectIdentifier; + var b2Data = b2.objectIdentifierAsString; + var b2Curvedata = ObjectIdentifiers.getIdentifierByIdentifier(b2Data); + if (b2Curvedata != null) { + curveName = b2Curvedata['readableName']; + } + + var octetString = topLevelSeq.elements!.elementAt(2) as ASN1OctetString; + asn1Parser = ASN1Parser(octetString.valueBytes); + var octetStringSeq = asn1Parser.nextObject() as ASN1Sequence; + var octetStringKeyData = + octetStringSeq.elements!.elementAt(1) as ASN1OctetString; + + x = octetStringKeyData.valueBytes!; + } else { + // Parse the SEC1 format + var privateKeyAsOctetString = + topLevelSeq.elements!.elementAt(1) as ASN1OctetString; + var choice = topLevelSeq.elements!.elementAt(2); + var s = ASN1Sequence(); + var parser = ASN1Parser(choice.valueBytes); + while (parser.hasNext()) { + s.add(parser.nextObject()); + } + var curveNameOi = s.elements!.elementAt(0) as ASN1ObjectIdentifier; + var data = ObjectIdentifiers.getIdentifierByIdentifier( + curveNameOi.objectIdentifierAsString); + if (data != null) { + curveName = data['readableName']; + } + + x = privateKeyAsOctetString.valueBytes!; + } + + return ECPrivateKey(thing.decodeBigInt(x), ECDomainParameters(curveName)); + } + + /// + /// Decode the given [bytes] into an [ECPublicKey]. + /// + static ECPublicKey ecPublicKeyFromDerBytes(Uint8List bytes) { + if (bytes.elementAt(0) == 0) { + bytes = bytes.sublist(1); + } + var asn1Parser = ASN1Parser(bytes); + var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; + + var algorithmIdentifierSequence = topLevelSeq.elements![0] as ASN1Sequence; + var curveNameOi = algorithmIdentifierSequence.elements!.elementAt(1) + as ASN1ObjectIdentifier; + var curveName; + var data = ObjectIdentifiers.getIdentifierByIdentifier( + curveNameOi.objectIdentifierAsString); + if (data != null) { + curveName = data['readableName']; + } + + var subjectPublicKey = topLevelSeq.elements![1] as ASN1BitString; + var compressed = false; + var pubBytes = subjectPublicKey.valueBytes!; + if (pubBytes.elementAt(0) == 0) { + pubBytes = pubBytes.sublist(1); + } + + // Looks good so far! + var firstByte = pubBytes.elementAt(0); + if (firstByte != 4) { + compressed = true; + } + var x = pubBytes.sublist(1, (pubBytes.length / 2).round()); + var y = pubBytes.sublist(1 + x.length, pubBytes.length); + var params = ECDomainParameters(curveName); + var bigX = thing.decodeBigIntWithSign(1, x); + var bigY = thing.decodeBigIntWithSign(1, y); + var pubKey = ECPublicKey( + ecc_fp.ECPoint( + params.curve as ecc_fp.ECCurve, + params.curve.fromBigInteger(bigX) as ecc_fp.ECFieldElement?, + params.curve.fromBigInteger(bigY) as ecc_fp.ECFieldElement?, + compressed), + params); + return pubKey; + } + + /// + /// Encrypt the given [message] using the given RSA [publicKey]. + /// + static Uint8List rsaEncrypt(Uint8List message, RSAPublicKey publicKey) { + var cipher = OAEPEncoding.withSHA256(RSAEngine()) + ..init(true, PublicKeyParameter(publicKey)); + + return _processInBlocks(cipher, message); + } + + /// + /// Decrypt the given [cipherMessage] using the given RSA [privateKey]. + /// + static Uint8List rsaDecrypt(Uint8List cipherMessage, RSAPrivateKey privateKey) { + var cipher = OAEPEncoding.withSHA256(RSAEngine()) + ..init(false, PrivateKeyParameter(privateKey)); + + return _processInBlocks(cipher, cipherMessage); + } + + static 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); + } + + /// + /// Signing the given [dataToSign] with the given [privateKey]. + /// + /// The default [algorithm] used is **SHA-256/RSA**. All supported algorihms are : + /// + /// * MD2/RSA + /// * MD4/RSA + /// * MD5/RSA + /// * RIPEMD-128/RSA + /// * RIPEMD-160/RSA + /// * RIPEMD-256/RSA + /// * SHA-1/RSA + /// * SHA-224/RSA + /// * SHA-256/RSA + /// * SHA-384/RSA + /// * SHA-512/RSA + /// + static Uint8List rsaSign(RSAPrivateKey privateKey, Uint8List dataToSign, + {String algorithmName = 'SHA-256/RSA'}) { + var signer = Signer(algorithmName) as RSASigner; + + signer.init(true, PrivateKeyParameter(privateKey)); + + var sig = signer.generateSignature(dataToSign); + + return sig.bytes; + } + + /// + /// Verifying the given [signedData] with the given [publicKey] and the given [signature]. + /// Will return **true** if the given [signature] matches the [signedData]. + /// + /// The default [algorithm] used is **SHA-256/RSA**. All supported algorihms are : + /// + /// * MD2/RSA + /// * MD4/RSA + /// * MD5/RSA + /// * RIPEMD-128/RSA + /// * RIPEMD-160/RSA + /// * RIPEMD-256/RSA + /// * SHA-1/RSA + /// * SHA-224/RSA + /// * SHA-256/RSA + /// * SHA-384/RSA + /// * SHA-512/RSA + /// + static bool rsaVerify( + RSAPublicKey publicKey, Uint8List signedData, Uint8List signature, + {String algorithm = 'SHA-256/RSA'}) { + final sig = RSASignature(signature); + + final verifier = Signer(algorithm); + + verifier.init(false, PublicKeyParameter(publicKey)); + + try { + return verifier.verifySignature(signedData, sig); + } on ArgumentError { + return false; + } + } + + /// + /// Signing the given [dataToSign] with the given [privateKey]. + /// + /// The default [algorithm] used is **SHA-1/ECDSA**. All supported algorihms are : + /// + /// * SHA-1/ECDSA + /// * SHA-224/ECDSA + /// * SHA-256/ECDSA + /// * SHA-384/ECDSA + /// * SHA-512/ECDSA + /// * SHA-1/DET-ECDSA + /// * SHA-224/DET-ECDSA + /// * SHA-256/DET-ECDSA + /// * SHA-384/DET-ECDSA + /// * SHA-512/DET-ECDSA + /// + static ECSignature ecSign(ECPrivateKey privateKey, Uint8List dataToSign, + {String algorithmName = 'SHA-1/ECDSA'}) { + var signer = Signer(algorithmName) as ECDSASigner; + + var params = ParametersWithRandom( + PrivateKeyParameter(privateKey), _getSecureRandom()); + signer.init(true, params); + + var sig = signer.generateSignature(dataToSign) as ECSignature; + + return sig; + } + + /// + /// Verifying the given [signedData] with the given [publicKey] and the given [signature]. + /// Will return **true** if the given [signature] matches the [signedData]. + /// + /// The default [algorithm] used is **SHA-1/ECDSA**. All supported algorihms are : + /// + /// * SHA-1/ECDSA + /// * SHA-224/ECDSA + /// * SHA-256/ECDSA + /// * SHA-384/ECDSA + /// * SHA-512/ECDSA + /// * SHA-1/DET-ECDSA + /// * SHA-224/DET-ECDSA + /// * SHA-256/DET-ECDSA + /// * SHA-384/DET-ECDSA + /// * SHA-512/DET-ECDSA + /// + static bool ecVerify( + ECPublicKey publicKey, Uint8List signedData, ECSignature signature, + {String algorithm = 'SHA-1/ECDSA'}) { + final verifier = Signer(algorithm) as ECDSASigner; + + verifier.init(false, PublicKeyParameter(publicKey)); + + try { + return verifier.verifySignature(signedData, signature); + } on ArgumentError { + return false; + } + } + + /// + /// Returns the modulus of the given [pem] that represents an RSA private key. + /// + /// This equals the following openssl command: + /// ``` + /// openssl rsa -noout -modulus -in FILE.key + /// ``` + /// + static BigInt getModulusFromRSAPrivateKeyPem(String pem) { + RSAPrivateKey privateKey; + switch (_getPrivateKeyType(pem)) { + case 'RSA': + privateKey = rsaPrivateKeyFromPem(pem); + return privateKey.modulus!; + case 'RSA_PKCS1': + privateKey = rsaPrivateKeyFromPemPkcs1(pem); + return privateKey.modulus!; + case 'ECC': + throw ArgumentError('ECC private key not supported.'); + default: + privateKey = rsaPrivateKeyFromPem(pem); + return privateKey.modulus!; + } + } + + /// + /// Returns the private key type of the given [pem] + /// + static String _getPrivateKeyType(String pem) { + if (pem.startsWith(BEGIN_RSA_PRIVATE_KEY)) { + return 'RSA_PKCS1'; + } else if (pem.startsWith(BEGIN_PRIVATE_KEY)) { + return 'RSA'; + } else if (pem.startsWith(BEGIN_EC_PRIVATE_KEY)) { + return 'ECC'; + } + return 'RSA'; + } +} diff --git a/mobile/lib/utils/encryption/rsa_key_helper.dart b/mobile/lib/utils/encryption/rsa_key_helper.dart index 2d3510e..72e854b 100644 --- a/mobile/lib/utils/encryption/rsa_key_helper.dart +++ b/mobile/lib/utils/encryption/rsa_key_helper.dart @@ -1,7 +1,6 @@ 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"; @@ -19,16 +18,18 @@ print(String.fromCharCodes(plainText)); List 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", + '-----BEGIN PUBLIC KEY-----', + '-----BEGIN PUBLIC KEY-----', + '\n-----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-----", + '-----END PUBLIC KEY-----', + '-----END PRIVATE KEY-----', + '-----END PGP PUBLIC KEY BLOCK-----', + '-----END PGP PRIVATE KEY BLOCK-----', ]; bool isOpenPgp = pem.contains('BEGIN PGP'); @@ -183,10 +184,12 @@ class RsaKeyHelper { // Encode RSA private key to pem format static encodePrivateKeyToPem(RSAPrivateKey privateKey) { - var version = ASN1Integer(BigInt.from(0)); + var version = ASN1Integer(BigInt.zero); var algorithmSeq = ASN1Sequence(); - var algorithmAsn1Obj = ASN1Object.fromBytes(Uint8List.fromList([0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1])); + 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); @@ -197,9 +200,9 @@ class RsaKeyHelper { 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 dP = privateKey.privateExponent! % (privateKey.p! - BigInt.one); var exp1 = ASN1Integer(dP); - var dQ = privateKey.privateExponent! % (privateKey.q! - BigInt.from(1)); + var dQ = privateKey.privateExponent! % (privateKey.q! - BigInt.one); var exp2 = ASN1Integer(dQ); var iQ = privateKey.q?.modInverse(privateKey.p!); var co = ASN1Integer(iQ!); diff --git a/mobile/lib/utils/encryption/string_utils.dart b/mobile/lib/utils/encryption/string_utils.dart new file mode 100644 index 0000000..ca40eee --- /dev/null +++ b/mobile/lib/utils/encryption/string_utils.dart @@ -0,0 +1,463 @@ +import 'dart:convert'; +import 'dart:math'; + +/// +/// Helper class for String operations +/// +class StringUtils { + static AsciiCodec asciiCodec = AsciiCodec(); + + /// + /// Returns the given string or the default string if the given string is null + /// + static String defaultString(String? str, {String defaultStr = ''}) { + return str ?? defaultStr; + } + + /// + /// Checks if the given String [s] is null or empty + /// + static bool isNullOrEmpty(String? s) => + (s == null || s.isEmpty) ? true : false; + + /// + /// Checks if the given String [s] is not null or empty + /// + static bool isNotNullOrEmpty(String? s) => !isNullOrEmpty(s); + + /// + /// Transfers the given String [s] from camcelCase to upperCaseUnderscore + /// Example : helloWorld => HELLO_WORLD + /// + static String camelCaseToUpperUnderscore(String s) { + var sb = StringBuffer(); + var first = true; + s.runes.forEach((int rune) { + var char = String.fromCharCode(rune); + if (isUpperCase(char) && !first) { + sb.write('_'); + sb.write(char.toUpperCase()); + } else { + first = false; + sb.write(char.toUpperCase()); + } + }); + return sb.toString(); + } + + /// + /// Transfers the given String [s] from camcelCase to lowerCaseUnderscore + /// Example : helloWorld => hello_world + /// + static String camelCaseToLowerUnderscore(String s) { + var sb = StringBuffer(); + var first = true; + s.runes.forEach((int rune) { + var char = String.fromCharCode(rune); + if (isUpperCase(char) && !first) { + if (char != '_') { + sb.write('_'); + } + sb.write(char.toLowerCase()); + } else { + first = false; + sb.write(char.toLowerCase()); + } + }); + return sb.toString(); + } + + /// + /// Checks if the given string [s] is lower case + /// + static bool isLowerCase(String s) { + return s == s.toLowerCase(); + } + + /// + /// Checks if the given string [s] is upper case + /// + static bool isUpperCase(String s) { + return s == s.toUpperCase(); + } + + /// + /// Checks if the given string [s] contains only ascii chars + /// + static bool isAscii(String s) { + try { + asciiCodec.decode(s.codeUnits); + } catch (e) { + return false; + } + return true; + } + + /// + /// Capitalize the given string [s]. If [allWords] is set to true, it will capitalize all words within the given string [s]. + /// + /// The string [s] is there fore splitted by " " (space). + /// + /// Example : + /// + /// * [s] = "world" => World + /// * [s] = "WORLD" => World + /// * [s] = "the quick lazy fox" => The quick lazy fox + /// * [s] = "the quick lazy fox" and [allWords] = true => The Quick Lazy Fox + /// + static String capitalize(String s, {bool allWords = false}) { + if (s.isEmpty) { + return ''; + } + s = s.trim(); + if (allWords) { + var words = s.split(' '); + var capitalized = []; + for (var w in words) { + capitalized.add(capitalize(w)); + } + return capitalized.join(' '); + } else { + return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); + } + } + + /// + /// Reverse the given string [s] + /// Example : hello => olleh + /// + static String reverse(String s) { + return String.fromCharCodes(s.runes.toList().reversed); + } + + /// + /// Counts how offen the given [char] apears in the given string [s]. + /// The value [caseSensitive] controlls whether it should only look for the given [char] + /// or also the equivalent lower/upper case version. + /// Example: Hello and char l => 2 + /// + static int countChars(String s, String char, {bool caseSensitive = true}) { + var count = 0; + s.codeUnits.toList().forEach((i) { + if (caseSensitive) { + if (i == char.runes.first) { + count++; + } + } else { + if (i == char.toLowerCase().runes.first || + i == char.toUpperCase().runes.first) { + count++; + } + } + }); + return count; + } + + /// + /// Checks if the given string [s] is a digit. + /// + /// Will return false if the given string [s] is empty. + /// + static bool isDigit(String s) { + if (s.isEmpty) { + return false; + } + if (s.length > 1) { + for (var r in s.runes) { + if (r ^ 0x30 > 9) { + return false; + } + } + return true; + } else { + return s.runes.first ^ 0x30 <= 9; + } + } + + /// + /// Compares the given strings [a] and [b]. + /// + static bool equalsIgnoreCase(String a, String b) => + a.toLowerCase() == b.toLowerCase(); + + /// + /// Checks if the given [list] contains the string [s] + /// + static bool inList(String s, List list, {bool ignoreCase = false}) { + for (var l in list) { + if (ignoreCase) { + if (equalsIgnoreCase(s, l)) { + return true; + } + } else { + if (s == l) { + return true; + } + } + } + return false; + } + + /// + /// Checks if the given string [s] is a palindrome + /// Example : + /// aha => true + /// hello => false + /// + static bool isPalindrome(String s) { + for (var i = 0; i < s.length / 2; i++) { + if (s[i] != s[s.length - 1 - i]) return false; + } + return true; + } + + /// + /// Replaces chars of the given String [s] with [replace]. + /// + /// The default value of [replace] is *. + /// [begin] determines the start of the 'replacing'. If [begin] is null, it starts from index 0. + /// [end] defines the end of the 'replacing'. If [end] is null, it ends at [s] length divided by 2. + /// If [s] is empty or consists of only 1 char, the method returns null. + /// + /// Example : + /// 1234567890 => *****67890 + /// 1234567890 with begin 2 and end 6 => 12****7890 + /// 1234567890 with begin 1 => 1****67890 + /// + static String? hidePartial(String s, + {int begin = 0, int? end, String replace = '*'}) { + var buffer = StringBuffer(); + if (s.length <= 1) { + return null; + } + if (end == null) { + end = (s.length / 2).round(); + } else { + if (end > s.length) { + end = s.length; + } + } + for (var i = 0; i < s.length; i++) { + if (i >= end) { + buffer.write(String.fromCharCode(s.runes.elementAt(i))); + continue; + } + if (i >= begin) { + buffer.write(replace); + continue; + } + buffer.write(String.fromCharCode(s.runes.elementAt(i))); + } + return buffer.toString(); + } + + /// + /// Add a [char] at a [position] with the given String [s]. + /// + /// The boolean [repeat] defines whether to add the [char] at every [position]. + /// If [position] is greater than the length of [s], it will return [s]. + /// If [repeat] is true and [position] is 0, it will return [s]. + /// + /// Example : + /// 1234567890 , '-', 3 => 123-4567890 + /// 1234567890 , '-', 3, true => 123-456-789-0 + /// + static String addCharAtPosition(String s, String char, int position, + {bool repeat = false}) { + if (!repeat) { + if (s.length < position) { + return s; + } + var before = s.substring(0, position); + var after = s.substring(position, s.length); + return before + char + after; + } else { + if (position == 0) { + return s; + } + var buffer = StringBuffer(); + for (var i = 0; i < s.length; i++) { + if (i != 0 && i % position == 0) { + buffer.write(char); + } + buffer.write(String.fromCharCode(s.runes.elementAt(i))); + } + return buffer.toString(); + } + } + + /// + /// Splits the given String [s] in chunks with the given [chunkSize]. + /// + static List chunk(String s, int chunkSize) { + var chunked = []; + for (var i = 0; i < s.length; i += chunkSize) { + var end = (i + chunkSize < s.length) ? i + chunkSize : s.length; + chunked.add(s.substring(i, end)); + } + return chunked; + } + + /// + /// Picks only required string[value] starting [from] and ending at [to] + /// + /// Example : + /// pickOnly('123456789',from:3,to:7); + /// returns '34567' + /// + static String pickOnly(value, {int from = 1, int to = -1}) { + try { + return value.substring( + from == 0 ? 0 : from - 1, to == -1 ? value.length : to); + } catch (e) { + return value; + } + } + + /// + /// Removes character with [index] from a String [value] + /// + /// Example: + /// removeCharAtPosition('flutterr', 8); + /// returns 'flutter' + static String removeCharAtPosition(String value, int index) { + try { + return value.substring(0, -1 + index) + + value.substring(index, value.length); + } catch (e) { + return value; + } + } + + /// + ///Remove String[value] with [pattern] + /// + ///[repeat]:boolean => if(true) removes all occurence + /// + ///[casensitive]:boolean => if(true) a != A + /// + ///Example: removeExp('Hello This World', 'This'); returns 'Hello World' + /// + static String removeExp(String value, String pattern, + {bool repeat = true, + bool caseSensitive = true, + bool multiLine = false, + bool dotAll = false, + bool unicode = false}) { + var result = value; + if (repeat) { + result = value + .replaceAll( + RegExp(pattern, + caseSensitive: caseSensitive, + multiLine: multiLine, + dotAll: dotAll, + unicode: unicode), + '') + .replaceAll(RegExp(' +'), ' ') + .trim(); + } else { + result = value + .replaceFirst( + RegExp(pattern, + caseSensitive: caseSensitive, + multiLine: multiLine, + dotAll: dotAll, + unicode: unicode), + '') + .replaceAll(RegExp(' +'), ' ') + .trim(); + } + return result; + } + + /// + /// Takes in a String[value] and truncates it with [length] + /// [symbol] default is '...' + ///truncate('This is a Dart Utility Library', 26) + /// returns 'This is a Dart Utility Lib...' + static String truncate(String value, int length, {String symbol = '...'}) { + var result = value; + + try { + result = value.substring(0, length) + symbol; + } catch (e) { + print(e.toString()); + } + return result; + } + + ///Generates a Random string + /// + ///[length]: length of string, + /// + ///[alphabet]:(boolean) add alphabet to string[uppercase]ABCD and [lowercase]abcd, + /// + ///[numeric]:(boolean) add integers to string like 3622737 + /// + ///[special]:(boolean) add special characters like $#@&^ + /// + ///[from]:where you want to generate string from + /// + static String generateRandomString(int length, + {alphabet = true, + numeric = true, + special = true, + uppercase = true, + lowercase = true, + String from = ''}) { + var res = ''; + + do { + res += randomizer(alphabet, numeric, lowercase, uppercase, special, from); + } while (res.length < length); + + var possible = res.split(''); + possible.shuffle(); //all possible combinations shuffled + var result = []; + + for (var i = 0; i < length; i++) { + var randomNumber = Random().nextInt(length); + result.add(possible[randomNumber]); + } + + return result.join(); + } + + static String randomizer(bool alphabet, bool numeric, bool lowercase, + bool uppercase, bool special, String from) { + var a = 'ABCDEFGHIJKLMNOPQRXYZ'; + var la = 'abcdefghijklmnopqrxyz'; + var b = '0123456789'; + var c = '~^!@#\$%^&*;`(=?]:[.)_+-|\{}'; + var result = ''; + + if (alphabet) { + if (lowercase) { + result += la; + } + if (uppercase) { + result += a; + } + + if (!uppercase && !lowercase) { + result += a; + result += la; + } + } + if (numeric) { + result += b; + } + + if (special) { + result += c; + } + + if (from != '') { + //if set return it + result = from; + } + + return result; + } +} diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart new file mode 100644 index 0000000..0b306c4 --- /dev/null +++ b/mobile/lib/utils/storage/database.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'package:flutter/widgets.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; + +Future getDatabaseConnection() async { + WidgetsFlutterBinding.ensureInitialized(); + + final database = openDatabase( + // Set the path to the database. Note: Using the `join` function from the + // `path` package is best practice to ensure the path is correctly + // constructed for each platform. + join(await getDatabasesPath(), 'envelope.db'), + // When the database is first created, create a table to store dogs. + onCreate: (db, version) { + // Run the CREATE TABLE statement on the database. + return db.execute( + ''' + CREATE TABLE IF NOT EXISTS friends( + id BLOB PRIMARY KEY, + user_id BLOB, + friend_id BLOB, + friend_id_decrypted BLOB, + accepted_at TEXT + ); + ''', + ); + }, + // Set the version. This executes the onCreate function and provides a + // path to perform database upgrades and downgrades. + version: 1, + ); + + return database; +} diff --git a/mobile/lib/utils/storage/encryption_keys.dart b/mobile/lib/utils/storage/encryption_keys.dart index edcd727..edc2f5c 100644 --- a/mobile/lib/utils/storage/encryption_keys.dart +++ b/mobile/lib/utils/storage/encryption_keys.dart @@ -1,11 +1,11 @@ import 'package:shared_preferences/shared_preferences.dart'; import "package:pointycastle/export.dart"; -import '/utils/encryption/rsa_key_helper.dart'; +import '/utils/encryption/crypto_utils.dart'; const rsaPrivateKeyName = 'rsaPrivateKey'; void setPrivateKey(RSAPrivateKey key) async { - String keyPem = RsaKeyHelper.encodePrivateKeyToPem(key); + String keyPem = CryptoUtils.encodeRSAPrivateKeyToPem(key); final prefs = await SharedPreferences.getInstance(); prefs.setString(rsaPrivateKeyName, keyPem); @@ -23,5 +23,5 @@ Future getPrivateKey() async { throw Exception('No RSA private key set'); } - return RsaKeyHelper.parsePrivateKeyFromPem(keyPem); + return CryptoUtils.rsaPrivateKeyFromPem(keyPem); } diff --git a/mobile/lib/utils/storage/friends.dart b/mobile/lib/utils/storage/friends.dart new file mode 100644 index 0000000..3e564ae --- /dev/null +++ b/mobile/lib/utils/storage/friends.dart @@ -0,0 +1,38 @@ +// import 'package:sqflite/sqflite.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import "package:pointycastle/export.dart"; +import '/models/friends.dart'; +import '/utils/storage/encryption_keys.dart'; +import '/utils/storage/session_cookie.dart'; + +void getFriends() async { + RSAPrivateKey privKey = await getPrivateKey(); + + final resp = await http.get( + Uri.parse('http://192.168.1.5:8080/api/v1/auth/friends'), + headers: { + 'cookie': await getSessionCookie(), + } + ); + + if (resp.statusCode != 200) { + throw Exception(resp.body); + } + + List friends = []; + + List friendsJson = jsonDecode(resp.body); + + for (var i = 0; i < friendsJson.length; i++) { + friends.add( + Friend.fromJson( + friendsJson[i] as Map, + privKey, + ) + ); + } + + print(friends); +} + diff --git a/mobile/lib/utils/storage/session_cookie.dart b/mobile/lib/utils/storage/session_cookie.dart new file mode 100644 index 0000000..1070de6 --- /dev/null +++ b/mobile/lib/utils/storage/session_cookie.dart @@ -0,0 +1,23 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +const sessionCookieName = 'sessionCookie'; + +void setSessionCookie(String cookie) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setString(sessionCookieName, cookie); +} + +void unsetSessionCookie() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(sessionCookieName); +} + +Future getSessionCookie() async { + final prefs = await SharedPreferences.getInstance(); + String? sessionCookie = prefs.getString(sessionCookieName); + if (sessionCookie == null) { + throw Exception('No session cookie set'); + } + + return sessionCookie; +} diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart index 98a3ce9..806729b 100644 --- a/mobile/lib/views/authentication/login.dart +++ b/mobile/lib/views/authentication/login.dart @@ -2,9 +2,10 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; -import '/utils/encryption/rsa_key_helper.dart'; +import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/storage/encryption_keys.dart'; +import '/utils/storage/session_cookie.dart'; class LoginResponse { final String status; @@ -40,15 +41,24 @@ Future login(context, String username, String password) async { 'password': password, }), ); - - LoginResponse response = LoginResponse.fromJson(jsonDecode(resp.body)); if (resp.statusCode != 200) { - throw Exception(response.message); + throw Exception(resp.body); + } + + String? rawCookie = resp.headers['set-cookie']; + if (rawCookie != null) { + int index = rawCookie.indexOf(';'); + setSessionCookie((index == -1) ? rawCookie : rawCookie.substring(0, index)); } + LoginResponse response = LoginResponse.fromJson(jsonDecode(resp.body)); + var rsaPrivPem = AesHelper.aesDecrypt(password, base64.decode(response.asymmetricPrivateKey)); - var rsaPriv = RsaKeyHelper.parsePrivateKeyFromPem(rsaPrivPem); + + debugPrint(rsaPrivPem); + + var rsaPriv = CryptoUtils.rsaPrivateKeyFromPem(rsaPrivPem); setPrivateKey(rsaPriv); final preferences = await SharedPreferences.getInstance(); @@ -171,14 +181,12 @@ class _LoginWidgetState extends State { usernameController.text, passwordController.text, ).then((value) { - /* - Navigator.of(context).popUntil((route) { - print(route.isFirst); - return route.isFirst; - }); - */ - - Navigator.pushNamedAndRemoveUntil(context, '/home', ModalRoute.withName('/home')); + Navigator. + pushNamedAndRemoveUntil( + context, + '/home', + ModalRoute.withName('/home'), + ); }).catchError((error) { print(error); // TODO: Show error on interface }); diff --git a/mobile/lib/views/authentication/signup.dart b/mobile/lib/views/authentication/signup.dart index 43af372..d179a21 100644 --- a/mobile/lib/views/authentication/signup.dart +++ b/mobile/lib/views/authentication/signup.dart @@ -4,11 +4,9 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; -import '/views/main/conversation_list.dart'; - -import '/utils/encryption/rsa_key_helper.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/storage/encryption_keys.dart'; +import '/utils/encryption/crypto_utils.dart'; class SignupResponse { final String status; @@ -28,16 +26,14 @@ class SignupResponse { } Future signUp(context, String username, String password, String confirmPassword) async { - var rsaKeyHelper = RsaKeyHelper(); - var keyPair = rsaKeyHelper.generateRSAkeyPair(); - - setPrivateKey(keyPair.privateKey); + var keyPair = CryptoUtils.generateRSAKeyPair(); - var rsaPubPem = RsaKeyHelper.encodePublicKeyToPem(keyPair.publicKey); - var rsaPrivPem = RsaKeyHelper.encodePrivateKeyToPem(keyPair.privateKey); + var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey); + var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey); var encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits)); + // TODO: Check for timeout here final resp = await http.post( Uri.parse('http://192.168.1.5:8080/api/v1/signup'), headers: { @@ -52,14 +48,15 @@ Future signUp(context, String username, String password, String }), ); - SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body)); + SignupResponse response = SignupResponse.fromJson(jsonDecode(resp.body)); if (resp.statusCode != 201) { throw Exception(response.message); } - final preferences = await SharedPreferences.getInstance(); - preferences.setBool('islogin', true); + debugPrint(rsaPubPem); + debugPrint(rsaPrivPem); + debugPrint(resp.body); return response; } diff --git a/mobile/lib/views/main/friend_list.dart b/mobile/lib/views/main/friend_list.dart index 8d2f4af..9299cb0 100644 --- a/mobile/lib/views/main/friend_list.dart +++ b/mobile/lib/views/main/friend_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '/models/friends.dart'; import '/views/main/friend_list_item.dart'; +import '/utils/storage/friends.dart'; class FriendList extends StatefulWidget { const FriendList({Key? key}) : super(key: key); @@ -10,13 +11,14 @@ class FriendList extends StatefulWidget { } class _FriendListState extends State { - List friends = [ - Friend(id: 'abc', username: 'Test1'), - Friend(id: 'abc', username: 'Test2'), - Friend(id: 'abc', username: 'Test3'), - Friend(id: 'abc', username: 'Test4'), - Friend(id: 'abc', username: 'Test5'), - ]; + List friends = []; + + @override + void initState() { + getFriends(); + super.initState(); + } + Widget list() { @@ -34,7 +36,7 @@ class _FriendListState extends State { itemBuilder: (context, i) { return FriendListItem( id: friends[i].id, - username: friends[i].username, + username: 'test', ); }, ); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 8bafdc8..e09490d 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -164,7 +164,7 @@ packages: source: hosted version: "1.7.0" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" @@ -287,6 +287,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + sqflite: + dependency: "direct main" + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1+1" stack_trace: dependency: transitive description: @@ -308,6 +322,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+2" term_glyph: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 1cba398..86d417a 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -11,13 +11,14 @@ environment: dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.2 font_awesome_flutter: ^10.1.0 pointycastle: ^3.5.2 asn1lib: ^1.1.0 http: ^0.13.4 shared_preferences: ^2.0.15 + sqflite: ^2.0.2 + path: 1.8.1 dev_dependencies: flutter_test: