This demo shows how to use the Capillary library to send end-to-end (E2E) encrypted messages from an application server to Android clients over Firebase cloud messaging (FCM). The application server is implemented as a java gRPC server. The demo supports all android versions starting from KitKat 4.4 (API level 19).
Please follow the following instructions to setup and run the demo.
$ git clone https://github.com/google/capillary.git
$ cd capillary/demo
-
Create a Firebase Android project for the package name
com.google.capillary.demo.android
(instructions). -
Download the
google-services.json
file (instructions). -
Copy the
google-services.json
file toandroid
directory. -
Generate and download a private key for the Firebase service account (instructions).
-
Rename the downloaded private key as
service-account.json
and copy it toresources/firebase
directory. -
Enable the Firebase Cloud Messaging API on the Google API Console
-
Make note of the Project ID in the Firebase project settings page. This ID is referred to as
<FIREBASE_PROJECT_ID>
in the instructions below.
Use the provided utility program to generate an ECDSA key pair:
$ cd ..
$ ./gradlew tools:installDist
$ ./tools/build/install/tools/bin/ecdsa-key-pair-generator \
> --ecdsa_public_key_path=demo/android/src/main/res/raw/sender_verification_key.dat \
> --ecdsa_private_key_path=demo/resources/ecdsa/sender_signing_key.dat
$ cd demo
The demo supports both RSA-based and Elliptic Curve (EC)-based TLS keys. To generate EC-based TLS keys:
-
Add the hostname of the server in which the demo application server will be run in the
[test_sans]
section of theresources/tls/init_tls.cnf
file. Note that this file already contains the hostnames that are typically used for local development (127.0.0.1
forlocalhost
, and10.0.2.2
for connecting tolocalhost
from an emulated Android device more info). -
Generate TLS keys:
$ cd resources/tls
$ openssl req -x509 -days 365 -nodes -newkey ec:<(openssl ecparam -name prime256v1) \
> -keyout tls_tmp.key -out tls.crt -config init_tls.cnf
$ openssl pkcs8 -topk8 -nocrypt -in tls_tmp.key -out tls.key
$ rm tls_tmp.key
$ cp tls.crt ../../android/src/main/res/raw
$ cd ../..
See the instructions in resources/tls/init_tls.cnf
to generate
RSA-based TLS keys.
Use the provided schema file to create a demo SQLite database:
$ cd resources/sqlite
$ sqlite3 demo.db -init demo-schema.sql
sqlite> .quit
$ cd ../..
First, bundle the server into a tarball:
$ ../gradlew server:distTar
At the end of the above process, you will see the tarball of the demo application server at
server/build/distributions/server.tar
. Next, copy that tarball and the resources
directory to a
directory in the server in which the demo application server will be run. Finally, run the server:
$ tar -xf server.tar
$ ./server/bin/server \
> --port=8443 \
> --firebase_project_id=<FIREBASE_PROJECT_ID> \
> --service_account_credentials_path=resources/firebase/service-account.json \
> --ecdsa_private_key_path=resources/ecdsa/sender_signing_key.dat \
> --tls_cert_path=resources/tls/tls.crt \
> --tls_private_key_path=resources/tls/tls.key \
> --database_path=resources/sqlite/demo.db
$ ../gradlew android:assembleDebug
$ adb install android/build/outputs/apk/debug/android-debug.apk
-
Type the hostname (1) and port (2) of the demo application server and click "connect" (3).
-
Click "log token" (1) to see the current FCM token.
-
Click "reg user" (1) to generate generate a unique ID that is specific to the current installation of the demo Android app, and register that ID along with the FCM token at the demo application server.
-
Click "del iid" (1) to delete the current FCM instance ID (IID). This results in the FCM SDK generating a new IID and an FCM token for the demo Android app instance. Observe that the demo Android app automatically registers the newly generated FCM token at the demo application server.
-
Select the algorithm (1) and authentication (2), and click "gen key" (3) to generate a new Capillary key pair.
The interaction with the Capillary library to generate keys can be summarized as:
Context context = ... // The current app context. String keychainId = ... // Some identifier for the key pair. boolean isAuth = ... // Whether the private key usage should be guarded by the device lock. // To generate an RSA-ECDSA key pair. InputStream senderVerificationKey = ... // The ECDSA verification key of the server. RsaEcdsaKeyManager.getInstance(context, keychainId, senderVerificationKey).generateKeyPair(isAuth); // To generate a Web Push key pair. WebPushKeyManager.getInstance(context, keychainId).generateKeyPair(isAuth);
-
Click "reg key" (1) to register the generated public key with the demo application server.
The "reg key" operation consists of two steps:
-
Obtain the public key from the Capillary library. This step can be summarized as:
Context context = ... // The current app context. String keychainId = ... // The identifier for the key pair. boolean isAuth = ... // Whether the private key usage is guarded by the device lock. CapillaryHandler handler = ... // An implementation of CapillaryHandler interface. Object extra = ... // Any extra information to be passed back to the handler. // To obtain an RSA-ECDSA public key. InputStream senderVerificationKey = ... // The ECDSA verification key of the server. RsaEcdsaKeyManager.getInstance(context, keychainId, senderVerificationKey) .getPublicKey(isAuth, handler, extra); // To obtain a Web Push public key. WebPushKeyManager.getInstance(context, keychainId).getPublicKey(isAuth, handler, extra); // The Capillary library returns a byte array representing the Capillary public key via the // handlePublicKey method of the CapillaryHandler instance.
-
Send that public key to the demo application server.
-
-
Click "req message" (1) to have the demo application server send an E2E-encrypted message to the demo Android app.
The "req message" operation consists of the following steps:
-
(on client) Request the demo application server to send an E2E-encrypted message.
-
(on server) Generate the E2E-encrypted message as a byte array (ciphertext). This step can be summarized as:
byte[] recipientPublicKey = ... // The Capillary public key of the client. byte[] message = ... // The message to be sent to the client. // To create an RSA-ECDSA ciphertext. InputStream senderSigningKey = ... // The ECDSA signing key of the server. EncrypterManager rsaEcdsaEncrypterManager = new RsaEcdsaEncrypterManager(senderSigningKey); rsaEcdsaEncrypterManager.loadPublicKey(recipientPublicKey); byte[] ciphertext = rsaEcdsaEncrypterManager.encrypt(message); rsaEcdsaEncrypterManager.clearPublicKey(); // To create a Web Push ciphertext. EncrypterManager webPushEncrypterManager = new WebPushEncrypterManager(); webPushEncrypterManager.loadPublicKey(recipientPublicKey); byte[] ciphertext = webPushEncrypterManager.encrypt(message); webPushEncrypterManager.clearPublicKey();
-
(on server) send that ciphertext to the demo Android app via FCM.
-
(on client) decrypt the received ciphertext using the Capillary library. This step can be summarized as:
byte[] ciphertext = ... // The ciphertext received through FCM. Context context = ... // The current app context. String keychainId = ... // The identifier for the key pair. CapillaryHandler handler = ... // An implementation of CapillaryHandler interface. Object extra = ... // Any extra information to be passed back to the handler. // To decrypt an RSA-ECDSA ciphertext. InputStream senderVerificationKey = ... // The ECDSA verification key of the server. RsaEcdsaKeyManager.getInstance(context, keychainId, senderVerificationKey) .getDecrypterManager().decrypt(ciphertext, handler, extra); // To decrypt a Web Push ciphertext. WebPushKeyManager.getInstance(context, keychainId) .getDecrypterManager().decrypt(ciphertext, handler, extra); // The Capillary library returns a byte array representing the plaintext via the handleData // method of the CapillaryHandler instance.
-
Display the decrypted message as an notification.
-
One of the main features of the Capillary library is to generate authenticated crypto keys, and guard the usage of such private keys with a device lock. To try authenticated keys in the demo Android app:
-
Make sure the device has a screen lock enabled.
-
Select an algorithm (1), select "IsAuth" as "true" (2), click "gen key" (3), and click "reg key" (4). This will generate a new authenticated key pair on the device and register the newly generated public key with the demo application server.
-
Request a ciphertext to be sent after some delay. To do so, input a short delay in seconds (1), say 10 seconds, and click "req message" (2).
-
Lock the device within the delay that we specified in the previous step.
-
Observe that no demo notification appears in the lock screen after the specified delay has passed.
-
Unlock the device, and observe that the demo notification appears right after.
What happens behind the scenes after pressing "req message" is the following:
-
The demo Android app requests the demo application server to send an E2E-encrypted message created with the newly generated public key.
-
The demo application server creates the ciphertext and sends it over FCM after the specified delay.
-
Notice that the device is now locked. Upon receiving the ciphertext over FCM, the demo Android app calls the
decrypt
method of the associatedDecrypterManager
. -
The Capillary library notices that the privated key that is required to decrypt the ciphertext is authenticated. So, the library saves the ciphertext in the demo Android app's local storage to be decrypted later, and notifies the demo Android app by calling the
authCiphertextSavedForLater
method of the suppliedCapillaryHandler
instance. Notice thatDemoCapillaryHandler
, the demo Android app's implementation ofCapillaryHandler
, merely logs any call to itsauthCiphertextSavedForLater
method. But, in real applications, the developers may want to do other work in this method, e.g., notifying the user that there are encrypted messages waiting to be decrypted upon device unlock. -
The demo Android app waits for the device unlock event using a
BroadcastReceiver
namedDeviceUnlockedBroadcastReceiver
. And, after the user unlocks the device, the demo Android app requests the Capillary library to decrypt any saved ciphertexts. This request to decrypt saved ciphertexts can be summarized as:Context context = ... // The current app context. String keychainId = ... // The identifier for the key pair. CapillaryHandler handler = ... // An implementation of CapillaryHandler interface. Object extra = ... // Any extra information to be passed back to the handler. // To decrypt saved RSA-ECDSA ciphertexts. InputStream senderVerificationKey = ... // The ECDSA verification key of the server. RsaEcdsaKeyManager.getInstance(context, keychainId, senderVerificationKey) .getDecrypterManager().decryptSaved(handler, extra); // To decrypt saved Web Push ciphertexts. WebPushKeyManager.getInstance(context, keychainId) .getDecrypterManager().decryptSaved(handler, extra); // For each decrypted ciphertext, the Capillary library returns a byte array representing the // plaintext via the handleData method of the CapillaryHandler instance.
-
The demo Android app then displays the plaintext as a notification.
The Capillary library can resolve most errors with minimal interaction with the client Android apps that use the library. One such error is the corruption of Capillary crypto keys due to the user clearing the app storage, bugs in the Android KeyStore, etc. To try this out, do the following:
-
Generate a new key pair (1, 2, 3) and register (4) the public key with the demo application server.
-
Delete (1) the key pair from the demo Android app.
-
Request an E2E-encrypted message (1) from the demo application server, and observe that a demo notification appears even though the key pair generated earlier has been deleted.
So, what happens behind the scenes?
-
Upon receiving the ciphertext over FCM, the demo Android app calls the
decrypt
method of the associatedDecrypterManager
. -
The Capillary library then notices that the private key required to decrypt the ciphertext is missing (or corrupted). Since there is no way to recover that private key, the library then generates a new key pair with the same algorithm and authentication parameters of the missing key pair.
-
Next, the library supplies the newly generated public key to the demo Android app by calling the overloaded
handlePublicKey
method of the suppliedCapillaryHandler
instance. -
The
DemoCapillaryHandler
, the demo Android app's implementation ofCapillaryHandler
, registers that public key with the demo application server and also requests a new E2E-encrypted message to be sent. -
The demo Android app receives that ciphertext, and passes it to the Capillary library to decrypt.
-
The Capillary library successfully decrypts that ciphertext and passes the plaintext back to the demo Android app, which displays the plaintext as a notification.