From 7fdad4d0db9c3e7052193475d895e1fe31292698 Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Fri, 16 Aug 2019 19:35:38 +0300 Subject: [PATCH 01/14] ability to encrypt \ decrypt files (iOS) --- ios/Constants.h | 17 +++ ios/Constants.m | 12 ++ ios/FSUtils.h | 17 +++ ios/FSUtils.m | 81 ++++++++++++++ ios/RNVirgilCrypto.h | 1 + ios/RNVirgilCrypto.m | 109 +++++++++++++++++++ ios/RNVirgilCrypto.xcodeproj/project.pbxproj | 12 ++ 7 files changed, 249 insertions(+) create mode 100644 ios/Constants.h create mode 100644 ios/Constants.m create mode 100644 ios/FSUtils.h create mode 100644 ios/FSUtils.m diff --git a/ios/Constants.h b/ios/Constants.h new file mode 100644 index 0000000..2a48cb3 --- /dev/null +++ b/ios/Constants.h @@ -0,0 +1,17 @@ +// +// Constants.h +// RNVirgilCrypto +// +// Created by vadim on 8/16/19. +// Copyright © 2019 Virgil Security, Inc. All rights reserved. +// + +#ifndef Constants_h +#define Constants_h + +#import + +extern NSString *const RNVC_ASSET_PREFIX; +extern NSString *const RNVC_ERROR_DOMAIN; + +#endif /* Constants_h */ diff --git a/ios/Constants.m b/ios/Constants.m new file mode 100644 index 0000000..4ec32a1 --- /dev/null +++ b/ios/Constants.m @@ -0,0 +1,12 @@ +// +// Constants.m +// RNVirgilCrypto +// +// Created by vadim on 8/16/19. +// Copyright © 2019 Virgil Security, Inc. All rights reserved. +// + +#import "Constants.h" + +NSString *const RNVC_ASSET_PREFIX = @"bundle-assets://"; +NSString *const RNVC_ERROR_DOMAIN = @"com.virgilsecurity.rn.crypto"; diff --git a/ios/FSUtils.h b/ios/FSUtils.h new file mode 100644 index 0000000..d92545f --- /dev/null +++ b/ios/FSUtils.h @@ -0,0 +1,17 @@ +// +// FSUtils.h +// RNVirgilCrypto +// +// Created by vadim on 8/16/19. +// Copyright © 2019 Virgil Security, Inc. All rights reserved. +// + +#import + +@interface FSUtils : NSObject + ++ (NSString*) getPathFromUri:(NSString* _Nonnull)uri; ++ (NSString*) getTempFilePath:(NSString* _Nullable)ext; ++ (BOOL) prepareFileForWriting:(NSString* _Nonnull)path error:(NSError** _Nullable) outError; + +@end diff --git a/ios/FSUtils.m b/ios/FSUtils.m new file mode 100644 index 0000000..1aa9072 --- /dev/null +++ b/ios/FSUtils.m @@ -0,0 +1,81 @@ +// +// FSUtils.m +// RNVirgilCrypto +// +// Created by vadim on 8/16/19. +// Copyright © 2019 Virgil Security, Inc. All rights reserved. +// Some code originally from https://github.com/joltup/rn-fetch-blob/blob/master/ios/RNFetchBlobFS.m +// + +#import +#import "FSUtils.h" +#import "Constants.h" + +@implementation FSUtils + ++ (NSString *) getPathFromUri:(NSString *)uri +{ + if([uri hasPrefix:RNVC_ASSET_PREFIX]) + { + uri = [uri stringByReplacingOccurrencesOfString:RNVC_ASSET_PREFIX withString:@""]; + uri = [[NSBundle mainBundle] pathForResource: [uri stringByDeletingPathExtension] + ofType: [uri pathExtension]]; + } + return uri; +} + ++ (NSString *) getTempFilePath:(nullable NSString *)ext +{ + NSString* tempFileName = [[NSUUID UUID] UUIDString]; + if (ext != nil) { + tempFileName = [tempFileName stringByAppendingString:[NSString stringWithFormat:@".%@", ext]]; + } + return [NSTemporaryDirectory() stringByAppendingPathComponent:tempFileName]; +} + ++ (BOOL) prepareFileForWriting:(NSString *)path error:(NSError* __autoreleasing *) outError +{ + NSError* err = nil; + NSFileManager* fm = [NSFileManager defaultManager]; + NSString* folder = [path stringByDeletingLastPathComponent]; + + BOOL isDirectory = NO; + BOOL exists = [fm fileExistsAtPath:path isDirectory:&isDirectory]; + + if (isDirectory) { + NSDictionary* userInfo = @{ NSLocalizedDescriptionKey: [NSString + stringWithFormat:@"Expected path to a file but '%@' is a directory", + path] + }; + *outError = [NSError errorWithDomain:RNVC_ERROR_DOMAIN code:-1 userInfo:userInfo]; + return NO; + } + + if (!exists) { + BOOL folderCreated = [fm createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:nil error:&err]; + if (!folderCreated) { + NSDictionary* userInfo = @{ NSUnderlyingErrorKey: err, + NSLocalizedDescriptionKey: [NSString + stringWithFormat:@"Failed to create parent directory of '%@'; error: %@", + path, + [err description]] + }; + *outError = [NSError errorWithDomain:RNVC_ERROR_DOMAIN code:-2 userInfo:userInfo]; + return NO; + } + + BOOL fileCreated = [fm createFileAtPath:path contents:nil attributes:nil]; + if (!fileCreated) { + NSDictionary* userInfo = @{ NSLocalizedDescriptionKey: [NSString + stringWithFormat:@"File '%@' does not exist and could not be created", + path] + }; + *outError = [NSError errorWithDomain:RNVC_ERROR_DOMAIN code:-3 userInfo:userInfo]; + return NO; + } + } + return YES; +} + +@end + diff --git a/ios/RNVirgilCrypto.h b/ios/RNVirgilCrypto.h index 9c7e726..e1ae955 100644 --- a/ios/RNVirgilCrypto.h +++ b/ios/RNVirgilCrypto.h @@ -16,6 +16,7 @@ #import "NSData+Encodings.h" #import "NSString+Encodings.h" #import "ResponseFactory.h" +#import "FSUtils.h" @interface RNVirgilCrypto : NSObject diff --git a/ios/RNVirgilCrypto.m b/ios/RNVirgilCrypto.m index 14a5be6..2133d93 100644 --- a/ios/RNVirgilCrypto.m +++ b/ios/RNVirgilCrypto.m @@ -275,4 +275,113 @@ - (NSData*) computeHashFor:(NSData*) data using:(VSMHashAlgorithm) alg return [ResponseFactory fromResult:[randomData stringUsingBase64]]; } + +RCT_EXPORT_METHOD(encryptFile:(NSString*)inputUri + toFile:(nullable NSString*)outputUri + for:(NSArray*)recipientPublicKeysBase64 + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString* inputPath = [FSUtils getPathFromUri:inputUri]; + if ([[NSFileManager defaultManager] fileExistsAtPath:inputPath] == NO) { + reject(@"no_such_file", [NSString stringWithFormat:@"File does not exist at path %@", inputPath], nil); + return; + } + + NSString* outputPath = outputUri == nil + ? [FSUtils getTempFilePath:nil] + : [FSUtils getPathFromUri:outputUri]; + + NSError* err = nil; + BOOL isOutputReady = [FSUtils prepareFileForWriting:outputPath error:&err]; + if (!isOutputReady) { + reject(@"invalid_destination", [err description], err); + } + + NSArray* publicKeys = [self decodeAndImportPublicKeys:recipientPublicKeysBase64 error:&err]; + if (nil == publicKeys) { + reject(@"invalid_public_key", @"Public keys array contains invalid public keys", err); + return; + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *encryptErr; + NSInputStream *inStream = [NSInputStream inputStreamWithFileAtPath:inputPath]; + NSOutputStream *outStream = [NSOutputStream outputStreamToFileAtPath:outputPath append:NO]; + + [inStream open]; + [outStream open]; + + BOOL isSuccessful = [self.crypto encrypt:inStream + to:outStream + for:publicKeys + error:&encryptErr]; + + [inStream close]; + [outStream close]; + + if (!isSuccessful) { + reject(@"failed_to_encrypt", @"Unexpected error encrypting stream", encryptErr); + return; + } + + resolve(outputPath); + }); +} + +RCT_EXPORT_METHOD(decryptFile:(NSString*)inputUri + toFile:(nullable NSString*)outputUri + with:(NSString*)privateKeyBase64 + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString* inputPath = [FSUtils getPathFromUri:inputUri]; + if ([[NSFileManager defaultManager] fileExistsAtPath:inputPath] == NO) { + reject(@"no_such_file", [NSString stringWithFormat:@"File does not exist at path %@", inputUri], nil); + return; + } + + NSString* outputPath = outputUri == nil + ? [FSUtils getTempFilePath:[inputPath pathExtension]] + : [FSUtils getPathFromUri:outputUri]; + + NSError* err = nil; + BOOL isOutputReady = [FSUtils prepareFileForWriting:outputPath error:&err]; + if (!isOutputReady) { + reject(@"invalid_destination", [err description], err); + } + + VSMVirgilKeyPair* keypair = [self.crypto importPrivateKeyFrom:[privateKeyBase64 dataUsingBase64] error:&err]; + if (nil == keypair) { + reject(@"invalid_private_key", @"The given value is not a valid private key", err); + return; + } + + VSMVirgilPrivateKey* privateKey = keypair.privateKey; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *decryptErr; + NSInputStream *inputStream = [NSInputStream inputStreamWithFileAtPath:inputPath]; + NSOutputStream *outputStream = [NSOutputStream outputStreamToFileAtPath:outputPath append:NO]; + + [inputStream open]; + [outputStream open]; + + BOOL isSuccessful = [self.crypto decrypt:inputStream + to:outputStream + with:privateKey + error:&decryptErr]; + + [inputStream close]; + [outputStream close]; + + if (!isSuccessful) { + NSLog(@"ERROR %@", decryptErr); + reject(@"failed_to_decrypt", @"Unexpected error decrypting stream", decryptErr); + return; + } + + resolve(outputPath); + }); +} @end diff --git a/ios/RNVirgilCrypto.xcodeproj/project.pbxproj b/ios/RNVirgilCrypto.xcodeproj/project.pbxproj index ed89444..0b4b920 100644 --- a/ios/RNVirgilCrypto.xcodeproj/project.pbxproj +++ b/ios/RNVirgilCrypto.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ DD3C903722F84B99001DD161 /* NSData+Encodings.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3C903622F84B99001DD161 /* NSData+Encodings.m */; }; DD3C903A22F84CA4001DD161 /* NSString+Encodings.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3C903922F84CA4001DD161 /* NSString+Encodings.m */; }; DD3C903C22F8533B001DD161 /* ResponseFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = DD3C903B22F8533B001DD161 /* ResponseFactory.m */; }; + DDDF207623070E3C00F7C404 /* FSUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = DDDF207523070E3C00F7C404 /* FSUtils.m */; }; + DDDF207923070FC200F7C404 /* Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = DDDF207823070FC200F7C404 /* Constants.m */; }; DDF69F6022F47D900016F2C0 /* RCTConvert+KeyPairType.m in Sources */ = {isa = PBXBuildFile; fileRef = DDF69F5F22F47D900016F2C0 /* RCTConvert+KeyPairType.m */; }; /* End PBXBuildFile section */ @@ -39,6 +41,10 @@ DD3C903922F84CA4001DD161 /* NSString+Encodings.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+Encodings.m"; sourceTree = ""; }; DD3C903B22F8533B001DD161 /* ResponseFactory.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ResponseFactory.m; sourceTree = ""; }; DD3C903D22F853B4001DD161 /* ResponseFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ResponseFactory.h; sourceTree = ""; }; + DDDF207523070E3C00F7C404 /* FSUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSUtils.m; sourceTree = ""; }; + DDDF207723070E4A00F7C404 /* FSUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSUtils.h; sourceTree = ""; }; + DDDF207823070FC200F7C404 /* Constants.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Constants.m; sourceTree = ""; }; + DDDF207A23070FD500F7C404 /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; DDF69F5E22F47D900016F2C0 /* RCTConvert+KeyPairType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+KeyPairType.h"; sourceTree = ""; }; DDF69F5F22F47D900016F2C0 /* RCTConvert+KeyPairType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+KeyPairType.m"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -65,6 +71,10 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + DDDF207A23070FD500F7C404 /* Constants.h */, + DDDF207823070FC200F7C404 /* Constants.m */, + DDDF207723070E4A00F7C404 /* FSUtils.h */, + DDDF207523070E3C00F7C404 /* FSUtils.m */, DD3C903D22F853B4001DD161 /* ResponseFactory.h */, DD3C903B22F8533B001DD161 /* ResponseFactory.m */, DD3C903822F84CA4001DD161 /* NSString+Encodings.h */, @@ -141,8 +151,10 @@ DD3C903A22F84CA4001DD161 /* NSString+Encodings.m in Sources */, DD3C903C22F8533B001DD161 /* ResponseFactory.m in Sources */, B3E7B58A1CC2AC0600A0062D /* RNVirgilCrypto.m in Sources */, + DDDF207923070FC200F7C404 /* Constants.m in Sources */, DD3C903722F84B99001DD161 /* NSData+Encodings.m in Sources */, DD3C903422F83282001DD161 /* RCTConvert+HashAlgorithm.m in Sources */, + DDDF207623070E3C00F7C404 /* FSUtils.m in Sources */, DDF69F6022F47D900016F2C0 /* RCTConvert+KeyPairType.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From eafcbc3158c4c1d738589cb5e8ec956318fd826b Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Mon, 19 Aug 2019 20:03:34 +0300 Subject: [PATCH 02/14] fix XCode warnings --- ios/FSUtils.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/FSUtils.h b/ios/FSUtils.h index d92545f..2c32987 100644 --- a/ios/FSUtils.h +++ b/ios/FSUtils.h @@ -10,8 +10,8 @@ @interface FSUtils : NSObject -+ (NSString*) getPathFromUri:(NSString* _Nonnull)uri; -+ (NSString*) getTempFilePath:(NSString* _Nullable)ext; -+ (BOOL) prepareFileForWriting:(NSString* _Nonnull)path error:(NSError** _Nullable) outError; ++ (NSString*_Nonnull) getPathFromUri:(NSString* _Nonnull)uri; ++ (NSString*_Nonnull) getTempFilePath:(NSString* _Nullable)ext; ++ (BOOL) prepareFileForWriting:(NSString* _Nonnull)path error:(NSError*_Nullable*_Nullable) outError; @end From 3eab5ca606843c3c2ee0adfebfdcdba0b5b3428e Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Mon, 19 Aug 2019 20:03:53 +0300 Subject: [PATCH 03/14] update error messages (iOS) --- ios/RNVirgilCrypto.m | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ios/RNVirgilCrypto.m b/ios/RNVirgilCrypto.m index 2133d93..4989753 100644 --- a/ios/RNVirgilCrypto.m +++ b/ios/RNVirgilCrypto.m @@ -284,7 +284,7 @@ - (NSData*) computeHashFor:(NSData*) data using:(VSMHashAlgorithm) alg { NSString* inputPath = [FSUtils getPathFromUri:inputUri]; if ([[NSFileManager defaultManager] fileExistsAtPath:inputPath] == NO) { - reject(@"no_such_file", [NSString stringWithFormat:@"File does not exist at path %@", inputPath], nil); + reject(@"invalid_input_file", [NSString stringWithFormat:@"File does not exist at path %@", inputPath], nil); return; } @@ -295,7 +295,7 @@ - (NSData*) computeHashFor:(NSData*) data using:(VSMHashAlgorithm) alg NSError* err = nil; BOOL isOutputReady = [FSUtils prepareFileForWriting:outputPath error:&err]; if (!isOutputReady) { - reject(@"invalid_destination", [err description], err); + reject(@"invalid_output_file", [err description], err); } NSArray* publicKeys = [self decodeAndImportPublicKeys:recipientPublicKeysBase64 error:&err]; @@ -321,7 +321,11 @@ - (NSData*) computeHashFor:(NSData*) data using:(VSMHashAlgorithm) alg [outStream close]; if (!isSuccessful) { - reject(@"failed_to_encrypt", @"Unexpected error encrypting stream", encryptErr); + reject( + @"failed_to_encrypt", + [NSString stringWithFormat:@"Could not encrypt file; %@", [encryptErr localizedDescription]], + encryptErr + ); return; } @@ -337,7 +341,7 @@ - (NSData*) computeHashFor:(NSData*) data using:(VSMHashAlgorithm) alg { NSString* inputPath = [FSUtils getPathFromUri:inputUri]; if ([[NSFileManager defaultManager] fileExistsAtPath:inputPath] == NO) { - reject(@"no_such_file", [NSString stringWithFormat:@"File does not exist at path %@", inputUri], nil); + reject(@"invalid_input_file", [NSString stringWithFormat:@"File does not exist at path %@", inputUri], nil); return; } @@ -348,7 +352,7 @@ - (NSData*) computeHashFor:(NSData*) data using:(VSMHashAlgorithm) alg NSError* err = nil; BOOL isOutputReady = [FSUtils prepareFileForWriting:outputPath error:&err]; if (!isOutputReady) { - reject(@"invalid_destination", [err description], err); + reject(@"invalid_output_file", [err description], err); } VSMVirgilKeyPair* keypair = [self.crypto importPrivateKeyFrom:[privateKeyBase64 dataUsingBase64] error:&err]; @@ -376,8 +380,11 @@ - (NSData*) computeHashFor:(NSData*) data using:(VSMHashAlgorithm) alg [outputStream close]; if (!isSuccessful) { - NSLog(@"ERROR %@", decryptErr); - reject(@"failed_to_decrypt", @"Unexpected error decrypting stream", decryptErr); + reject( + @"failed_to_decrypt", + [NSString stringWithFormat:@"Could not decrypt file; %@", [decryptErr localizedDescription]], + decryptErr + ); return; } From 9d87782e8e972bc97dc16468422eb6bdbd8153fe Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Mon, 19 Aug 2019 20:08:33 +0300 Subject: [PATCH 04/14] add ability to encrypt \ decrypt file on Android --- .../rn/crypto/RNVirgilCryptoModule.java | 124 +++++++++++ .../virgilsecurity/rn/crypto/utils/FS.java | 115 ++++++++++ .../utils/InvalidOutputFilePathException.java | 7 + .../rn/crypto/utils/PathResolver.java | 204 ++++++++++++++++++ 4 files changed, 450 insertions(+) create mode 100644 android/src/main/java/com/virgilsecurity/rn/crypto/utils/FS.java create mode 100644 android/src/main/java/com/virgilsecurity/rn/crypto/utils/InvalidOutputFilePathException.java create mode 100644 android/src/main/java/com/virgilsecurity/rn/crypto/utils/PathResolver.java diff --git a/android/src/main/java/com/virgilsecurity/rn/crypto/RNVirgilCryptoModule.java b/android/src/main/java/com/virgilsecurity/rn/crypto/RNVirgilCryptoModule.java index 86f7cf4..a9b4462 100644 --- a/android/src/main/java/com/virgilsecurity/rn/crypto/RNVirgilCryptoModule.java +++ b/android/src/main/java/com/virgilsecurity/rn/crypto/RNVirgilCryptoModule.java @@ -3,26 +3,39 @@ import android.util.Log; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.WritableMap; +import com.virgilsecurity.rn.crypto.utils.FS; +import com.virgilsecurity.rn.crypto.utils.InvalidOutputFilePathException; import com.virgilsecurity.sdk.crypto.HashAlgorithm; import com.virgilsecurity.sdk.crypto.KeyType; import com.virgilsecurity.sdk.crypto.VirgilCrypto; import com.virgilsecurity.sdk.crypto.VirgilKeyPair; +import com.virgilsecurity.sdk.crypto.VirgilPrivateKey; import com.virgilsecurity.sdk.crypto.VirgilPublicKey; import com.virgilsecurity.sdk.crypto.exceptions.CryptoException; import com.virgilsecurity.rn.crypto.utils.Encodings; import com.virgilsecurity.rn.crypto.utils.ResponseFactory; +import com.virgilsecurity.sdk.crypto.exceptions.DecryptionException; +import com.virgilsecurity.sdk.crypto.exceptions.EncryptionException; public class RNVirgilCryptoModule extends ReactContextBaseJavaModule { @@ -30,10 +43,21 @@ public class RNVirgilCryptoModule extends ReactContextBaseJavaModule { private final ReactApplicationContext reactContext; private final VirgilCrypto crypto; + public static ReactApplicationContext RCTContext; + private static LinkedBlockingQueue taskQueue = new LinkedBlockingQueue<>(); + private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor( + 2, + 8, + 5000, + TimeUnit.MILLISECONDS, + taskQueue); + public RNVirgilCryptoModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext = reactContext; this.crypto = new VirgilCrypto(); + + RCTContext = reactContext; } @Override @@ -219,6 +243,106 @@ public WritableMap generateRandomData(Integer size) { return ResponseFactory.createStringResponse(Encodings.encodeBase64(randomData)); } + @ReactMethod + public void encryptFile(String inputUri, String outputUri, ReadableArray recipientsBase64, final Promise promise) { + final List publicKeys; + try { + publicKeys = this.decodeAndImportPublicKeys(recipientsBase64); + } + catch (CryptoException e) { + promise.reject("invalid_public_key", "Public keys array contains invalid public keys"); + return; + } + + final String inputPath = FS.normalizePath(inputUri); + final String outputPath; + if (outputUri == null) { + outputPath = FS.getTempFilePath(FS.getFileExtension(inputPath)); + } else { + outputPath = outputUri; + } + + final VirgilCrypto vc = this.crypto; + + threadPool.execute(new Runnable() { + @Override + public void run() { + try { + InputStream inStream = FS.getInputStreamFromPath(inputPath); + OutputStream outStream = FS.getOutputStreamFromPath(outputPath); + vc.encrypt(inStream, outStream, publicKeys); + outStream.close(); + inStream.close(); + promise.resolve(outputPath); + } catch (FileNotFoundException e) { + promise.reject( + "invalid_input_file", + String.format("File does not exist at path %s", inputPath) + ); + } catch (InvalidOutputFilePathException e) { + promise.reject("invalid_output_file", e.getLocalizedMessage()); + } catch (EncryptionException e) { + promise.reject( + "failed_to_encrypt", + String.format("Could not encrypt file; %s", e.getLocalizedMessage()) + ); + } catch (IOException e) { + promise.reject("unexpected_error", e.getLocalizedMessage()); + } + } + }); + } + + @ReactMethod + public void decryptFile(String inputUri, String outputUri, String privateKeyBase64, final Promise promise) { + VirgilKeyPair keypair; + try { + keypair = this.crypto.importPrivateKey(Encodings.decodeBase64(privateKeyBase64)); + } catch (CryptoException e) { + promise.reject("invalid_private_key", "The given value is not a valid private key"); + return; + } + + final String inputPath = FS.normalizePath(inputUri); + final String outputPath; + if (outputUri == null) { + outputPath = FS.getTempFilePath(FS.getFileExtension(inputPath)); + } else { + outputPath = outputUri; + } + + final VirgilCrypto vc = this.crypto; + final VirgilPrivateKey privateKey = keypair.getPrivateKey(); + + threadPool.execute(new Runnable() { + @Override + public void run() { + try { + InputStream inStream = FS.getInputStreamFromPath(inputPath); + OutputStream outStream = FS.getOutputStreamFromPath(outputPath); + vc.decrypt(inStream, outStream, privateKey); + outStream.close(); + inStream.close(); + promise.resolve(outputPath); + } catch (FileNotFoundException e) { + promise.reject( + "invalid_input_file", + String.format("File does not exist at path %s", inputPath) + ); + } catch (InvalidOutputFilePathException e) { + promise.reject("invalid_output_file", e.getLocalizedMessage()); + } catch (DecryptionException e) { + promise.reject( + "failed_to_decrypt", + String.format("Could not decrypt file; %s", e.getLocalizedMessage()) + ); + } catch (IOException e) { + promise.reject("unexpected_error", e.getLocalizedMessage()); + } + } + }); + } + private List decodeAndImportPublicKeys(ReadableArray publicKeysBase64) throws CryptoException { List publicKeys = new ArrayList<>(publicKeysBase64.size()); for(Object publicKeyBase64 : publicKeysBase64.toArrayList()) { diff --git a/android/src/main/java/com/virgilsecurity/rn/crypto/utils/FS.java b/android/src/main/java/com/virgilsecurity/rn/crypto/utils/FS.java new file mode 100644 index 0000000..1d6c917 --- /dev/null +++ b/android/src/main/java/com/virgilsecurity/rn/crypto/utils/FS.java @@ -0,0 +1,115 @@ +package com.virgilsecurity.rn.crypto.utils; + +import android.net.Uri; + +import com.virgilsecurity.rn.crypto.RNVirgilCryptoModule; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + +public final class FS { + + public static final String FILE_PREFIX_BUNDLE_ASSET = "bundle-assets://"; + + public static InputStream getInputStreamFromPath(String path) throws IOException { + String resolved = normalizePath(path); + if (resolved != null) { + path = resolved; + } + + if (resolved != null && resolved.startsWith(FILE_PREFIX_BUNDLE_ASSET)) { + String assetName = path.replace(FILE_PREFIX_BUNDLE_ASSET, ""); + return RNVirgilCryptoModule.RCTContext.getAssets().open(assetName); + } + + if (resolved == null) { + return RNVirgilCryptoModule.RCTContext.getContentResolver() + .openInputStream(Uri.parse(path)); + } + + File f = new File(path); + return new FileInputStream(f); + } + + public static OutputStream getOutputStreamFromPath(String path) throws InvalidOutputFilePathException { + File dest = new File(path); + File dir = dest.getParentFile(); + + try { + if (!dest.exists()) { + if (dir != null && !dir.exists()) { + if (!dir.mkdirs()) { + throw new InvalidOutputFilePathException( + String.format("Failed to create parent directory of '%s'", path) + ); + } + } + + + if (!dest.createNewFile()) { + throw new InvalidOutputFilePathException( + String.format("File '%s' does not exist and could not be created", path) + ); + } + } else if (dest.isDirectory()) { + throw new InvalidOutputFilePathException( + String.format("Expected a file but '%s' is a directory", path) + ); + } + + return new FileOutputStream(path, false); + } catch (IOException e) { + throw new InvalidOutputFilePathException( + String.format( + "Failed to create write stream at path: '%s'; %s", + path, + e.getLocalizedMessage() + ) + ); + } + } + + /** + * Normalize the path, remove URI scheme (xxx://) so that we can handle it. + * @param path URI string. + * @return Normalized string + */ + public static String normalizePath(String path) { + if(path == null) + return null; + if(!path.matches("\\w+:.*")) + return path; + if(path.startsWith("file://")) { + return path.replace("file://", ""); + } + + Uri uri = Uri.parse(path); + if(path.startsWith(FS.FILE_PREFIX_BUNDLE_ASSET)) { + return path; + } + + return PathResolver.getRealPathFromURI(RNVirgilCryptoModule.RCTContext, uri); + } + + public static String getTempFilePath(String extension) { + String cacheDir = RNVirgilCryptoModule.RCTContext.getCacheDir().getAbsolutePath(); + String fileName = UUID.randomUUID().toString(); + if (extension != null) { + fileName = String.format("%s.%s", fileName, extension); + } + return String.format("%s/%s", cacheDir, fileName); + } + + public static String getFileExtension(String path) { + int i = path.lastIndexOf("."); + if (i > 0) { + return path.substring(i + 1); + } + return null; + } +} diff --git a/android/src/main/java/com/virgilsecurity/rn/crypto/utils/InvalidOutputFilePathException.java b/android/src/main/java/com/virgilsecurity/rn/crypto/utils/InvalidOutputFilePathException.java new file mode 100644 index 0000000..ae529da --- /dev/null +++ b/android/src/main/java/com/virgilsecurity/rn/crypto/utils/InvalidOutputFilePathException.java @@ -0,0 +1,7 @@ +package com.virgilsecurity.rn.crypto.utils; + +public class InvalidOutputFilePathException extends Exception { + public InvalidOutputFilePathException(String errorMessage) { + super(errorMessage); + } +} diff --git a/android/src/main/java/com/virgilsecurity/rn/crypto/utils/PathResolver.java b/android/src/main/java/com/virgilsecurity/rn/crypto/utils/PathResolver.java new file mode 100644 index 0000000..fead471 --- /dev/null +++ b/android/src/main/java/com/virgilsecurity/rn/crypto/utils/PathResolver.java @@ -0,0 +1,204 @@ +// implementation borrowed from https://github.com/joltup/rn-fetch-blob/blob/master/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java + +package com.virgilsecurity.rn.crypto.utils; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.content.ContentUris; +import android.os.Environment; +import android.content.ContentResolver; +import android.util.Log; + +import java.io.File; +import java.io.InputStream; +import java.io.FileOutputStream; + +public class PathResolver { + + public static String getRealPathFromURI(final Context context, final Uri uri) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[] { + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(context, uri, null, null); + } + // Other Providers + else{ + try { + InputStream attachment = context.getContentResolver().openInputStream(uri); + if (attachment != null) { + String filename = getContentName(context.getContentResolver(), uri); + if (filename != null) { + File file = new File(context.getCacheDir(), filename); + FileOutputStream tmp = new FileOutputStream(file); + byte[] buffer = new byte[1024]; + while (attachment.read(buffer) > 0) { + tmp.write(buffer); + } + tmp.close(); + attachment.close(); + return file.getAbsolutePath(); + } + } + } catch (Exception e) { + Log.w("PathResolver", e.toString()); + return null; + } + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + private static String getContentName(ContentResolver resolver, Uri uri) { + Cursor cursor = resolver.query(uri, null, null, null, null); + cursor.moveToFirst(); + int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); + if (nameIndex >= 0) { + String name = cursor.getString(nameIndex); + cursor.close(); + return name; + } + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + public static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + String result = null; + final String column = "_data"; + final String[] projection = { + column + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(column); + result = cursor.getString(index); + } + } + catch (Exception ex) { + ex.printStackTrace(); + return null; + } + finally { + if (cursor != null) + cursor.close(); + } + return result; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + +} From 5e31809d57c558ca958b2d951fdd38b8b06a6bd2 Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Wed, 21 Aug 2019 19:00:53 +0300 Subject: [PATCH 05/14] add ability to sign and verify files (Android) --- .../rn/crypto/RNVirgilCryptoModule.java | 115 ++++++++++++++---- 1 file changed, 89 insertions(+), 26 deletions(-) diff --git a/android/src/main/java/com/virgilsecurity/rn/crypto/RNVirgilCryptoModule.java b/android/src/main/java/com/virgilsecurity/rn/crypto/RNVirgilCryptoModule.java index a9b4462..76734a8 100644 --- a/android/src/main/java/com/virgilsecurity/rn/crypto/RNVirgilCryptoModule.java +++ b/android/src/main/java/com/virgilsecurity/rn/crypto/RNVirgilCryptoModule.java @@ -36,6 +36,8 @@ import com.virgilsecurity.rn.crypto.utils.ResponseFactory; import com.virgilsecurity.sdk.crypto.exceptions.DecryptionException; import com.virgilsecurity.sdk.crypto.exceptions.EncryptionException; +import com.virgilsecurity.sdk.crypto.exceptions.SigningException; +import com.virgilsecurity.sdk.crypto.exceptions.VerificationException; public class RNVirgilCryptoModule extends ReactContextBaseJavaModule { @@ -198,8 +200,7 @@ public WritableMap verifySignature(String signatureBase64, String dataBase64, St } @ReactMethod(isBlockingSynchronousMethod = true) - public WritableMap signAndEncrypt(String dataBase64, String privateKeyBase64, ReadableArray recipientsBase64) - { + public WritableMap signAndEncrypt(String dataBase64, String privateKeyBase64, ReadableArray recipientsBase64) { try { VirgilKeyPair keypair = this.crypto.importPrivateKey(Encodings.decodeBase64(privateKeyBase64)); List publicKeys = this.decodeAndImportPublicKeys(recipientsBase64); @@ -212,8 +213,7 @@ public WritableMap signAndEncrypt(String dataBase64, String privateKeyBase64, Re } @ReactMethod(isBlockingSynchronousMethod = true) - public WritableMap decryptAndVerify(String dataBase64, String privateKeyBase64, ReadableArray sendersPublicKeysBase64) - { + public WritableMap decryptAndVerify(String dataBase64, String privateKeyBase64, ReadableArray sendersPublicKeysBase64) { try { VirgilKeyPair keypair = this.crypto.importPrivateKey(Encodings.decodeBase64(privateKeyBase64)); List publicKeys = this.decodeAndImportPublicKeys(sendersPublicKeysBase64); @@ -244,7 +244,7 @@ public WritableMap generateRandomData(Integer size) { } @ReactMethod - public void encryptFile(String inputUri, String outputUri, ReadableArray recipientsBase64, final Promise promise) { + public void encryptFile(final String inputPath, String outputPath, ReadableArray recipientsBase64, final Promise promise) { final List publicKeys; try { publicKeys = this.decodeAndImportPublicKeys(recipientsBase64); @@ -254,12 +254,11 @@ public void encryptFile(String inputUri, String outputUri, ReadableArray recipie return; } - final String inputPath = FS.normalizePath(inputUri); - final String outputPath; - if (outputUri == null) { - outputPath = FS.getTempFilePath(FS.getFileExtension(inputPath)); + final String resolvedOutputPath; + if (outputPath == null) { + resolvedOutputPath = FS.getTempFilePath(FS.getFileExtension(inputPath)); } else { - outputPath = outputUri; + resolvedOutputPath = outputPath; } final VirgilCrypto vc = this.crypto; @@ -267,13 +266,12 @@ public void encryptFile(String inputUri, String outputUri, ReadableArray recipie threadPool.execute(new Runnable() { @Override public void run() { - try { + try ( InputStream inStream = FS.getInputStreamFromPath(inputPath); - OutputStream outStream = FS.getOutputStreamFromPath(outputPath); + OutputStream outStream = FS.getOutputStreamFromPath(resolvedOutputPath) + ) { vc.encrypt(inStream, outStream, publicKeys); - outStream.close(); - inStream.close(); - promise.resolve(outputPath); + promise.resolve(resolvedOutputPath); } catch (FileNotFoundException e) { promise.reject( "invalid_input_file", @@ -294,7 +292,7 @@ public void run() { } @ReactMethod - public void decryptFile(String inputUri, String outputUri, String privateKeyBase64, final Promise promise) { + public void decryptFile(final String inputPath, String outputPath, String privateKeyBase64, final Promise promise) { VirgilKeyPair keypair; try { keypair = this.crypto.importPrivateKey(Encodings.decodeBase64(privateKeyBase64)); @@ -303,12 +301,11 @@ public void decryptFile(String inputUri, String outputUri, String privateKeyBase return; } - final String inputPath = FS.normalizePath(inputUri); - final String outputPath; - if (outputUri == null) { - outputPath = FS.getTempFilePath(FS.getFileExtension(inputPath)); + final String resolvedOutputPath; + if (outputPath == null) { + resolvedOutputPath = FS.getTempFilePath(FS.getFileExtension(inputPath)); } else { - outputPath = outputUri; + resolvedOutputPath = outputPath; } final VirgilCrypto vc = this.crypto; @@ -317,13 +314,12 @@ public void decryptFile(String inputUri, String outputUri, String privateKeyBase threadPool.execute(new Runnable() { @Override public void run() { - try { + try ( InputStream inStream = FS.getInputStreamFromPath(inputPath); - OutputStream outStream = FS.getOutputStreamFromPath(outputPath); + OutputStream outStream = FS.getOutputStreamFromPath(resolvedOutputPath) + ) { vc.decrypt(inStream, outStream, privateKey); - outStream.close(); - inStream.close(); - promise.resolve(outputPath); + promise.resolve(resolvedOutputPath); } catch (FileNotFoundException e) { promise.reject( "invalid_input_file", @@ -343,6 +339,73 @@ public void run() { }); } + @ReactMethod + public void generateFileSignature(final String inputPath, String privateKeyBase64, final Promise promise) { + VirgilKeyPair keypair; + try { + keypair = this.crypto.importPrivateKey(Encodings.decodeBase64(privateKeyBase64)); + } catch (CryptoException e) { + promise.reject("invalid_private_key", "The given value is not a valid private key"); + return; + } + + final VirgilCrypto vc = this.crypto; + final VirgilPrivateKey privateKey = keypair.getPrivateKey(); + + threadPool.execute(new Runnable() { + @Override + public void run() { + try (InputStream inStream = FS.getInputStreamFromPath(inputPath)) { + byte[] signature = vc.generateSignature(inStream, privateKey); + promise.resolve(Encodings.encodeBase64(signature)); + } catch (FileNotFoundException e) { + promise.reject( + "invalid_input_file", + String.format("File does not exist at path %s", inputPath) + ); + } catch (SigningException e) { + promise.reject("failed_to_sign", e.getLocalizedMessage()); + } catch (IOException e) { + promise.reject("unexpected_error", e.getLocalizedMessage()); + } + } + }); + } + + @ReactMethod + public void verifyFileSignature(String signatureBase64, final String inputPath, String publicKeyBase64, final Promise promise) { + final VirgilPublicKey publicKey; + try { + publicKey = this.crypto.importPublicKey(Encodings.decodeBase64(publicKeyBase64)); + } catch (CryptoException e) { + promise.reject("invalid_public_key", "The given value is not a valid public key"); + return; + } + + final byte[] signature = Encodings.decodeBase64(signatureBase64); + final VirgilCrypto vc = this.crypto; + + + threadPool.execute(new Runnable() { + @Override + public void run() { + try (InputStream inStream = FS.getInputStreamFromPath(inputPath)) { + boolean isVerified = vc.verifySignature(signature, inStream, publicKey); + promise.resolve(isVerified); + } catch (FileNotFoundException e) { + promise.reject( + "invalid_input_file", + String.format("File does not exist at path %s", inputPath) + ); + } catch (VerificationException e) { + promise.reject("failed_to_verify", e.getLocalizedMessage()); + } catch (IOException e) { + promise.reject("unexpected_error", e.getLocalizedMessage()); + } + } + }); + } + private List decodeAndImportPublicKeys(ReadableArray publicKeysBase64) throws CryptoException { List publicKeys = new ArrayList<>(publicKeysBase64.size()); for(Object publicKeyBase64 : publicKeysBase64.toArrayList()) { From 42db5e2e21e567bc54ece45a6b3089181eee2afd Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Wed, 21 Aug 2019 19:36:41 +0300 Subject: [PATCH 06/14] add ability to sign and verify files (iOS) --- ios/RNVirgilCrypto.m | 69 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/ios/RNVirgilCrypto.m b/ios/RNVirgilCrypto.m index 4989753..ff15b88 100644 --- a/ios/RNVirgilCrypto.m +++ b/ios/RNVirgilCrypto.m @@ -391,4 +391,73 @@ - (NSData*) computeHashFor:(NSData*) data using:(VSMHashAlgorithm) alg resolve(outputPath); }); } + +RCT_EXPORT_METHOD(generateFileSignature:(NSString*)inputUri + with:(NSString*)privateKeyBase64 + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString* inputPath = [FSUtils getPathFromUri:inputUri]; + if ([[NSFileManager defaultManager] fileExistsAtPath:inputPath] == NO) { + reject(@"invalid_input_file", [NSString stringWithFormat:@"File does not exist at path %@", inputUri], nil); + return; + } + + NSError* err = nil; + VSMVirgilKeyPair* keypair = [self.crypto importPrivateKeyFrom:[privateKeyBase64 dataUsingBase64] error:&err]; + if (nil == keypair) { + reject(@"invalid_private_key", @"The given value is not a valid private key", err); + return; + } + + VSMVirgilPrivateKey* privateKey = keypair.privateKey; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSError *signErr; + NSInputStream *inputStream = [NSInputStream inputStreamWithFileAtPath:inputPath]; + + [inputStream open]; + NSData* signature = [self.crypto generateStreamSignatureOf:inputStream using:privateKey error:&signErr]; + [inputStream close]; + + if (nil == signature) { + reject( + @"failed_to_sign", + [NSString stringWithFormat:@"Could not generate signature of file; %@", [signErr localizedDescription]], + signErr + ); + return; + } + + resolve([signature stringUsingBase64]); + }); +} + +RCT_EXPORT_METHOD(verifyFileSignature:(NSString*)signatureBase64 + ofFile:(NSString*)inputUri + with:(NSString*)publicKeyBase64 + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString* inputPath = [FSUtils getPathFromUri:inputUri]; + if ([[NSFileManager defaultManager] fileExistsAtPath:inputPath] == NO) { + reject(@"invalid_input_file", [NSString stringWithFormat:@"File does not exist at path %@", inputPath], nil); + return; + } + + NSError* err = nil; + VSMVirgilPublicKey* publicKey = [self.crypto importPublicKeyFrom:[publicKeyBase64 dataUsingBase64] error:&err]; + if (nil == publicKey) { + reject(@"invalid_public_key", @"The given value is not a valid public key", err); + return; + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSInputStream *inStream = [NSInputStream inputStreamWithFileAtPath:inputPath]; + [inStream open]; + BOOL isValid = [self.crypto verifyStreamSignature_objc:[signatureBase64 dataUsingBase64] of:inStream with:publicKey]; + [inStream close]; + resolve(@(isValid)); + }); +} @end From 0b60254c666ecdddefc5b27e3748f12d40bee9ac Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Thu, 22 Aug 2019 18:07:32 +0300 Subject: [PATCH 07/14] fix temporary file creation on Android --- .../java/com/virgilsecurity/rn/crypto/utils/FS.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/virgilsecurity/rn/crypto/utils/FS.java b/android/src/main/java/com/virgilsecurity/rn/crypto/utils/FS.java index 1d6c917..abfcb40 100644 --- a/android/src/main/java/com/virgilsecurity/rn/crypto/utils/FS.java +++ b/android/src/main/java/com/virgilsecurity/rn/crypto/utils/FS.java @@ -106,9 +106,16 @@ public static String getTempFilePath(String extension) { } public static String getFileExtension(String path) { - int i = path.lastIndexOf("."); - if (i > 0) { - return path.substring(i + 1); + path = normalizePath(path); + if (path == null) { + return null; + } + + int dotIndex = path.lastIndexOf("."); + int separatorIndex = path.indexOf(File.separator); + + if (dotIndex > Math.max(0, separatorIndex)) { + return path.substring(dotIndex + 1); } return null; } From f1d20f08ec55ec209f185623fae91b8dcb2ce378 Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Thu, 22 Aug 2019 19:48:30 +0300 Subject: [PATCH 08/14] expose methods to encrypt, decrypt, sign, verify files (JS) --- src/virgil-crypto.js | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/virgil-crypto.js b/src/virgil-crypto.js index 660e0e9..e8c1dc2 100644 --- a/src/virgil-crypto.js +++ b/src/virgil-crypto.js @@ -14,6 +14,8 @@ import { checkedGetKeyPairType } from './key-pair-type'; const { RNVirgilCrypto } = NativeModules; +const normalizeFilePath = (path) => (path.startsWith('file://') ? path.slice(7) : path); + export const virgilCrypto = { getRandomBytes(size) { if (!Number.isSafeInteger(size)) { @@ -143,5 +145,58 @@ export const virgilCrypto = { importPublicKey(rawPublicKey) { const publicKeyBase64 = anyToBase64(rawPublicKey, 'base64', 'rawPublicKey'); return new VirgilPublicKey(publicKeyBase64); + }, + + encryptFile({ inputPath, outputPath, publicKeys }) { + if (typeof inputPath !== 'string') { + throw new TypeError('Expected "inputPath" parameter to be a string. Got ' + typeof inputPath); + } + + if (outputPath != null && typeof outputPath !== 'string') { + throw new TypeError('Expected "outputPath" parameter to be a string. Got ' + typeof outputPath); + } + + const publicKeysValues = checkedGetPublicKeyValues(publicKeys); + + return RNVirgilCrypto.encryptFile( + normalizeFilePath(inputPath), + outputPath != null ? normalizeFilePath(outputPath) : undefined, + publicKeysValues + ); + }, + + decryptFile({ inputPath, outputPath, privateKey}) { + if (typeof inputPath !== 'string') { + throw new TypeError('Expected "inputPath" parameter to be a string. Got ' + typeof inputPath); + } + + if (outputPath != null && typeof outputPath !== 'string') { + throw new TypeError('Expected "outputPath" parameter to be a string. Got ' + typeof outputPath); + } + + const privateKeyValue = checkedGetPrivateKeyValue(privateKey); + + return RNVirgilCrypto.decryptFile( + normalizeFilePath(inputPath), + outputPath != null ? normalizeFilePath(outputPath) : outputPath, + privateKeyValue + ); + }, + + generateFileSignature({ inputPath, privateKey }) { + if (typeof inputPath !== 'string') { + throw new TypeError('Expected "inputPath" parameter to be a string. Got ' + typeof inputPath); + } + const privateKeyValue = checkedGetPrivateKeyValue(privateKey); + return RNVirgilCrypto.generateFileSignature(normalizeFilePath(inputPath), privateKeyValue); + }, + + verifyFileSignature({ inputPath, signature, publicKey }) { + if (typeof inputPath !== 'string') { + throw new TypeError('Expected "inputPath" parameter to be a string. Got ' + typeof inputPath); + } + const publicKeyValue = checkedGetPublicKeyValue(publicKey); + const signatureBase64 = anyToBase64(signature, 'base64', 'signature'); + return RNVirgilCrypto.verifyFileSignature(signatureBase64, normalizeFilePath(inputPath), publicKeyValue); } } From ad44815f9853c2d8449c2e00f9a177764b06ee41 Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Fri, 23 Aug 2019 13:06:45 +0300 Subject: [PATCH 09/14] fix formatting --- src/virgil-crypto.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/virgil-crypto.js b/src/virgil-crypto.js index e8c1dc2..52286d9 100644 --- a/src/virgil-crypto.js +++ b/src/virgil-crypto.js @@ -165,7 +165,7 @@ export const virgilCrypto = { ); }, - decryptFile({ inputPath, outputPath, privateKey}) { + decryptFile({ inputPath, outputPath, privateKey }) { if (typeof inputPath !== 'string') { throw new TypeError('Expected "inputPath" parameter to be a string. Got ' + typeof inputPath); } From b542270d37291230c1a7a7613752bc3534f2fbc0 Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Fri, 23 Aug 2019 17:35:56 +0300 Subject: [PATCH 10/14] fix header search paths (iOS) --- ios/RNVirgilCrypto.xcodeproj/project.pbxproj | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ios/RNVirgilCrypto.xcodeproj/project.pbxproj b/ios/RNVirgilCrypto.xcodeproj/project.pbxproj index 0b4b920..b2dba9a 100644 --- a/ios/RNVirgilCrypto.xcodeproj/project.pbxproj +++ b/ios/RNVirgilCrypto.xcodeproj/project.pbxproj @@ -272,9 +272,8 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/../../../React", - "$(SRCROOT)/../../react-native/React", - "$(SRCROOT)/../../../ios/Pods/Headers", + "$(SRCROOT)/../../react-native/React/**", + "$(SRCROOT)/../../../ios/Pods/Headers/**", "${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/VirgilCrypto/VirgilCrypto.framework/Headers", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; @@ -295,9 +294,8 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/../../../React", - "$(SRCROOT)/../../react-native/React", - "$(SRCROOT)/../../../ios/Pods/Headers", + "$(SRCROOT)/../../react-native/React/**", + "$(SRCROOT)/../../../ios/Pods/Headers/**", "${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/VirgilCrypto/VirgilCrypto.framework/Headers", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; From 1126f5c7e3e2b18f05aee0d620518fc155f03170 Mon Sep 17 00:00:00 2001 From: Vadim Avdeev Date: Fri, 23 Aug 2019 19:38:43 +0300 Subject: [PATCH 11/14] add file encryption example --- examples/FileEncryptionSample/.buckconfig | 6 + examples/FileEncryptionSample/.eslintrc.js | 4 + examples/FileEncryptionSample/.flowconfig | 99 + examples/FileEncryptionSample/.gitattributes | 1 + examples/FileEncryptionSample/.gitignore | 62 + examples/FileEncryptionSample/.prettierrc.js | 6 + examples/FileEncryptionSample/.watchmanconfig | 1 + examples/FileEncryptionSample/App.js | 202 + examples/FileEncryptionSample/README.md | 41 + .../__tests__/App-test.js | 14 + .../FileEncryptionSample/android/app/BUCK | 55 + .../android/app/build.gradle | 210 + .../android/app/build_defs.bzl | 19 + .../android/app/proguard-rules.pro | 10 + .../android/app/src/debug/AndroidManifest.xml | 8 + .../android/app/src/main/AndroidManifest.xml | 28 + .../fileencryptionsample/MainActivity.java | 15 + .../fileencryptionsample/MainApplication.java | 49 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 9 + .../FileEncryptionSample/android/build.gradle | 38 + .../android/gradle.properties | 21 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + examples/FileEncryptionSample/android/gradlew | 188 + .../FileEncryptionSample/android/gradlew.bat | 100 + .../android/settings.gradle | 3 + examples/FileEncryptionSample/app.json | 4 + examples/FileEncryptionSample/babel.config.js | 3 + .../download-verify-and-decrypt.js | 65 + .../encrypt-sign-and-upload.js | 66 + examples/FileEncryptionSample/index.js | 9 + examples/FileEncryptionSample/ios/Cartfile | 1 + .../ios/Cartfile.resolved | 3 + .../ios/FileEncryptionSample-tvOS/Info.plist | 53 + .../FileEncryptionSample-tvOSTests/Info.plist | 24 + .../project.pbxproj | 1004 +++ .../FileEncryptionSample-tvOS.xcscheme | 129 + .../xcschemes/FileEncryptionSample.xcscheme | 129 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../ios/FileEncryptionSample/AppDelegate.h | 15 + .../ios/FileEncryptionSample/AppDelegate.m | 42 + .../Base.lproj/LaunchScreen.xib | 42 + .../AppIcon.appiconset/Contents.json | 38 + .../Images.xcassets/Contents.json | 6 + .../ios/FileEncryptionSample/Info.plist | 63 + .../ios/FileEncryptionSample/main.m | 16 + .../FileEncryptionSampleTests.m | 68 + .../ios/FileEncryptionSampleTests/Info.plist | 24 + examples/FileEncryptionSample/ios/Podfile | 46 + .../FileEncryptionSample/ios/Podfile.lock | 193 + examples/FileEncryptionSample/metro.config.js | 17 + examples/FileEncryptionSample/package.json | 35 + .../react-native.config.js | 12 + .../server/package-lock.json | 526 ++ .../FileEncryptionSample/server/package.json | 16 + .../FileEncryptionSample/server/server.js | 89 + examples/FileEncryptionSample/yarn.lock | 6689 +++++++++++++++++ 69 files changed, 10642 insertions(+) create mode 100644 examples/FileEncryptionSample/.buckconfig create mode 100644 examples/FileEncryptionSample/.eslintrc.js create mode 100644 examples/FileEncryptionSample/.flowconfig create mode 100644 examples/FileEncryptionSample/.gitattributes create mode 100644 examples/FileEncryptionSample/.gitignore create mode 100644 examples/FileEncryptionSample/.prettierrc.js create mode 100644 examples/FileEncryptionSample/.watchmanconfig create mode 100644 examples/FileEncryptionSample/App.js create mode 100644 examples/FileEncryptionSample/README.md create mode 100644 examples/FileEncryptionSample/__tests__/App-test.js create mode 100644 examples/FileEncryptionSample/android/app/BUCK create mode 100644 examples/FileEncryptionSample/android/app/build.gradle create mode 100644 examples/FileEncryptionSample/android/app/build_defs.bzl create mode 100644 examples/FileEncryptionSample/android/app/proguard-rules.pro create mode 100644 examples/FileEncryptionSample/android/app/src/debug/AndroidManifest.xml create mode 100644 examples/FileEncryptionSample/android/app/src/main/AndroidManifest.xml create mode 100644 examples/FileEncryptionSample/android/app/src/main/java/com/fileencryptionsample/MainActivity.java create mode 100644 examples/FileEncryptionSample/android/app/src/main/java/com/fileencryptionsample/MainApplication.java create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/values/strings.xml create mode 100644 examples/FileEncryptionSample/android/app/src/main/res/values/styles.xml create mode 100644 examples/FileEncryptionSample/android/build.gradle create mode 100644 examples/FileEncryptionSample/android/gradle.properties create mode 100644 examples/FileEncryptionSample/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/FileEncryptionSample/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/FileEncryptionSample/android/gradlew create mode 100644 examples/FileEncryptionSample/android/gradlew.bat create mode 100644 examples/FileEncryptionSample/android/settings.gradle create mode 100644 examples/FileEncryptionSample/app.json create mode 100644 examples/FileEncryptionSample/babel.config.js create mode 100644 examples/FileEncryptionSample/download-verify-and-decrypt.js create mode 100644 examples/FileEncryptionSample/encrypt-sign-and-upload.js create mode 100644 examples/FileEncryptionSample/index.js create mode 100644 examples/FileEncryptionSample/ios/Cartfile create mode 100644 examples/FileEncryptionSample/ios/Cartfile.resolved create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample-tvOS/Info.plist create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample-tvOSTests/Info.plist create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample.xcodeproj/project.pbxproj create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample.xcodeproj/xcshareddata/xcschemes/FileEncryptionSample-tvOS.xcscheme create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample.xcodeproj/xcshareddata/xcschemes/FileEncryptionSample.xcscheme create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample.xcworkspace/contents.xcworkspacedata create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample/AppDelegate.h create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample/AppDelegate.m create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample/Base.lproj/LaunchScreen.xib create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample/Images.xcassets/Contents.json create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample/Info.plist create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSample/main.m create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSampleTests/FileEncryptionSampleTests.m create mode 100644 examples/FileEncryptionSample/ios/FileEncryptionSampleTests/Info.plist create mode 100644 examples/FileEncryptionSample/ios/Podfile create mode 100644 examples/FileEncryptionSample/ios/Podfile.lock create mode 100644 examples/FileEncryptionSample/metro.config.js create mode 100644 examples/FileEncryptionSample/package.json create mode 100644 examples/FileEncryptionSample/react-native.config.js create mode 100644 examples/FileEncryptionSample/server/package-lock.json create mode 100644 examples/FileEncryptionSample/server/package.json create mode 100644 examples/FileEncryptionSample/server/server.js create mode 100644 examples/FileEncryptionSample/yarn.lock diff --git a/examples/FileEncryptionSample/.buckconfig b/examples/FileEncryptionSample/.buckconfig new file mode 100644 index 0000000..934256c --- /dev/null +++ b/examples/FileEncryptionSample/.buckconfig @@ -0,0 +1,6 @@ + +[android] + target = Google Inc.:Google APIs:23 + +[maven_repositories] + central = https://repo1.maven.org/maven2 diff --git a/examples/FileEncryptionSample/.eslintrc.js b/examples/FileEncryptionSample/.eslintrc.js new file mode 100644 index 0000000..40c6dcd --- /dev/null +++ b/examples/FileEncryptionSample/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: '@react-native-community', +}; diff --git a/examples/FileEncryptionSample/.flowconfig b/examples/FileEncryptionSample/.flowconfig new file mode 100644 index 0000000..1319ea1 --- /dev/null +++ b/examples/FileEncryptionSample/.flowconfig @@ -0,0 +1,99 @@ +[ignore] +; We fork some components by platform +.*/*[.]android.js + +; Ignore "BUCK" generated dirs +/\.buckd/ + +; Ignore unexpected extra "@providesModule" +.*/node_modules/.*/node_modules/fbjs/.* + +; Ignore duplicate module providers +; For RN Apps installed via npm, "Libraries" folder is inside +; "node_modules/react-native" but in the source repo it is in the root +node_modules/react-native/Libraries/react-native/React.js + +; Ignore polyfills +node_modules/react-native/Libraries/polyfills/.* + +; These should not be required directly +; require from fbjs/lib instead: require('fbjs/lib/warning') +node_modules/warning/.* + +; Flow doesn't support platforms +.*/Libraries/Utilities/HMRLoadingView.js + +[untyped] +.*/node_modules/@react-native-community/cli/.*/.* + +[include] + +[libs] +node_modules/react-native/Libraries/react-native/react-native-interface.js +node_modules/react-native/flow/ + +[options] +emoji=true + +esproposal.optional_chaining=enable +esproposal.nullish_coalescing=enable + +module.file_ext=.js +module.file_ext=.json +module.file_ext=.ios.js + +module.system=haste +module.system.haste.use_name_reducers=true +# get basename +module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' +# strip .js or .js.flow suffix +module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' +# strip .ios suffix +module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' +module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' +module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' +module.system.haste.paths.blacklist=.*/__tests__/.* +module.system.haste.paths.blacklist=.*/__mocks__/.* +module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* +module.system.haste.paths.whitelist=/node_modules/react-native/RNTester/.* +module.system.haste.paths.whitelist=/node_modules/react-native/IntegrationTests/.* +module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/react-native/react-native-implementation.js +module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* + +munge_underscores=true + +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FlowFixMeProps +suppress_type=$FlowFixMeState + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError + +[lints] +sketchy-null-number=warn +sketchy-null-mixed=warn +sketchy-number=warn +untyped-type-import=warn +nonstrict-import=warn +deprecated-type=warn +unsafe-getters-setters=warn +inexact-spread=warn +unnecessary-invariant=warn +signature-verification-failure=warn +deprecated-utility=error + +[strict] +deprecated-type +nonstrict-import +sketchy-null +unclear-type +unsafe-getters-setters +untyped-import +untyped-type-import + +[version] +^0.98.0 diff --git a/examples/FileEncryptionSample/.gitattributes b/examples/FileEncryptionSample/.gitattributes new file mode 100644 index 0000000..d42ff18 --- /dev/null +++ b/examples/FileEncryptionSample/.gitattributes @@ -0,0 +1 @@ +*.pbxproj -text diff --git a/examples/FileEncryptionSample/.gitignore b/examples/FileEncryptionSample/.gitignore new file mode 100644 index 0000000..d53732f --- /dev/null +++ b/examples/FileEncryptionSample/.gitignore @@ -0,0 +1,62 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +*/fastlane/report.xml +*/fastlane/Preview.html +*/fastlane/screenshots + +# Bundle artifact +*.jsbundle + +# CocoaPods +/ios/Pods/ +/ios/Carthage/ + +/server/uploads/ \ No newline at end of file diff --git a/examples/FileEncryptionSample/.prettierrc.js b/examples/FileEncryptionSample/.prettierrc.js new file mode 100644 index 0000000..5c4de1a --- /dev/null +++ b/examples/FileEncryptionSample/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + bracketSpacing: false, + jsxBracketSameLine: true, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/examples/FileEncryptionSample/.watchmanconfig b/examples/FileEncryptionSample/.watchmanconfig new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/examples/FileEncryptionSample/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/examples/FileEncryptionSample/App.js b/examples/FileEncryptionSample/App.js new file mode 100644 index 0000000..7bd6112 --- /dev/null +++ b/examples/FileEncryptionSample/App.js @@ -0,0 +1,202 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + * + * @format + * @flow + */ + +import React, {Component} from 'react'; +import {Platform, StyleSheet, Text, View, Button, Image, ScrollView} from 'react-native'; +import ImagePicker from 'react-native-image-picker'; +import { virgilCrypto } from 'react-native-virgil-crypto'; +import { encryptSignAndUploadImage } from './encrypt-sign-and-upload'; +import { downloadVerifyAndDecryptImage } from './download-verify-and-decrypt'; + +const apiUrl = `http://${ Platform.OS === 'android' ? '10.0.2.2' : 'localhost' }:3000`; +const keypair = virgilCrypto.generateKeys(); + +const getFileExtension = (filename) => { + let i = filename.lastIndexOf('.'); + return i > 0 ? filename.substr(i + 1) : null; +} + +export default class App extends Component { + state = { + uploadedImage: undefined, + imageIdToDownload: undefined, + downloadedImage: undefined, + error: undefined + } + + pickImage = () => { + return new Promise((resolve, reject) => { + const options = { + title: 'Select image to upload', + storageOptions: { + skipBackup: true, + waitUntilSaved: true, + }, + quality: 1 + }; + ImagePicker.showImagePicker(options, (response) => { + if (response.didCancel) { + return resolve(null); + } + + if (response.error) { + console.log('ImagePicker Error: ', response.error); + return reject(response.error); + } + + const { data, ...image } = response; + + resolve(image); + }); + }); + } + + handleUpload = () => { + this.reset(); + this.pickImage() + .then(image => encryptSignAndUploadImage({ image, keypair, url: `${apiUrl}/upload` })) + .then(result => { + if (result) { + const { id, ...image } = result; + this.setState({ + uploadedImage: image, + imageIdToDownload: id + }); + } + }) + .catch(err => { + console.log('Error while uploading: ', err); + this.setState({ error: err.toString() }); + }); + }; + + handleDownload = () => { + downloadVerifyAndDecryptImage({ + url: `${apiUrl}/upload/${this.state.imageIdToDownload}`, + keypair, + extension: getFileExtension(this.state.uploadedImage.fileName) + }) + .then(image => { + this.setState({ downloadedImage: image }); + }) + .catch(err => { + console.log('Error while downloading: ', err); + this.setState({ error: err.toString() }); + }); + } + + reset = () => { + this.setState({ + uploadedImage: null, + imageIdToDownload: null, + downloadedImage: null, + error: null + }); + } + + renderUploadedImage = () => { + const { uploadedImage } = this.state; + if (!uploadedImage) { + return null; + } + + return this.renderObject(uploadedImage); + } + + renderDownloadedImage = () => { + const { downloadedImage } = this.state; + if (!downloadedImage) { + return null; + } + + if (!downloadedImage.isSigantureVerified) { + return ( + { + 'The downloaded image cannot be displayed because its digital signature failed verification' + } + ); + } + + return this.renderImage(downloadedImage); + } + + renderObject(p) { + function objectToText(obj, indent = 0) { + return Object.keys(obj).map(k => { + if (typeof obj[k] === 'object') { + return objectToText(obj[k], indent + 4); + } + return `${' '.repeat(indent)}${k}: ${obj[k]}`; + }).flat(); + } + + let text = objectToText(p); + return ( + + {text.map((t, i) => ({t}))} + + ); + } + + renderImage(p) { + return ( + + ); + } + + render() { + return ( + + + +