diff --git a/README.md b/README.md index 73fd04b..334167e 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,102 @@ const decryptedMessage = decryptedData.toString('utf8'); ### File encryption -Coming soon. +To encrypt a file you will need to know its location in the file system. For images you can use [React Native API](https://facebook.github.io/react-native/docs/cameraroll.html), or a library such as [react-native-image-picker](https://github.com/react-native-community/react-native-image-picker) or [react-native-camera-roll-picker](https://github.com/jeanpan/react-native-camera-roll-picker). + +```javascript +import { virgilCrypto } from 'react-native-virgil-crypto'; + +const keypair = virgilCrypto.generateKeys(); + +// this must be defined in your code +pickAnImage() +.then(image => { + return virgilCrypto.encryptFile({ + // assuming `image` has a `uri` property that points to its location in file system + inputPath: image.uri, + // This can be a custom path that your application can write to + // e.g. RNFetchBlob.fs.dirs.DocumentDir + '/encrypted_uploads/' + image.fileName, + // If not specified, a temporary file will be created. + outputPath: undefined, + publicKeys: keypair.publicKey + }) + .then(encryptedFilePath => { + // encryptedFilePath is the location of the encrypted file in the file system + // the original image file remain intact + // you can now upload this file using `fetch` and `FormData`, e.g.: + const data = new FormData(); + data.append('photo', { + uri: 'file://' + encryptedFilePath, + type: image.type, + name: image.fileName + }); + + return fetch(url, { method: 'POST', body: data }); + }); +}); +``` + +Decryption works similarly to encryption - you provide a path to the encrypted file in the file system (network urls are not supported). You can use a library such as [rn-fetch-blob](https://github.com/joltup/rn-fetch-blob) or [react-native-fs](https://github.com/itinance/react-native-fs) to download a file directly into file system. + +```javascript +import { virgilCrypto } from 'react-native-virgil-crypto'; + +const keypair = virgilCrypto.generateKeys(); + +// this must be defined in your code +downloadImage() +.then(downloadedFilePath => { + return virgilCrypto.decryptFile({ + inputPath: downloadedFilePath, + // This can be a custom path that your application can write to + // e.g. RNFetchBlob.fs.dirs.DocumentDir + '/decrypted_downloads/' + image.id + '.jpg'; + // If not specified, a temporary file will be created + outputPath: undefined, + privateKey: keypair.privateKey + }) + .then(decryptedFilePath => { + return + }); +}); +``` + +It is also possible to calculate the digital signature of a file + +```javascript +import { virgilCrypto } from 'react-native-virgil-crypto'; + +const keypair = virgilCrypto.generateKeys(); +// this must be defined in your code +pickAnImage() +.then(image => { + return virgilCrypto.generateFileSignature({ + inputPath: image.uri, + privateKey: keypair.privateKey + }) + .then(signature => ({ ...image, signature: signature.toString('base64') })); +}); +``` + +And verify the signature of a file + +```javascript +import { virgilCrypto } from 'react-native-virgil-crypto'; + +const keypair = virgilCrypto.generateKeys(); + +// this must be defined in your code +downloadImage() +.then(downloadedFilePath => { + return virgilCrypto.verifyFileSignature({ + inputPath: image.downloadedFilePath, + signature: image.signature, + publicKey: keypair.publicKey + }) + .then(isSigantureVerified => ({ ...image, isSigantureVerified })); +}); +``` + +See the [sample project](/examples/FileEnryptionSample) for a complete example of working with encrypted files. ### Working with binary data @@ -112,7 +207,7 @@ VirgilCrypto supports two options for dependency management: Add the following line to your `Podfile`: ```sh - pod 'VirgilCrypto', '~> 5.0.0' + pod 'VirgilCrypto', '~> 5.1.0' ``` Make sure you have `use_frameworks!` there as well. Then run `pod install` from inside the `ios` directory. @@ -125,10 +220,10 @@ VirgilCrypto supports two options for dependency management: With [Carthage](https://github.com/Carthage/Carthage) add the following line to your `Cartfile`: ``` - github "VirgilSecurity/virgil-crypto-x" ~> 5.0.0 + github "VirgilSecurity/virgil-crypto-x" ~> 5.1.0 ``` - Then run `carthage update --plaform iOS` from inside the `ios` folder. + Then run `carthage update --platform iOS` from inside the `ios` folder. On your application target's “General” settings tab, in the “Linked Frameworks and Libraries” section, add following frameworks from the *Carthage/Build* folder inside your project's folder: 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..76734a8 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,41 @@ 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; +import com.virgilsecurity.sdk.crypto.exceptions.SigningException; +import com.virgilsecurity.sdk.crypto.exceptions.VerificationException; public class RNVirgilCryptoModule extends ReactContextBaseJavaModule { @@ -30,10 +45,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 @@ -174,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); @@ -188,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); @@ -219,6 +243,169 @@ public WritableMap generateRandomData(Integer size) { return ResponseFactory.createStringResponse(Encodings.encodeBase64(randomData)); } + @ReactMethod + public void encryptFile(final String inputPath, String outputPath, 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 resolvedOutputPath; + if (outputPath == null) { + resolvedOutputPath = FS.getTempFilePath(FS.getFileExtension(inputPath)); + } else { + resolvedOutputPath = outputPath; + } + + final VirgilCrypto vc = this.crypto; + + threadPool.execute(new Runnable() { + @Override + public void run() { + try ( + InputStream inStream = FS.getInputStreamFromPath(inputPath); + OutputStream outStream = FS.getOutputStreamFromPath(resolvedOutputPath) + ) { + vc.encrypt(inStream, outStream, publicKeys); + promise.resolve(resolvedOutputPath); + } 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(final String inputPath, String outputPath, 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 resolvedOutputPath; + if (outputPath == null) { + resolvedOutputPath = FS.getTempFilePath(FS.getFileExtension(inputPath)); + } else { + resolvedOutputPath = outputPath; + } + + 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(resolvedOutputPath) + ) { + vc.decrypt(inStream, outStream, privateKey); + promise.resolve(resolvedOutputPath); + } 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()); + } + } + }); + } + + @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()) { 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..abfcb40 --- /dev/null +++ b/android/src/main/java/com/virgilsecurity/rn/crypto/utils/FS.java @@ -0,0 +1,122 @@ +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) { + 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; + } +} 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()); + } + +} 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 ( + + + +