Skip to content

Commit

Permalink
Merge pull request #2 from VirgilSecurity/file-encryption
Browse files Browse the repository at this point in the history
Encrypting, decrypting, signing and verifying files
  • Loading branch information
vadimavdeev authored Aug 26, 2019
2 parents ad3f646 + 4ee85d1 commit 9facdd2
Show file tree
Hide file tree
Showing 83 changed files with 11,656 additions and 18 deletions.
103 changes: 99 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Image src={`file://${decryptedFilePath}`} />
});
});
```

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

Expand Down Expand Up @@ -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.
Expand All @@ -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:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,63 @@

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 {

private final ReactApplicationContext reactContext;
private final VirgilCrypto crypto;

public static ReactApplicationContext RCTContext;
private static LinkedBlockingQueue<Runnable> 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
Expand Down Expand Up @@ -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<VirgilPublicKey> publicKeys = this.decodeAndImportPublicKeys(recipientsBase64);
Expand All @@ -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<VirgilPublicKey> publicKeys = this.decodeAndImportPublicKeys(sendersPublicKeysBase64);
Expand Down Expand Up @@ -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<VirgilPublicKey> 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<VirgilPublicKey> decodeAndImportPublicKeys(ReadableArray publicKeysBase64) throws CryptoException {
List<VirgilPublicKey> publicKeys = new ArrayList<>(publicKeysBase64.size());
for(Object publicKeyBase64 : publicKeysBase64.toArrayList()) {
Expand Down
Loading

0 comments on commit 9facdd2

Please sign in to comment.