├── demo
├── resources
│ ├── ecdsa
│ │ └── .gitkeep
│ ├── firebase
│ │ └── .gitkeep
│ ├── tls
│ │ └── init_tls.cnf
│ └── sqlite
│ │ └── demo-schema.sql
├── android
│ ├── src
│ │ └── main
│ │ │ ├── res
│ │ │ ├── raw
│ │ │ │ └── .gitkeep
│ │ │ ├── values
│ │ │ │ ├── styles.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── google
│ │ │ │ └── capillary
│ │ │ │ └── demo
│ │ │ │ └── android
│ │ │ │ ├── AndroidConstants.java
│ │ │ │ ├── callables
│ │ │ │ ├── LogToken.java
│ │ │ │ ├── DelIid.java
│ │ │ │ ├── GenKey.java
│ │ │ │ ├── DelKey.java
│ │ │ │ ├── DecryptSavedCiphertexts.java
│ │ │ │ ├── RegUser.java
│ │ │ │ ├── RegKey.java
│ │ │ │ └── RequestMessage.java
│ │ │ │ ├── DeviceUnlockedBroadcastReceiver.java
│ │ │ │ ├── DemoFiidService.java
│ │ │ │ ├── DemoFmService.java
│ │ │ │ ├── TlsOkHttpChannelGenerator.java
│ │ │ │ └── DemoCapillaryHandler.java
│ │ │ └── AndroidManifest.xml
│ ├── lint.xml
│ └── build.gradle
├── common
│ ├── build.gradle
│ ├── src
│ │ └── main
│ │ │ └── java
│ │ │ └── com
│ │ │ └── google
│ │ │ └── capillary
│ │ │ └── demo
│ │ │ └── common
│ │ │ └── Constants.java
│ └── proto
│ │ └── capillary_demo_common.proto
├── img
│ ├── connect.png
│ ├── del_iid.png
│ ├── del_key.png
│ ├── gen_key.png
│ ├── reg_key.png
│ ├── auth_key.png
│ ├── log_token.png
│ ├── reg_user.png
│ ├── regen_del.png
│ ├── regen_reg.png
│ ├── regen_req.png
│ ├── req_message.png
│ └── req_auth_message.png
└── server
│ ├── src
│ └── main
│ │ └── java
│ │ └── com
│ │ └── google
│ │ └── capillary
│ │ └── demo
│ │ └── server
│ │ ├── NoSuchUserException.java
│ │ ├── DemoDb.java
│ │ ├── FcmSender.java
│ │ ├── DemoServiceImpl.java
│ │ └── DemoServer.java
│ └── build.gradle
├── img
├── no_e2ee.png
└── with_capillary.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── lib-android
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── google
│ │ │ └── capillary
│ │ │ └── android
│ │ │ ├── CapillaryHandlerErrorCode.java
│ │ │ ├── CiphertextStorage.java
│ │ │ ├── CapillaryHandler.java
│ │ │ ├── Utils.java
│ │ │ ├── RsaEcdsaKeyManager.java
│ │ │ ├── AndroidKeyStoreRsaUtils.java
│ │ │ └── DecrypterManager.java
│ ├── androidTest
│ │ ├── resources
│ │ │ └── com
│ │ │ │ └── google
│ │ │ │ └── capillary
│ │ │ │ └── android
│ │ │ │ ├── signing_key.dat
│ │ │ │ └── verification_key.dat
│ │ └── java
│ │ │ └── com
│ │ │ └── google
│ │ │ └── capillary
│ │ │ └── android
│ │ │ ├── TestUtils.java
│ │ │ ├── CiphertextStorageAndroidTest.java
│ │ │ ├── AndroidKeyStoreRsaUtilsAndroidTest.java
│ │ │ └── TestHandler.java
│ └── test
│ │ └── java
│ │ └── com
│ │ └── google
│ │ └── capillary
│ │ └── android
│ │ ├── TestUtils.java
│ │ ├── CiphertextStorageTest.java
│ │ └── KeyManagerTest.java
├── publish.gradle
└── build.gradle
├── settings.gradle
├── lib
├── src
│ ├── test
│ │ ├── resources
│ │ │ └── com
│ │ │ │ └── google
│ │ │ │ └── capillary
│ │ │ │ ├── signing_key.dat
│ │ │ │ ├── ec_public_key.dat
│ │ │ │ ├── rsa_private_key.dat
│ │ │ │ ├── rsa_public_key.dat
│ │ │ │ └── verification_key.dat
│ │ └── java
│ │ │ └── com
│ │ │ └── google
│ │ │ └── capillary
│ │ │ ├── HybridRsaUtilsTest.java
│ │ │ ├── WebPushEncrypterManagerTest.java
│ │ │ ├── RsaEcdsaEncrypterManagerTest.java
│ │ │ ├── TestUtils.java
│ │ │ ├── RsaEcdsaHybridEncryptTest.java
│ │ │ ├── EncrypterManagerTest.java
│ │ │ └── RsaEcdsaHybridDecryptTest.java
│ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── google
│ │ │ └── capillary
│ │ │ ├── CapillaryException.java
│ │ │ ├── NoSuchKeyException.java
│ │ │ ├── AuthModeUnavailableException.java
│ │ │ ├── Config.java
│ │ │ ├── WebPushEncrypterManager.java
│ │ │ ├── RsaEcdsaConstants.java
│ │ │ ├── RsaEcdsaEncrypterManager.java
│ │ │ ├── EncrypterManager.java
│ │ │ └── HybridRsaUtils.java
│ │ └── proto
│ │ └── capillary_internal.proto
├── build.gradle
└── publish.gradle
├── gradle.properties
├── .gitignore
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── tools
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── google
│ └── capillary
│ └── tools
│ └── EcdsaKeyPairGenerator.java
├── config
└── checkstyle
│ └── java.header
├── CONTRIBUTING.md
├── gradlew.bat
└── gradlew
/demo/resources/ecdsa/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/resources/firebase/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/raw/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/common/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java-library'
2 |
--------------------------------------------------------------------------------
/img/no_e2ee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/img/no_e2ee.png
--------------------------------------------------------------------------------
/demo/img/connect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/connect.png
--------------------------------------------------------------------------------
/demo/img/del_iid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/del_iid.png
--------------------------------------------------------------------------------
/demo/img/del_key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/del_key.png
--------------------------------------------------------------------------------
/demo/img/gen_key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/gen_key.png
--------------------------------------------------------------------------------
/demo/img/reg_key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/reg_key.png
--------------------------------------------------------------------------------
/demo/img/auth_key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/auth_key.png
--------------------------------------------------------------------------------
/demo/img/log_token.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/log_token.png
--------------------------------------------------------------------------------
/demo/img/reg_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/reg_user.png
--------------------------------------------------------------------------------
/demo/img/regen_del.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/regen_del.png
--------------------------------------------------------------------------------
/demo/img/regen_reg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/regen_reg.png
--------------------------------------------------------------------------------
/demo/img/regen_req.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/regen_req.png
--------------------------------------------------------------------------------
/img/with_capillary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/img/with_capillary.png
--------------------------------------------------------------------------------
/demo/img/req_message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/req_message.png
--------------------------------------------------------------------------------
/demo/img/req_auth_message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/img/req_auth_message.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/demo/android/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/lib-android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':lib'
2 | include ':lib-android'
3 | include ':tools'
4 | include ':demo:common'
5 | include ':demo:android'
6 | include ':demo:server'
7 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/lib/src/test/resources/com/google/capillary/signing_key.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/lib/src/test/resources/com/google/capillary/signing_key.dat
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/demo/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/lib/src/test/resources/com/google/capillary/ec_public_key.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/lib/src/test/resources/com/google/capillary/ec_public_key.dat
--------------------------------------------------------------------------------
/lib/src/test/resources/com/google/capillary/rsa_private_key.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/lib/src/test/resources/com/google/capillary/rsa_private_key.dat
--------------------------------------------------------------------------------
/lib/src/test/resources/com/google/capillary/rsa_public_key.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/lib/src/test/resources/com/google/capillary/rsa_public_key.dat
--------------------------------------------------------------------------------
/lib/src/test/resources/com/google/capillary/verification_key.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/lib/src/test/resources/com/google/capillary/verification_key.dat
--------------------------------------------------------------------------------
/lib-android/src/androidTest/resources/com/google/capillary/android/signing_key.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/lib-android/src/androidTest/resources/com/google/capillary/android/signing_key.dat
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # Specifies the JVM arguments used for the daemon process.
4 | # The setting is particularly useful for tweaking memory settings.
5 | org.gradle.jvmargs=-Xmx1536m
6 |
--------------------------------------------------------------------------------
/lib-android/src/androidTest/resources/com/google/capillary/android/verification_key.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/google/capillary/HEAD/lib-android/src/androidTest/resources/com/google/capillary/android/verification_key.dat
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Apr 11 15:32:36 PDT 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .DS_Store
3 | local.properties
4 | .gradle/
5 | .idea/
6 | build/
7 |
8 | # Resources used by Capillary demo.
9 | /demo/android/google-services.json
10 | /demo/android/src/main/res/raw/sender_verification_key.dat
11 | /demo/android/src/main/res/raw/tls.crt
12 | /demo/resources/ecdsa/sender_signing_key.dat
13 | /demo/resources/firebase/service-account.json
14 | /demo/resources/sqlite/demo.db
15 | /demo/resources/tls/tls.crt
16 | /demo/resources/tls/tls.key
17 |
18 |
--------------------------------------------------------------------------------
/demo/android/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/tools/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'application'
2 |
3 | dependencies {
4 | implementation project(':lib')
5 | implementation 'commons-cli:commons-cli:1.4'
6 | implementation 'com.google.crypto.tink:tink:1.1.0'
7 | }
8 |
9 | startScripts.enabled = false
10 |
11 | task ecdsaKeyPairGenerator(type: CreateStartScripts) {
12 | mainClassName = 'com.google.capillary.tools.EcdsaKeyPairGenerator'
13 | applicationName = 'ecdsa-key-pair-generator'
14 | classpath = startScripts.classpath
15 | outputDir = startScripts.outputDir
16 | }
17 |
18 | applicationDistribution.into('bin') {
19 | from(ecdsaKeyPairGenerator)
20 | fileMode = 0755
21 | }
22 |
--------------------------------------------------------------------------------
/config/checkstyle/java.header:
--------------------------------------------------------------------------------
1 | /\*$
2 | \* Copyright (2018 Google LLC|\(C\) 2010 The Android Open Source Project)$
3 | \*$
4 | \* Licensed under the Apache License, Version 2\.0 \(the "License"\);$
5 | \* you may not use this file except in compliance with the License\.$
6 | \* You may obtain a copy of the License at$
7 | \*$
8 | \* https://www\.apache\.org/licenses/LICENSE-2\.0$
9 | \*$
10 | \* Unless required by applicable law or agreed to in writing, software$
11 | \* distributed under the License is distributed on an "AS IS" BASIS,$
12 | \* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.$
13 | \* See the License for the specific language governing permissions and$
14 | \* limitations under the License\.$
15 | \*/$
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Desktop (please complete the following information):**
24 | - OS: [e.g. iOS]
25 | - Browser [e.g. chrome, safari]
26 | - Version [e.g. 22]
27 |
28 | **Smartphone (please complete the following information):**
29 | - Device: [e.g. iPhone6]
30 | - OS: [e.g. iOS8.1]
31 | - Browser [e.g. stock browser, safari]
32 | - Version [e.g. 22]
33 |
34 | **Additional context**
35 | Add any other context about the problem here.
36 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Capillary Demo
3 | Messages
4 | Capillary Demo Messages
5 |
6 | Host
7 | 10.0.2.2
8 | Port
9 | 8443
10 | Connect
11 | Disconnect
12 | Log Token
13 | Del IID
14 | Reg User
15 | Algorithm
16 | IsAuth
17 | Gen Key
18 | Del Key
19 | Reg Key
20 | delay (s)
21 | Req Message
22 |
23 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/AndroidConstants.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android;
18 |
19 | /**
20 | * Contains the common constants used by the Android classes.
21 | */
22 | final class AndroidConstants {
23 |
24 | static final String RSA_ECDSA_KEYCHAIN_ID = "rsa_ecdsa_keychain";
25 | static final String WEB_PUSH_KEYCHAIN_ID = "web_push_keychain";
26 | }
27 |
--------------------------------------------------------------------------------
/demo/common/src/main/java/com/google/capillary/demo/common/Constants.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.common;
18 |
19 | /**
20 | * Contains the common constants used by the Android and server apps.
21 | */
22 | public class Constants {
23 |
24 | public static final String CAPILLARY_CIPHERTEXT_KEY = "capillary_ciphertext";
25 | public static final String CAPILLARY_KEY_ALGORITHM_KEY = "capillary_key_algorithm";
26 | }
27 |
--------------------------------------------------------------------------------
/demo/resources/tls/init_tls.cnf:
--------------------------------------------------------------------------------
1 | # 1.1 Generate RSA {cert, key} pair:
2 | # openssl req -x509 -days 365 -nodes -newkey rsa:2048 -keyout tls_rsa_tmp.key -out tls_rsa.crt -config init_tls.cnf
3 | # 1.2 Convert RSA key to pkcs8 format:
4 | # openssl pkcs8 -topk8 -nocrypt -in tls_rsa_tmp.key -out tls_rsa.key
5 | # 1.3 View RSA cert:
6 | # openssl x509 -text -noout -in tls_rsa.crt
7 | #
8 | # OR
9 | #
10 | # 2.1 Generate EC (cert, key) pair:
11 | # openssl req -x509 -days 365 -nodes -newkey ec:<(openssl ecparam -name prime256v1) -keyout tls_ec_tmp.key -out tls_ec.crt -config init_tls.cnf
12 | # 2.2 Convert RSA key to pkcs8 format:
13 | # openssl pkcs8 -topk8 -nocrypt -in tls_ec_tmp.key -out tls_ec.key
14 | # 2.3 View EC cert:
15 | # openssl x509 -text -noout -in tls_ec.crt
16 |
17 | prompt = no
18 |
19 | [req]
20 | distinguished_name = test_distinguished_name
21 | x509_extensions = test_x509_extensions
22 |
23 | [test_distinguished_name]
24 | commonName = localhost
25 |
26 | [test_x509_extensions]
27 | subjectAltName=@test_sans
28 |
29 | [test_sans]
30 | IP.1 = 10.0.2.2
31 | IP.2 = 127.0.0.1
32 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/google/capillary/CapillaryException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | /**
20 | * Base checked exception class for Capillary-related errors.
21 | */
22 | public class CapillaryException extends Exception {
23 |
24 | /**
25 | * Creates a new {@link CapillaryException} instance.
26 | *
27 | * @param msg the exception message.
28 | */
29 | public CapillaryException(String msg) {
30 | super(msg);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/google/capillary/NoSuchKeyException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | /**
20 | * A checked exception to notify that a key is missing.
21 | */
22 | public final class NoSuchKeyException extends CapillaryException {
23 |
24 | /**
25 | * Creates a new {@link NoSuchKeyException} object.
26 | *
27 | * @param msg the exception message.
28 | */
29 | public NoSuchKeyException(String msg) {
30 | super(msg);
31 | }
32 | }
--------------------------------------------------------------------------------
/demo/server/src/main/java/com/google/capillary/demo/server/NoSuchUserException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.server;
18 |
19 | /**
20 | * A checked exception to notify that a user ID is missing.
21 | */
22 | final class NoSuchUserException extends Exception {
23 |
24 | /**
25 | * Creates a new {@link NoSuchUserException} object.
26 | *
27 | * @param msg the exception message.
28 | */
29 | NoSuchUserException(String msg) {
30 | super(msg);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/demo/resources/sqlite/demo-schema.sql:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | /*
18 | To create initial SQLite database, run the following in command line:
19 | sqlite3 demo.db -init demo-schema.sql
20 | */
21 |
22 | CREATE TABLE PublicKeys (
23 | user_id TEXT NOT NULL,
24 | algorithm INTEGER NOT NULL,
25 | is_auth INTEGER NOT NULL,
26 | key_bytes BLOB NOT NULL,
27 | PRIMARY KEY(user_id, algorithm, is_auth)
28 | );
29 |
30 | CREATE TABLE Users (
31 | user_id TEXT NOT NULL,
32 | token TEXT NOT NULL,
33 | PRIMARY KEY(user_id)
34 | );
35 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/callables/LogToken.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android.callables;
18 |
19 | import com.google.firebase.iid.FirebaseInstanceId;
20 | import java.util.concurrent.Callable;
21 |
22 | /**
23 | * Returns the current FCM token.
24 | */
25 | public final class LogToken implements Callable {
26 |
27 | @Override
28 | public String call() throws Exception {
29 | return FirebaseInstanceId.getInstance().getToken();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/google/capillary/AuthModeUnavailableException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | /**
20 | * Indicates that authenticated keys are not supported.
21 | */
22 | public final class AuthModeUnavailableException extends CapillaryException {
23 |
24 | /**
25 | * Creates a new {@link AuthModeUnavailableException} instance.
26 | *
27 | * @param msg the exception message.
28 | */
29 | public AuthModeUnavailableException(String msg) {
30 | super(msg);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows [Google's Open Source Community
28 | Guidelines](https://opensource.google.com/conduct/).
29 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/callables/DelIid.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android.callables;
18 |
19 | import com.google.firebase.iid.FirebaseInstanceId;
20 | import java.util.concurrent.Callable;
21 |
22 | /**
23 | * Deletes the current FCM instance ID.
24 | */
25 | public final class DelIid implements Callable {
26 |
27 | @Override
28 | public String call() throws Exception {
29 | FirebaseInstanceId.getInstance().deleteInstanceId();
30 | FirebaseInstanceId.getInstance().getToken();
31 | return "deleted IID";
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib-android/src/androidTest/java/com/google/capillary/android/TestUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import java.security.GeneralSecurityException;
20 | import java.security.KeyStore;
21 | import java.util.Enumeration;
22 |
23 | final class TestUtils {
24 |
25 | static void clearKeyStore() throws GeneralSecurityException {
26 | // Clear existing keystore entries.
27 | KeyStore keyStore = Utils.getInstance().loadKeyStore();
28 | Enumeration aliases = keyStore.aliases();
29 | while (aliases.hasMoreElements()) {
30 | keyStore.deleteEntry(aliases.nextElement());
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | dependencies {
3 | classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.3'
4 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
5 | }
6 | }
7 |
8 | apply plugin: 'java-library'
9 | apply plugin: 'com.google.protobuf'
10 |
11 | dependencies {
12 | implementation 'com.google.crypto.tink:tink:1.1.0'
13 | implementation 'com.google.crypto.tink:apps-webpush:1.1.0'
14 | implementation 'com.google.protobuf:protobuf-java:3.4.0'
15 | testImplementation 'junit:junit:4.12'
16 | }
17 |
18 | protobuf {
19 | protoc {
20 | artifact = 'com.google.protobuf:protoc:3.0.0'
21 | }
22 | }
23 |
24 | task sourcesJar(type: Jar, dependsOn: classes) {
25 | classifier = 'sources'
26 | from sourceSets.main.allSource
27 | }
28 |
29 | task javadocJar(type: Jar, dependsOn: javadoc) {
30 | classifier = 'javadoc'
31 | from javadoc.destinationDir
32 | }
33 |
34 | artifacts {
35 | archives sourcesJar
36 | archives javadocJar
37 | }
38 |
39 | // Inform IntelliJ projects about the generated code.
40 | apply plugin: 'idea'
41 |
42 | idea {
43 | module {
44 | sourceDirs += file("${projectDir}/build/generated/source/proto/main/java")
45 | }
46 | }
47 |
48 | apply from: 'publish.gradle'
49 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/google/capillary/Config.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import com.google.crypto.tink.aead.AeadConfig;
20 | import com.google.crypto.tink.signature.SignatureConfig;
21 | import java.security.GeneralSecurityException;
22 |
23 | /**
24 | * Static methods to initialize Capillary library.
25 | */
26 | public final class Config {
27 |
28 | /**
29 | * Initializes the Capillary library.
30 | *
31 | *
This should be called before using any functionality provided by Capillary.
32 | *
33 | * @throws GeneralSecurityException if the initialization fails.
34 | */
35 | public static void initialize() throws GeneralSecurityException {
36 | com.google.crypto.tink.Config.register(SignatureConfig.TINK_1_1_0);
37 | com.google.crypto.tink.Config.register(AeadConfig.TINK_1_1_0);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib-android/src/main/java/com/google/capillary/android/CapillaryHandlerErrorCode.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | /**
20 | * These are the common errors that can occur in Capillary library.
21 | */
22 | public enum CapillaryHandlerErrorCode {
23 | // A ciphertext that was encrypted using an authenticated Capillary public key was received in a
24 | // device that does not have authentication enabled.
25 | AUTH_CIPHER_IN_NO_AUTH_DEVICE,
26 | // A malformed ciphertext was received.
27 | MALFORMED_CIPHERTEXT,
28 | // A ciphertext that was encrypted using an older Capillary public key was received. The client
29 | // should re-register the current Capillary public key with the application server to avoid
30 | // this error happening again.
31 | STALE_CIPHERTEXT,
32 | // An error other than the above has occurred.
33 | UNKNOWN_ERROR
34 | }
35 |
--------------------------------------------------------------------------------
/lib-android/src/test/java/com/google/capillary/android/TestUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import android.os.Build.VERSION;
20 | import java.lang.reflect.Field;
21 | import java.lang.reflect.Modifier;
22 |
23 | final class TestUtils {
24 |
25 | static void setBuildVersion(int version) throws Exception {
26 | Field field = VERSION.class.getDeclaredField("SDK_INT");
27 |
28 | Field modifiersField = Field.class.getDeclaredField("modifiers");
29 | boolean isModifierAccessible = modifiersField.isAccessible();
30 | modifiersField.setAccessible(true);
31 | modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
32 | modifiersField.setAccessible(isModifierAccessible);
33 |
34 | boolean isAccessible = field.isAccessible();
35 | field.setAccessible(true);
36 | field.set(null, version);
37 | field.setAccessible(isAccessible);
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/lib-android/src/test/java/com/google/capillary/android/CiphertextStorageTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import static org.mockito.Matchers.anyString;
20 | import static org.mockito.Matchers.eq;
21 | import static org.mockito.Mockito.mock;
22 | import static org.mockito.Mockito.times;
23 | import static org.mockito.Mockito.verify;
24 |
25 | import android.content.Context;
26 | import org.junit.Test;
27 | import org.junit.runner.RunWith;
28 | import org.junit.runners.JUnit4;
29 |
30 | @RunWith(JUnit4.class)
31 | public final class CiphertextStorageTest {
32 |
33 | @Test
34 | public void testStorageContextIsPrivate() {
35 | Context context = mock(Context.class);
36 |
37 | new CiphertextStorage(context, Utils.getInstance(), "id_1");
38 |
39 | // There should be two calls to getSharedPreferences with Context.MODE_PRIVATE.
40 | verify(context, times(2)).getSharedPreferences(anyString(), eq(Context.MODE_PRIVATE));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/demo/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/callables/GenKey.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android.callables;
18 |
19 | import com.google.capillary.android.KeyManager;
20 | import com.google.capillary.demo.common.KeyAlgorithm;
21 | import java.util.concurrent.Callable;
22 |
23 | /**
24 | * Generates a Capillary key pair.
25 | */
26 | public final class GenKey implements Callable {
27 |
28 | private final KeyManager keyManager;
29 | private final KeyAlgorithm algorithm;
30 | private final boolean isAuth;
31 |
32 | /**
33 | * Initializes a new {@link GenKey}.
34 | *
35 | * @param keyManager the key manager to use.
36 | * @param algorithm the algorithm of the generated key.
37 | * @param isAuth whether the generated key requires authenticated.
38 | */
39 | public GenKey(KeyManager keyManager, KeyAlgorithm algorithm, boolean isAuth) {
40 | this.keyManager = keyManager;
41 | this.algorithm = algorithm;
42 | this.isAuth = isAuth;
43 | }
44 |
45 | @Override
46 | public String call() throws Exception {
47 | keyManager.generateKeyPair(isAuth);
48 | return String.format("generated key pair with\nKeyAlgorithm=%s\nIsAuth=%s", algorithm, isAuth);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/google/capillary/WebPushEncrypterManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import com.google.capillary.internal.WrappedWebPushPublicKey;
20 | import com.google.crypto.tink.HybridEncrypt;
21 | import com.google.crypto.tink.apps.webpush.WebPushHybridEncrypt;
22 | import com.google.protobuf.InvalidProtocolBufferException;
23 | import java.security.GeneralSecurityException;
24 |
25 | /**
26 | * An implementation of {@link EncrypterManager} that supports Web Push encryption.
27 | */
28 | public final class WebPushEncrypterManager extends EncrypterManager {
29 |
30 | @Override
31 | HybridEncrypt rawLoadPublicKey(byte[] publicKey) throws GeneralSecurityException {
32 | WrappedWebPushPublicKey wrappedWebPushPublicKey;
33 | try {
34 | wrappedWebPushPublicKey = WrappedWebPushPublicKey.parseFrom(publicKey);
35 | } catch (InvalidProtocolBufferException e) {
36 | throw new GeneralSecurityException("unable to parse public key", e);
37 | }
38 | return new WebPushHybridEncrypt.Builder()
39 | .withAuthSecret(wrappedWebPushPublicKey.getAuthSecret().toByteArray())
40 | .withRecipientPublicKey(wrappedWebPushPublicKey.getKeyBytes().toByteArray())
41 | .build();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/callables/DelKey.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android.callables;
18 |
19 | import com.google.capillary.android.KeyManager;
20 | import com.google.capillary.demo.common.KeyAlgorithm;
21 | import java.util.concurrent.Callable;
22 |
23 | /**
24 | * Deletes the specified Capillary key pair from the device.
25 | */
26 | public final class DelKey implements Callable {
27 |
28 | private final KeyManager keyManager;
29 | private final KeyAlgorithm algorithm;
30 | private final boolean isAuth;
31 |
32 | /**
33 | * Initializes a new {@link DelKey}.
34 | *
35 | * @param keyManager the key manager to use.
36 | * @param algorithm the algorithm of the deleted key.
37 | * @param isAuth whether the deleted key requires authenticated.
38 | */
39 | public DelKey(KeyManager keyManager, KeyAlgorithm algorithm, boolean isAuth) {
40 | this.keyManager = keyManager;
41 | this.algorithm = algorithm;
42 | this.isAuth = isAuth;
43 | }
44 |
45 | @Override
46 | public String call() throws Exception {
47 | keyManager.deleteKeyPair(isAuth);
48 | return String.format("deleted key pair with\nKeyAlgorithm=%s\nIsAuth=%s", algorithm, isAuth);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/google/capillary/RsaEcdsaConstants.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import java.security.spec.MGF1ParameterSpec;
20 | import javax.crypto.spec.OAEPParameterSpec;
21 | import javax.crypto.spec.PSource.PSpecified;
22 |
23 | /**
24 | * Contains the constants and enums used by RSA-ECDSA encryption/decryption.
25 | */
26 | public final class RsaEcdsaConstants {
27 |
28 | static final OAEPParameterSpec OAEP_PARAMETER_SPEC =
29 | new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSpecified.DEFAULT);
30 | static final int SIGNATURE_LENGTH_BYTES_LENGTH = 4;
31 |
32 | /**
33 | * Encapsulates the ciphertext padding modes supported by RSA-ECDSA encryption/decryption.
34 | */
35 | public enum Padding {
36 | OAEP("OAEPPadding"),
37 | PKCS1("PKCS1Padding");
38 |
39 | private static final String PREFIX = "RSA/ECB/";
40 |
41 | private final String padding;
42 |
43 | Padding(String val) {
44 | padding = val;
45 | }
46 |
47 | /**
48 | * Returns the current padding enum's transformation string that should be used when calling
49 | * {@code javax.crypto.Cipher.getInstance}.
50 | *
51 | * @return the transformation string.
52 | */
53 | public String getTransformation() {
54 | return PREFIX + padding;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/src/main/proto/capillary_internal.proto:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | syntax = "proto3";
18 |
19 | option java_package = "com.google.capillary.internal";
20 | option java_multiple_files = true;
21 |
22 | package capillary.internal;
23 |
24 | // Container for a RSA hybrid encryption ciphertext.
25 | message HybridRsaCiphertext {
26 | bytes symmetric_key_ciphertext = 1;
27 | bytes payload_ciphertext = 2;
28 | }
29 |
30 | // Container for a raw RSA public key and its associated metadata.
31 | message WrappedRsaEcdsaPublicKey {
32 | string padding = 1;
33 | bytes key_bytes = 2;
34 | }
35 |
36 | // Container for a tink Web Push public key parameters.
37 | message WrappedWebPushPublicKey {
38 | bytes auth_secret = 1;
39 | bytes key_bytes = 2;
40 | }
41 |
42 | // Container for a tink Web Push private key parameters.
43 | message WrappedWebPushPrivateKey {
44 | bytes auth_secret = 1;
45 | bytes public_key_bytes = 2;
46 | bytes private_key_bytes = 3;
47 | }
48 |
49 | // Container for a Capillary public key and its associated metadata.
50 | message CapillaryPublicKey {
51 | string keychain_unique_id = 1;
52 | int32 serial_number = 2;
53 | bool is_auth = 3;
54 | bytes key_bytes = 4;
55 | }
56 |
57 | // Container for a Capillary ciphertext and its associated metadata.
58 | message CapillaryCiphertext {
59 | string keychain_unique_id = 1;
60 | int32 key_serial_number = 2;
61 | bool is_auth_key = 3;
62 | bytes ciphertext = 4;
63 | }
64 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/callables/DecryptSavedCiphertexts.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android.callables;
18 |
19 | import com.google.capillary.android.CapillaryHandler;
20 | import com.google.capillary.android.DecrypterManager;
21 | import com.google.capillary.demo.common.KeyAlgorithm;
22 | import java.util.concurrent.Callable;
23 |
24 | /**
25 | * Processes any saved Capillary ciphertexts.
26 | */
27 | public final class DecryptSavedCiphertexts implements Callable {
28 |
29 | private final CapillaryHandler handler;
30 | private final DecrypterManager decrypterManager;
31 | private final KeyAlgorithm keyAlgorithm;
32 |
33 | /**
34 | * Initializes a new {@link DecryptSavedCiphertexts}.
35 | *
36 | * @param handler the Capillary handler to use.
37 | * @param decrypterManager the decrypter manager to use.
38 | * @param keyAlgorithm the key algorithm of the saved ciphertexts.
39 | */
40 | public DecryptSavedCiphertexts(
41 | CapillaryHandler handler, DecrypterManager decrypterManager, KeyAlgorithm keyAlgorithm) {
42 | this.handler = handler;
43 | this.decrypterManager = decrypterManager;
44 | this.keyAlgorithm = keyAlgorithm;
45 | }
46 |
47 | @Override
48 | public String call() throws Exception {
49 | decrypterManager.decryptSaved(handler, keyAlgorithm);
50 | return String.format("decrypted saved ciphertexts with\nKeyAlgorithm=%s", keyAlgorithm);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/callables/RegUser.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android.callables;
18 |
19 | import com.google.capillary.demo.common.AddOrUpdateUserRequest;
20 | import com.google.capillary.demo.common.DemoServiceGrpc;
21 | import com.google.capillary.demo.common.DemoServiceGrpc.DemoServiceBlockingStub;
22 | import com.google.firebase.iid.FirebaseInstanceId;
23 | import io.grpc.ManagedChannel;
24 | import java.util.concurrent.Callable;
25 |
26 | /**
27 | * Registers the current user with the application server.
28 | */
29 | public final class RegUser implements Callable {
30 |
31 | private final ManagedChannel channel;
32 | private final String userId;
33 |
34 | /**
35 | * Initializes a new {@link RegUser}.
36 | *
37 | * @param channel the Capillary handler to use.
38 | * @param userId the user ID.
39 | */
40 | public RegUser(ManagedChannel channel, String userId) {
41 | this.channel = channel;
42 | this.userId = userId;
43 | }
44 |
45 | @Override
46 | public String call() throws Exception {
47 | DemoServiceBlockingStub blockingStub = DemoServiceGrpc.newBlockingStub(channel);
48 | AddOrUpdateUserRequest request = AddOrUpdateUserRequest.newBuilder()
49 | .setUserId(userId)
50 | .setToken(FirebaseInstanceId.getInstance().getToken()).build();
51 | blockingStub.addOrUpdateUser(request);
52 | return String.format("registered user with:\n%s", request);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/callables/RegKey.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android.callables;
18 |
19 | import com.google.capillary.android.CapillaryHandler;
20 | import com.google.capillary.android.KeyManager;
21 | import com.google.capillary.demo.common.KeyAlgorithm;
22 | import java.util.concurrent.Callable;
23 |
24 | /**
25 | * Registers the specified Capillary public key with the application server.
26 | */
27 | public final class RegKey implements Callable {
28 |
29 | private final CapillaryHandler handler;
30 | private final KeyManager keyManager;
31 | private final KeyAlgorithm algorithm;
32 | private final boolean isAuth;
33 |
34 | /**
35 | * Initializes a new {@link RegKey}.
36 | *
37 | * @param handler the Capillary handler to use.
38 | * @param keyManager the key manager to use.
39 | * @param algorithm the algorithm of the registered key.
40 | * @param isAuth whether the registered key requires authenticated.
41 | */
42 | public RegKey(
43 | CapillaryHandler handler, KeyManager keyManager, KeyAlgorithm algorithm, boolean isAuth) {
44 | this.handler = handler;
45 | this.keyManager = keyManager;
46 | this.algorithm = algorithm;
47 | this.isAuth = isAuth;
48 | }
49 |
50 | @Override
51 | public String call() throws Exception {
52 | keyManager.getPublicKey(isAuth, handler, algorithm);
53 | return String
54 | .format("registered key pair with:\nKeyAlgorithm=%s\nIsAuth=%s", algorithm, isAuth);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/demo/server/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'application'
2 | apply plugin: 'com.google.protobuf'
3 |
4 | buildscript {
5 | dependencies {
6 | classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.3"
7 | }
8 | }
9 |
10 | def grpcVersion = '1.7.0' // CURRENT_GRPC_VERSION
11 |
12 | dependencies {
13 | implementation project(':demo:common')
14 | implementation 'com.google.capillary:lib:1.0.0'
15 | implementation 'com.google.crypto.tink:tink:1.1.0'
16 | implementation 'com.google.protobuf:protobuf-java:3.4.0'
17 | implementation "io.grpc:grpc-netty:${grpcVersion}"
18 | implementation "io.grpc:grpc-protobuf:${grpcVersion}"
19 | implementation "io.grpc:grpc-stub:${grpcVersion}"
20 | implementation 'io.netty:netty-tcnative-boringssl-static:2.0.6.Final'
21 | implementation 'com.google.api-client:google-api-client:1.23.0'
22 | implementation 'com.google.code.gson:gson:2.8.2'
23 | implementation 'com.google.guava:guava:23.4-jre'
24 | implementation 'commons-cli:commons-cli:1.4'
25 | implementation 'org.xerial:sqlite-jdbc:3.21.0.1'
26 | }
27 |
28 | sourceSets {
29 | main {
30 | proto {
31 | srcDir project(':demo:common').projectDir.toString() + '/proto'
32 | }
33 | }
34 | }
35 |
36 | mainClassName = 'com.google.capillary.demo.server.DemoServer'
37 |
38 | run {
39 | if (project.hasProperty("runArgs")) {
40 | args(runArgs.split(','))
41 | }
42 | }
43 |
44 | protobuf {
45 | protoc {
46 | artifact = 'com.google.protobuf:protoc:3.4.0'
47 | }
48 | plugins {
49 | grpc {
50 | artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
51 | }
52 | }
53 | generateProtoTasks {
54 | all().each { task ->
55 | task.plugins {
56 | grpc {
57 | option 'enable_deprecated=false'
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
64 | // Inform IntelliJ projects about the generated code.
65 | apply plugin: 'idea'
66 |
67 | idea {
68 | module {
69 | sourceDirs += file("${projectDir}/build/generated/source/proto/main/grpc")
70 | sourceDirs += file("${projectDir}/build/generated/source/proto/main/java")
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib-android/src/test/java/com/google/capillary/android/KeyManagerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import static org.mockito.Matchers.anyString;
20 | import static org.mockito.Matchers.eq;
21 | import static org.mockito.Mockito.mock;
22 | import static org.mockito.Mockito.verify;
23 |
24 | import android.content.Context;
25 | import com.google.capillary.NoSuchKeyException;
26 | import com.google.crypto.tink.HybridDecrypt;
27 | import java.security.GeneralSecurityException;
28 | import org.junit.Test;
29 | import org.junit.runner.RunWith;
30 | import org.junit.runners.JUnit4;
31 |
32 | @RunWith(JUnit4.class)
33 | public final class KeyManagerTest {
34 |
35 | @Test
36 | public void testStorageContextIsPrivate() {
37 | Context context = mock(Context.class);
38 | new KeyManager(context, Utils.getInstance(), "keychain id") {
39 | @Override
40 | void rawGenerateKeyPair(boolean isAuth) throws GeneralSecurityException {
41 | }
42 |
43 | @Override
44 | byte[] rawGetPublicKey(boolean isAuth) throws NoSuchKeyException, GeneralSecurityException {
45 | return new byte[0];
46 | }
47 |
48 | @Override
49 | HybridDecrypt rawGetDecrypter(boolean isAuth)
50 | throws NoSuchKeyException, GeneralSecurityException {
51 | return null;
52 | }
53 |
54 | @Override
55 | void rawDeleteKeyPair(boolean isAuth) throws NoSuchKeyException, GeneralSecurityException {
56 | }
57 | };
58 |
59 | // There should be one call to getSharedPreferences with Context.MODE_PRIVATE.
60 | verify(context).getSharedPreferences(anyString(), eq(Context.MODE_PRIVATE));
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib-android/publish.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.github.dcendents.android-maven'
2 | apply plugin: 'com.jfrog.bintray'
3 |
4 | def libraryVersion = '1.0.0'
5 | def bintrayRepo = 'capillary'
6 | def bintrayPackage = 'lib-android'
7 |
8 | group = 'com.google.capillary'
9 | version = libraryVersion
10 |
11 | install {
12 | repositories.mavenInstaller {
13 | pom.project {
14 | name 'Capillary Android'
15 | description 'Capillary is a library to simplify the sending of end-to-end (E2E) ' +
16 | 'encrypted push messages from Java-based application servers to Android ' +
17 | 'clients. This is the Capillary library for Android clients.'
18 | url 'https://github.com/google/capillary'
19 | licenses {
20 | license {
21 | name 'The Apache License, Version 2.0'
22 | url 'https://www.apache.org/licenses/LICENSE-2.0.txt'
23 | }
24 | }
25 | developers {
26 | developer {
27 | name project.hasProperty('user.name') ?
28 | project.property('user.name') : System.getenv('USER_NAME')
29 | email project.hasProperty('user.email') ?
30 | project.property('user.email') : System.getenv('USER_EMAIL')
31 | organization = 'Google LLC'
32 | organizationUrl 'https://www.google.com'
33 | }
34 | }
35 | scm {
36 | connection 'scm:git:https://github.com/google/capillary.git'
37 | developerConnection 'scm:git:https://github.com/google/capillary.git'
38 | url 'https://github.com/google/capillary'
39 | }
40 | }
41 | }
42 | }
43 |
44 | bintray {
45 | user = project.hasProperty('bintray.user') ?
46 | project.property('bintray.user') : System.getenv('BINTRAY_USER')
47 | key = project.hasProperty('bintray.apikey') ?
48 | project.property('bintray.apikey') : System.getenv('BINTRAY_APIKEY')
49 | configurations = ['archives']
50 | pkg {
51 | repo = bintrayRepo
52 | name = bintrayPackage
53 | userOrg = 'google'
54 | version {
55 | name = libraryVersion
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/publish.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 | apply plugin: 'com.jfrog.bintray'
3 |
4 | def libraryVersion = '1.0.0'
5 | def bintrayRepo = 'capillary'
6 | def bintrayPackage = 'lib'
7 |
8 | def pomConfig = {
9 | url 'https://github.com/google/capillary'
10 | licenses {
11 | license {
12 | name 'The Apache License, Version 2.0'
13 | url 'https://www.apache.org/licenses/LICENSE-2.0.txt'
14 | }
15 | }
16 | developers {
17 | developer {
18 | name project.property('user.name')
19 | email project.property('user.email')
20 | organization 'Google LLC'
21 | organizationUrl 'https://www.google.com'
22 | }
23 | }
24 | scm {
25 | connection 'scm:git:https://github.com/google/capillary.git'
26 | developerConnection 'scm:git:https://github.com/google/capillary.git'
27 | url 'https://github.com/google/capillary'
28 | }
29 | }
30 |
31 | publishing {
32 | publications {
33 | MavenLib(MavenPublication) {
34 | groupId 'com.google.capillary'
35 | artifactId 'lib'
36 | version libraryVersion
37 |
38 | pom.withXml {
39 | def root = asNode()
40 | root.appendNode('name', 'Capillary')
41 | root.appendNode('description', 'Capillary is a library to simplify the sending ' +
42 | 'of end-to-end (E2E) encrypted push messages from Java-based application ' +
43 | 'servers to Android clients. This is the Capillary library for ' +
44 | 'Java-based application servers.')
45 | root.children().last() + pomConfig
46 | }
47 |
48 | from components.java
49 | artifact sourcesJar
50 | artifact javadocJar
51 | }
52 | }
53 | }
54 |
55 | bintray {
56 | user = project.hasProperty('bintray.user') ?
57 | project.property('bintray.user') : System.getenv('BINTRAY_USER')
58 | key = project.hasProperty('bintray.apikey') ?
59 | project.property('bintray.apikey') : System.getenv('BINTRAY_APIKEY')
60 | publications = ['MavenLib']
61 | pkg {
62 | repo = bintrayRepo
63 | name = bintrayPackage
64 | userOrg = 'google'
65 | version {
66 | name = libraryVersion
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/src/test/java/com/google/capillary/HybridRsaUtilsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import static org.junit.Assert.assertArrayEquals;
20 |
21 | import com.google.capillary.RsaEcdsaConstants.Padding;
22 | import com.google.crypto.tink.subtle.Random;
23 | import java.io.IOException;
24 | import java.security.GeneralSecurityException;
25 | import java.security.PrivateKey;
26 | import java.security.PublicKey;
27 | import org.junit.Test;
28 | import org.junit.runner.RunWith;
29 | import org.junit.runners.JUnit4;
30 |
31 | @RunWith(JUnit4.class)
32 | public final class HybridRsaUtilsTest {
33 |
34 | /**
35 | * Creates a new {@link HybridRsaUtilsTest} instance.
36 | */
37 | public HybridRsaUtilsTest() throws GeneralSecurityException {
38 | Config.initialize();
39 | }
40 |
41 | @Test
42 | public void testEncryptDecrypt() throws GeneralSecurityException, IOException {
43 | PublicKey publicKey = TestUtils.createTestRsaPublicKey();
44 | PrivateKey privateKey = TestUtils.createTestRsaPrivateKey();
45 |
46 | // Try Encryption/Decryption for each padding mode.
47 | for (Padding padding : Padding.values()) {
48 | // Try Encryption/Decryption for plaintext sizes 64, 128, 256, 512, and 1048 bytes.
49 | for (int plaintextSize : new int[]{64, 128, 256, 512, 1048}) {
50 | byte[] plaintext = Random.randBytes(plaintextSize);
51 | byte[] ciphertext = HybridRsaUtils.encrypt(
52 | plaintext, publicKey, padding, RsaEcdsaConstants.OAEP_PARAMETER_SPEC);
53 | byte[] decryptedPlaintext = HybridRsaUtils.decrypt(
54 | ciphertext, privateKey, padding, RsaEcdsaConstants.OAEP_PARAMETER_SPEC);
55 | assertArrayEquals(plaintext, decryptedPlaintext);
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/DeviceUnlockedBroadcastReceiver.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android;
18 |
19 | import android.content.BroadcastReceiver;
20 | import android.content.Context;
21 | import android.content.Intent;
22 | import com.google.capillary.demo.common.KeyAlgorithm;
23 | import io.grpc.ManagedChannel;
24 | import java.io.IOException;
25 | import java.security.GeneralSecurityException;
26 |
27 | /**
28 | * Upon device unlock event, attempts to decrypt any Capillary ciphertexts that were previously
29 | * saved due to authenticated Capillary decryption keys not being available.
30 | */
31 | public final class DeviceUnlockedBroadcastReceiver extends BroadcastReceiver {
32 |
33 | @Override
34 | public void onReceive(Context context, Intent intent) {
35 | // Check if this is the right intent action.
36 | if (!Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {
37 | return;
38 | }
39 |
40 | try {
41 | Utils.initialize(context);
42 |
43 | // Create the gRPC channel.
44 | ManagedChannel channel = Utils.createGrpcChannel(context);
45 |
46 | // Create the DemoCapillaryHandler.
47 | DemoCapillaryHandler handler = new DemoCapillaryHandler(context, channel);
48 |
49 | // Process any saved RsaEcdsa ciphertexts.
50 | Utils.getKeyManager(context, KeyAlgorithm.RSA_ECDSA)
51 | .getDecrypterManager().decryptSaved(handler, KeyAlgorithm.RSA_ECDSA);
52 |
53 | // Process any saved WebPush ciphertexts.
54 | Utils.getKeyManager(context, KeyAlgorithm.WEB_PUSH)
55 | .getDecrypterManager().decryptSaved(handler, KeyAlgorithm.WEB_PUSH);
56 |
57 | // Close the gRPC channel.
58 | channel.shutdown();
59 | } catch (GeneralSecurityException | IOException e) {
60 | e.printStackTrace();
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/DemoFiidService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android;
18 |
19 | import android.util.Log;
20 | import com.google.capillary.demo.common.AddOrUpdateUserRequest;
21 | import com.google.capillary.demo.common.DemoServiceGrpc;
22 | import com.google.capillary.demo.common.DemoServiceGrpc.DemoServiceBlockingStub;
23 | import com.google.firebase.iid.FirebaseInstanceId;
24 | import com.google.firebase.iid.FirebaseInstanceIdService;
25 | import io.grpc.ManagedChannel;
26 | import java.io.IOException;
27 |
28 | /**
29 | * Extends the {@link FirebaseInstanceIdService} to register refreshed IID tokens with the server.
30 | * The communication with the server is done via gRPC.
31 | */
32 | public final class DemoFiidService extends FirebaseInstanceIdService {
33 |
34 | private static final String TAG = DemoFiidService.class.getSimpleName();
35 |
36 | @Override
37 | public void onTokenRefresh() {
38 | String newToken = FirebaseInstanceId.getInstance().getToken();
39 | Log.d(TAG, "new token received: " + newToken);
40 |
41 | sendRegistrationToServer(newToken);
42 | }
43 |
44 | private void sendRegistrationToServer(String token) {
45 | try {
46 | Utils.initialize(this);
47 |
48 | // Create the gRPC channel and stub.
49 | ManagedChannel channel = Utils.createGrpcChannel(this);
50 | DemoServiceBlockingStub blockingStub = DemoServiceGrpc.newBlockingStub(channel);
51 |
52 | // Create and send the AddOrUpdateUserRequest.
53 | AddOrUpdateUserRequest request = AddOrUpdateUserRequest.newBuilder()
54 | .setUserId(Utils.getUserId(this))
55 | .setToken(token)
56 | .build();
57 | blockingStub.addOrUpdateUser(request);
58 |
59 | // Close the gRPC channel.
60 | channel.shutdown();
61 | } catch (IOException e) {
62 | e.printStackTrace();
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/demo/common/proto/capillary_demo_common.proto:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | syntax = "proto3";
18 |
19 | import "google/protobuf/empty.proto";
20 |
21 | option java_package = "com.google.capillary.demo.common";
22 | option java_multiple_files = true;
23 |
24 | package capillary.demo.common;
25 |
26 | // The types of Capillary public keys.
27 | enum KeyAlgorithm {
28 | RSA_ECDSA = 0;
29 | WEB_PUSH = 1;
30 | }
31 |
32 | // A request to add the given user or update the FCM token of an existing user.
33 | message AddOrUpdateUserRequest {
34 | string user_id = 1;
35 | string token = 2;
36 | }
37 |
38 | // A request to register the given Capillary public key for the specified user.
39 | message AddOrUpdatePublicKeyRequest {
40 | string user_id = 1;
41 | KeyAlgorithm algorithm = 2;
42 | bool is_auth = 3;
43 | bytes key_bytes = 4;
44 | }
45 |
46 | // A request to send the embedded data bytes encrypted into a Capillary
47 | // ciphertext to the specified user after the specified delay.
48 | message SendMessageRequest {
49 | string user_id = 1;
50 | KeyAlgorithm key_algorithm = 2;
51 | bool is_auth_key = 3;
52 | int32 delay_seconds = 4;
53 | bytes data = 5;
54 | }
55 |
56 | // The Android notification that is shown to the user.
57 | message SecureNotification {
58 | int32 id = 1;
59 | string title = 2;
60 | string body = 3;
61 | }
62 |
63 | service DemoService {
64 | // Registers a new user or updates the FCM token of an existing user.
65 | rpc AddOrUpdateUser (AddOrUpdateUserRequest) returns (google.protobuf.Empty) {
66 | }
67 | // Registers a new Capillary public key for the user or updates the existing
68 | // Capillary public key.
69 | rpc AddOrUpdatePublicKey (AddOrUpdatePublicKeyRequest) returns (google.protobuf.Empty) {
70 | }
71 | // Encrypts the supplied data bytes into a Capillary ciphertext and sends it
72 | // to the user's device via FCM after the specified delay.
73 | rpc SendMessage (SendMessageRequest) returns (google.protobuf.Empty) {
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/src/test/java/com/google/capillary/WebPushEncrypterManagerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import static org.junit.Assert.fail;
20 |
21 | import com.google.capillary.internal.WrappedWebPushPublicKey;
22 | import com.google.crypto.tink.subtle.Random;
23 | import com.google.protobuf.ByteString;
24 | import java.io.IOException;
25 | import java.security.GeneralSecurityException;
26 | import org.junit.Before;
27 | import org.junit.Test;
28 | import org.junit.runner.RunWith;
29 | import org.junit.runners.JUnit4;
30 |
31 | @RunWith(JUnit4.class)
32 | public final class WebPushEncrypterManagerTest {
33 |
34 | private WebPushEncrypterManager encrypterManager;
35 |
36 | /**
37 | * Creates a new {@link WebPushEncrypterManagerTest} instance.
38 | */
39 | public WebPushEncrypterManagerTest() throws GeneralSecurityException {
40 | Config.initialize();
41 | }
42 |
43 | /**
44 | * Initializes test case-specific state.
45 | */
46 | @Before
47 | public void setUp() throws IOException, GeneralSecurityException {
48 | encrypterManager = new WebPushEncrypterManager();
49 | }
50 |
51 | @Test
52 | public void stub() {
53 | }
54 |
55 | @Test
56 | public void testLoadMalformedRawPublicKey() {
57 | byte[] rawPublicKey = "malformed raw public key".getBytes();
58 |
59 | try {
60 | encrypterManager.rawLoadPublicKey(rawPublicKey);
61 | fail("Did not throw GeneralSecurityException");
62 | } catch (GeneralSecurityException e) {
63 | // This is expected.
64 | }
65 | }
66 |
67 | @Test
68 | public void testLoadValidRawPublicKey() throws IOException, GeneralSecurityException {
69 | byte[] authSecret = Random.randBytes(16);
70 | byte[] ecPublicKeyBytes = TestUtils.getBytes("ec_public_key.dat");
71 | byte[] rawPublicKey = WrappedWebPushPublicKey.newBuilder()
72 | .setAuthSecret(ByteString.copyFrom(authSecret))
73 | .setKeyBytes(ByteString.copyFrom(ecPublicKeyBytes))
74 | .build().toByteArray();
75 |
76 | encrypterManager.rawLoadPublicKey(rawPublicKey);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/lib/src/test/java/com/google/capillary/RsaEcdsaEncrypterManagerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import static org.junit.Assert.fail;
20 |
21 | import com.google.capillary.RsaEcdsaConstants.Padding;
22 | import com.google.capillary.internal.WrappedRsaEcdsaPublicKey;
23 | import com.google.protobuf.ByteString;
24 | import java.io.IOException;
25 | import java.io.InputStream;
26 | import java.security.GeneralSecurityException;
27 | import org.junit.Before;
28 | import org.junit.Test;
29 | import org.junit.runner.RunWith;
30 | import org.junit.runners.JUnit4;
31 |
32 | @RunWith(JUnit4.class)
33 | public final class RsaEcdsaEncrypterManagerTest {
34 |
35 | private RsaEcdsaEncrypterManager encrypterManager;
36 |
37 | /**
38 | * Creates a new {@link RsaEcdsaEncrypterManagerTest} instance.
39 | */
40 | public RsaEcdsaEncrypterManagerTest() throws GeneralSecurityException {
41 | Config.initialize();
42 | }
43 |
44 | /**
45 | * Initializes test case-specific state.
46 | */
47 | @Before
48 | public void setUp() throws IOException, GeneralSecurityException {
49 | try (InputStream senderSigner =
50 | RsaEcdsaEncrypterManagerTest.class.getResourceAsStream("signing_key.dat")) {
51 | encrypterManager = new RsaEcdsaEncrypterManager(senderSigner);
52 | }
53 | }
54 |
55 | @Test
56 | public void testLoadMalformedRawPublicKey() {
57 | byte[] rawPublicKey = "malformed raw public key".getBytes();
58 |
59 | try {
60 | encrypterManager.rawLoadPublicKey(rawPublicKey);
61 | fail("Did not throw GeneralSecurityException");
62 | } catch (GeneralSecurityException e) {
63 | // This is expected.
64 | }
65 | }
66 |
67 | @Test
68 | public void testLoadValidRawPublicKey() throws IOException, GeneralSecurityException {
69 | byte[] testRsaPublicKey = TestUtils.getBytes("rsa_public_key.dat");
70 | byte[] rawPublicKey = WrappedRsaEcdsaPublicKey.newBuilder()
71 | .setPadding(Padding.OAEP.name())
72 | .setKeyBytes(ByteString.copyFrom(testRsaPublicKey))
73 | .build().toByteArray();
74 |
75 | encrypterManager.rawLoadPublicKey(rawPublicKey);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/lib-android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | dependencies {
3 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3'
4 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
5 | }
6 | }
7 |
8 | apply plugin: 'com.android.library'
9 |
10 | android {
11 | compileSdkVersion 27
12 | defaultConfig {
13 | minSdkVersion 19
14 | targetSdkVersion 27
15 | versionCode 1
16 | versionName "1.0"
17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
18 | }
19 | compileOptions {
20 | sourceCompatibility JavaVersion.VERSION_1_8
21 | targetCompatibility JavaVersion.VERSION_1_8
22 | }
23 | }
24 |
25 | // Checkstyle doesn't run automatically with android
26 | task checkstyleMain(type: Checkstyle) {
27 | source "src/main/java"
28 | include '**/*.java'
29 | classpath = files()
30 | }
31 | task checkStyleTest(type: Checkstyle) {
32 | source 'src/test/java'
33 | include '**/*.java'
34 | classpath = files()
35 | }
36 | task checkStyleAndroidTest(type: Checkstyle) {
37 | source 'src/androidTest/java'
38 | include '**/*.java'
39 | classpath = files()
40 | }
41 | check.dependsOn checkstyleMain, checkStyleTest, checkStyleAndroidTest
42 |
43 | dependencies {
44 | api(project(':lib'), {
45 | exclude group: 'com.google.crypto.tink', module: '*'
46 | })
47 | implementation('com.google.crypto.tink:tink-android:1.1.0', {
48 | exclude group: 'com.google.protobuf', module: '*'
49 | })
50 | implementation('com.google.crypto.tink:apps-webpush:1.1.0', {
51 | exclude group: 'com.google.crypto.tink', module: '*'
52 | })
53 | implementation 'com.google.protobuf:protobuf-java:3.4.0'
54 | implementation 'joda-time:joda-time:2.9.9'
55 | implementation 'com.android.support:support-annotations:27.1.1'
56 | testImplementation 'junit:junit:4.12'
57 | testImplementation 'org.mockito:mockito-core:1.10.19'
58 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
59 | }
60 |
61 | task sourcesJar(type: Jar) {
62 | classifier = 'sources'
63 | from android.sourceSets.main.java.srcDirs
64 | }
65 |
66 | task javadoc(type: Javadoc) {
67 | source = android.sourceSets.main.java.srcDirs
68 | classpath += files(project.android.getBootClasspath())
69 | failOnError false
70 | }
71 |
72 | // Only needed when cutting releases. lib.jar has to be created before running the following.
73 | /*
74 | afterEvaluate {
75 | javadoc.classpath += files(android.libraryVariants.collect
76 | { variant ->
77 | variant.javaCompiler.classpath.files
78 | })
79 | }
80 | */
81 |
82 | task javadocJar(type: Jar, dependsOn: javadoc) {
83 | classifier = 'javadoc'
84 | from javadoc.destinationDir
85 | }
86 |
87 | artifacts {
88 | archives sourcesJar
89 | archives javadocJar
90 | }
91 |
92 | apply from: 'publish.gradle'
93 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/callables/RequestMessage.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android.callables;
18 |
19 | import com.google.capillary.demo.android.Utils;
20 | import com.google.capillary.demo.common.DemoServiceGrpc;
21 | import com.google.capillary.demo.common.DemoServiceGrpc.DemoServiceBlockingStub;
22 | import com.google.capillary.demo.common.KeyAlgorithm;
23 | import com.google.capillary.demo.common.SendMessageRequest;
24 | import io.grpc.ManagedChannel;
25 | import java.util.concurrent.Callable;
26 |
27 | /**
28 | * Requests the application server to send a demo notification message.
29 | */
30 | public final class RequestMessage implements Callable {
31 |
32 | private final ManagedChannel channel;
33 | private final KeyAlgorithm keyAlgorithm;
34 | private final String userId;
35 | private final boolean isAuthKey;
36 | private final int delay;
37 |
38 | /**
39 | * Initializes a new {@link RequestMessage}.
40 | *
41 | * @param channel the gRPC channel to use.
42 | * @param keyAlgorithm the algorithm to be used to encrypt the demo notification.
43 | * @param userId the user ID of the current app instance.
44 | * @param isAuthKey whether the demo notification should be encrypted using an authenticated key.
45 | * @param delay the amount of time the application server should wait before sending the
46 | * notification.
47 | */
48 | public RequestMessage(
49 | ManagedChannel channel,
50 | KeyAlgorithm keyAlgorithm,
51 | String userId,
52 | boolean isAuthKey,
53 | int delay) {
54 | this.channel = channel;
55 | this.keyAlgorithm = keyAlgorithm;
56 | this.userId = userId;
57 | this.isAuthKey = isAuthKey;
58 | this.delay = delay;
59 | }
60 |
61 | @Override
62 | public String call() throws Exception {
63 | DemoServiceBlockingStub blockingStub = DemoServiceGrpc.newBlockingStub(channel);
64 | SendMessageRequest request = SendMessageRequest.newBuilder()
65 | .setUserId(userId)
66 | .setKeyAlgorithm(keyAlgorithm)
67 | .setIsAuthKey(isAuthKey)
68 | .setDelaySeconds(delay)
69 | .setData(Utils.createSecureMessageBytes("new msg", keyAlgorithm, isAuthKey)).build();
70 | blockingStub.sendMessage(request);
71 | return String.format("requested message with:\n%s", request);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/demo/android/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'com.google.protobuf'
3 |
4 | buildscript {
5 | dependencies {
6 | classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.3"
7 | classpath 'com.google.gms:google-services:4.0.1'
8 | }
9 | }
10 |
11 | android {
12 | compileSdkVersion 27
13 | defaultConfig {
14 | applicationId "com.google.capillary.demo.android"
15 | minSdkVersion 19
16 | targetSdkVersion 27
17 | versionCode 1
18 | versionName "1.0"
19 | multiDexEnabled true
20 | }
21 | sourceSets {
22 | main {
23 | proto {
24 | srcDir project(':demo:common').projectDir.toString() + '/proto'
25 | }
26 | }
27 | }
28 | compileOptions {
29 | sourceCompatibility JavaVersion.VERSION_1_8
30 | targetCompatibility JavaVersion.VERSION_1_8
31 | }
32 | packagingOptions {
33 | exclude 'build-data.properties'
34 | }
35 | }
36 |
37 | // Checkstyle doesn't run automatically with android
38 | task checkstyleMain(type: Checkstyle) {
39 | source "src/main/java"
40 | include '**/*.java'
41 | classpath = files()
42 | }
43 | check.dependsOn checkstyleMain
44 |
45 | def grpcVersion = '1.7.0' // CURRENT_GRPC_VERSION
46 |
47 | dependencies {
48 | implementation project(':demo:common')
49 | implementation 'com.google.capillary:lib-android:1.0.0'
50 | implementation('com.google.crypto.tink:tink-android:1.1.0', {
51 | exclude group: 'com.google.protobuf'
52 | })
53 | implementation 'com.android.support:appcompat-v7:27.1.1'
54 | implementation 'com.android.support:support-v4:27.1.1'
55 | implementation 'javax.annotation:javax.annotation-api:1.2'
56 | implementation 'com.android.support.constraint:constraint-layout:1.1.0'
57 | implementation 'com.google.protobuf:protobuf-java:3.4.0'
58 | implementation "io.grpc:grpc-okhttp:${grpcVersion}"
59 | implementation "io.grpc:grpc-protobuf:${grpcVersion}"
60 | implementation "io.grpc:grpc-stub:${grpcVersion}"
61 | implementation 'com.google.firebase:firebase-core:16.0.0'
62 | implementation 'com.google.firebase:firebase-messaging:17.0.0'
63 | implementation 'com.android.support:multidex:1.0.3'
64 | implementation 'joda-time:joda-time:2.9.9'
65 | androidTestImplementation 'com.android.support:multidex:1.0.3'
66 | }
67 |
68 | protobuf {
69 | protoc {
70 | artifact = 'com.google.protobuf:protoc:3.4.0'
71 | }
72 | plugins {
73 | grpc {
74 | artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
75 | }
76 | }
77 | generateProtoTasks {
78 | all().each { task ->
79 | task.builtins {
80 | add java
81 | }
82 | }
83 | all()*.plugins {
84 | grpc {
85 | option 'enable_deprecated=false'
86 | }
87 | }
88 | }
89 | }
90 |
91 | apply plugin: 'com.google.gms.google-services'
92 |
--------------------------------------------------------------------------------
/lib-android/src/main/java/com/google/capillary/android/CiphertextStorage.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import android.content.Context;
20 | import android.content.SharedPreferences;
21 | import com.google.crypto.tink.subtle.Base64;
22 | import java.util.LinkedList;
23 | import java.util.List;
24 |
25 | /**
26 | * Allows storing Capillary ciphertext in {@link SharedPreferences} to be decrypted later.
27 | */
28 | class CiphertextStorage {
29 |
30 | private static final String DATA_COUNT_KEY = "current_count";
31 |
32 | private final SharedPreferences dataSharedPreferences;
33 | private final SharedPreferences metaSharedPreferences;
34 |
35 | /**
36 | * Initializes a {@link SharedPreferences} backed ciphertext storage for the given keychain ID.
37 | */
38 | CiphertextStorage(Context context, Utils utils, String keychainId) {
39 | Context storageContext = utils.getDeviceProtectedStorageContext(context);
40 | String dataPrefName =
41 | String.format("%s_%s_data_preferences", getClass().getCanonicalName(), keychainId);
42 | String metaPrefName =
43 | String.format("%s_%s_meta_preferences", getClass().getCanonicalName(), keychainId);
44 | dataSharedPreferences = storageContext.getSharedPreferences(dataPrefName, Context.MODE_PRIVATE);
45 | metaSharedPreferences = storageContext.getSharedPreferences(metaPrefName, Context.MODE_PRIVATE);
46 | }
47 |
48 | /**
49 | * Saves the given ciphertext.
50 | */
51 | synchronized void save(byte[] ciphertext) {
52 | String ciphertextString = Base64.encode(ciphertext);
53 | int nextCount = metaSharedPreferences.getInt(DATA_COUNT_KEY, 0) + 1;
54 | dataSharedPreferences.edit().putString(String.valueOf(nextCount), ciphertextString).apply();
55 | metaSharedPreferences.edit().putInt(DATA_COUNT_KEY, nextCount).apply();
56 | }
57 |
58 | /**
59 | * Returns all saved ciphertexts.
60 | */
61 | List get() {
62 | List ciphertextList = new LinkedList<>();
63 | for (Object ciphertextString : dataSharedPreferences.getAll().values()) {
64 | byte[] ciphertextBytes = Base64.decode(ciphertextString.toString());
65 | ciphertextList.add(ciphertextBytes);
66 | }
67 | return ciphertextList;
68 | }
69 |
70 | /**
71 | * Clears all saved ciphertexts.
72 | */
73 | synchronized void clear() {
74 | dataSharedPreferences.edit().clear().apply();
75 | metaSharedPreferences.edit().putInt(DATA_COUNT_KEY, 0).apply();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/DemoFmService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android;
18 |
19 | import android.util.Log;
20 | import com.google.capillary.android.DecrypterManager;
21 | import com.google.capillary.demo.common.Constants;
22 | import com.google.capillary.demo.common.KeyAlgorithm;
23 | import com.google.crypto.tink.subtle.Base64;
24 | import com.google.firebase.messaging.FirebaseMessagingService;
25 | import com.google.firebase.messaging.RemoteMessage;
26 | import io.grpc.ManagedChannel;
27 | import java.io.IOException;
28 | import java.security.GeneralSecurityException;
29 | import java.util.Map;
30 |
31 | /**
32 | * Extends {@link FirebaseMessagingService} to retrieve Capillary ciphertexts that are received
33 | * as FCM data messages, and pass these ciphertexts to {@link DecrypterManager} to decrypt.
34 | */
35 | public final class DemoFmService extends FirebaseMessagingService {
36 |
37 | private static final String TAG = DemoFmService.class.getSimpleName();
38 |
39 | @Override
40 | public void onMessageReceived(RemoteMessage remoteMessage) {
41 | // Check if message contains a data payload.
42 | if (remoteMessage.getData().size() > 0) {
43 | Log.d(TAG, "data message received with payload: " + remoteMessage.getData());
44 | handleDataMessage(remoteMessage.getData());
45 | }
46 |
47 | // Check if message contains a notification payload.
48 | if (remoteMessage.getNotification() != null) {
49 | Log.d(TAG, "notification message received with body: "
50 | + remoteMessage.getNotification().getBody());
51 | }
52 | }
53 |
54 | private void handleDataMessage(Map dataMap) {
55 | try {
56 | Utils.initialize(this);
57 |
58 | // Get the encryption algorithm and the ciphertext bytes.
59 | KeyAlgorithm keyAlgorithm =
60 | KeyAlgorithm.valueOf(dataMap.get(Constants.CAPILLARY_KEY_ALGORITHM_KEY));
61 | byte[] ciphertext = Base64.decode(dataMap.get(Constants.CAPILLARY_CIPHERTEXT_KEY));
62 |
63 | // Create the gRPC channel.
64 | ManagedChannel channel = Utils.createGrpcChannel(this);
65 |
66 | // Create the DemoCapillaryHandler.
67 | DemoCapillaryHandler handler = new DemoCapillaryHandler(this, channel);
68 |
69 | // Handle ciphertext.
70 | Utils.getKeyManager(this, keyAlgorithm)
71 | .getDecrypterManager().decrypt(ciphertext, handler, keyAlgorithm);
72 |
73 | // Close the gRPC channel.
74 | channel.shutdown();
75 | } catch (GeneralSecurityException | IOException e) {
76 | e.printStackTrace();
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/TlsOkHttpChannelGenerator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android;
18 |
19 | import io.grpc.ManagedChannel;
20 | import io.grpc.okhttp.OkHttpChannelBuilder;
21 | import java.io.IOException;
22 | import java.io.InputStream;
23 | import java.security.KeyStore;
24 | import java.security.cert.CertificateFactory;
25 | import java.security.cert.X509Certificate;
26 | import javax.net.ssl.SSLContext;
27 | import javax.net.ssl.SSLSocketFactory;
28 | import javax.net.ssl.TrustManager;
29 | import javax.net.ssl.TrustManagerFactory;
30 | import javax.security.auth.x500.X500Principal;
31 |
32 | /**
33 | * A helper class to create an OkHttp based TLS channel.
34 | */
35 | final class TlsOkHttpChannelGenerator {
36 |
37 | /**
38 | * Creates a new {@link ManagedChannel} to the given host and port with the given TLS
39 | * certificates.
40 | */
41 | static ManagedChannel generate(String host, int port, InputStream certStream) throws IOException {
42 | OkHttpChannelBuilder channelBuilder = OkHttpChannelBuilder.forAddress(host, port);
43 | try {
44 | SSLSocketFactory sslSocketFactory = getSslSocketFactory(certStream);
45 | channelBuilder.sslSocketFactory(sslSocketFactory);
46 | } catch (Exception e) {
47 | throw new RuntimeException(e);
48 | }
49 | return channelBuilder.build();
50 | }
51 |
52 | private static SSLSocketFactory getSslSocketFactory(InputStream certStream)
53 | throws Exception {
54 | if (certStream == null) {
55 | return (SSLSocketFactory) SSLSocketFactory.getDefault();
56 | }
57 |
58 | SSLContext context = SSLContext.getInstance("TLS");
59 | context.init(null, getTrustManagers(certStream), null);
60 | return context.getSocketFactory();
61 | }
62 |
63 | private static TrustManager[] getTrustManagers(InputStream certStream) throws Exception {
64 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
65 | keyStore.load(null);
66 | CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
67 | X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(certStream);
68 | X500Principal principal = cert.getSubjectX500Principal();
69 | keyStore.setCertificateEntry(principal.getName("RFC2253"), cert);
70 | // Set up trust manager factory to use our key store.
71 | TrustManagerFactory trustManagerFactory =
72 | TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
73 | trustManagerFactory.init(keyStore);
74 | return trustManagerFactory.getTrustManagers();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/google/capillary/RsaEcdsaEncrypterManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import com.google.capillary.internal.WrappedRsaEcdsaPublicKey;
20 | import com.google.crypto.tink.BinaryKeysetReader;
21 | import com.google.crypto.tink.CleartextKeysetHandle;
22 | import com.google.crypto.tink.HybridEncrypt;
23 | import com.google.crypto.tink.KeysetHandle;
24 | import com.google.crypto.tink.PublicKeySign;
25 | import com.google.crypto.tink.signature.PublicKeySignFactory;
26 | import com.google.protobuf.InvalidProtocolBufferException;
27 | import java.io.IOException;
28 | import java.io.InputStream;
29 | import java.security.GeneralSecurityException;
30 | import java.security.KeyFactory;
31 | import java.security.PublicKey;
32 | import java.security.spec.X509EncodedKeySpec;
33 |
34 | /**
35 | * An implementation of {@link EncrypterManager} that supports RSA-ECDSA encryption.
36 | */
37 | public final class RsaEcdsaEncrypterManager extends EncrypterManager {
38 |
39 | private final PublicKeySign senderSigner;
40 |
41 | /**
42 | * Constructs a new RSA-ECDSA EncrypterManager.
43 | *
44 | *
Please note that the {@link InputStream} {@code senderSigningKey} will not be closed.
45 | *
46 | * @param senderSigningKey the serialized Tink signing key.
47 | * @throws GeneralSecurityException if the initialization fails.
48 | * @throws IOException if the given sender signing key cannot be read.
49 | */
50 | public RsaEcdsaEncrypterManager(InputStream senderSigningKey)
51 | throws GeneralSecurityException, IOException {
52 | KeysetHandle signingKeyHandle = CleartextKeysetHandle
53 | .read(BinaryKeysetReader.withInputStream(senderSigningKey));
54 | senderSigner = PublicKeySignFactory.getPrimitive(signingKeyHandle);
55 | }
56 |
57 | @Override
58 | HybridEncrypt rawLoadPublicKey(byte[] publicKey) throws GeneralSecurityException {
59 | WrappedRsaEcdsaPublicKey wrappedRsaEcdsaPublicKey;
60 | try {
61 | wrappedRsaEcdsaPublicKey = WrappedRsaEcdsaPublicKey.parseFrom(publicKey);
62 | } catch (InvalidProtocolBufferException e) {
63 | throw new GeneralSecurityException("unable to parse public key", e);
64 | }
65 | PublicKey recipientPublicKey = KeyFactory.getInstance("RSA").generatePublic(
66 | new X509EncodedKeySpec(wrappedRsaEcdsaPublicKey.getKeyBytes().toByteArray()));
67 | return new RsaEcdsaHybridEncrypt.Builder()
68 | .withSenderSigner(senderSigner)
69 | .withRecipientPublicKey(recipientPublicKey)
70 | .withPadding(RsaEcdsaConstants.Padding.valueOf(wrappedRsaEcdsaPublicKey.getPadding()))
71 | .build();
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib-android/src/main/java/com/google/capillary/android/CapillaryHandler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | /**
20 | * Defines the interface for generating Capillary public keys and decrypting Capillary ciphertexts.
21 | *
22 | *
One should implement {@link CapillaryHandler} and pass an instance to:
23 | *
24 | *
{@link DecrypterManager} to decrypt Capillary ciphertexts.
25 | *
{@link KeyManager} to generate Capillary public keys.
26 | *
27 | *
28 | *
The implemented {@link CapillaryHandler} should finish the execution of its methods as soon as
29 | * possible. Any long-running tasks, such as network requests, should be done in separate threads.
30 | */
31 | public interface CapillaryHandler {
32 |
33 | /**
34 | * Provides the plaintext after decrypting a Capillary ciphertext.
35 | *
36 | * @param isAuthKey whether the decryption was done using a public key requiring authentication.
37 | * @param data the plaintext.
38 | * @param extra the extra parameter originally passed to {@link DecrypterManager}.
39 | */
40 | void handleData(boolean isAuthKey, byte[] data, Object extra);
41 |
42 | /**
43 | * Provides the generated Capillary public key.
44 | *
45 | * @param isAuthKey whether the public key requires authentication.
46 | * @param publicKey the public key.
47 | * @param extra the extra parameter originally passed to {@link KeyManager}.
48 | */
49 | void handlePublicKey(boolean isAuthKey, byte[] publicKey, Object extra);
50 |
51 | /**
52 | * Provides the Capillary public key that was generated as a result of a decryption error.
53 | *
54 | * @param isAuthKey whether the public key requires authentication.
55 | * @param publicKey the public key.
56 | * @param ciphertext the ciphertext that caused the decryption error.
57 | * @param extra the extra parameter originally passed to {@link DecrypterManager}.
58 | */
59 | void handlePublicKey(boolean isAuthKey, byte[] publicKey, byte[] ciphertext, Object extra);
60 |
61 | /**
62 | * Signals that a Capillary ciphertext was saved to be decrypted after user authentication.
63 | *
64 | * @param ciphertext the saved Capillary ciphertext.
65 | * @param extra the extra parameter originally passed to {@link DecrypterManager}.
66 | */
67 | void authCiphertextSavedForLater(byte[] ciphertext, Object extra);
68 |
69 | /**
70 | * Signals that an error has occurred.
71 | *
72 | * @param errorCode the error code enum.
73 | * @param ciphertext the Capillary ciphertext that is affected by this error.
74 | * @param extra the extra parameter originally passed to {@link DecrypterManager}.
75 | */
76 | void error(CapillaryHandlerErrorCode errorCode, byte[] ciphertext, Object extra);
77 | }
78 |
--------------------------------------------------------------------------------
/lib-android/src/androidTest/java/com/google/capillary/android/CiphertextStorageAndroidTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import static org.junit.Assert.assertArrayEquals;
20 | import static org.junit.Assert.assertEquals;
21 | import static org.junit.Assert.assertTrue;
22 |
23 | import android.content.Context;
24 | import android.support.test.InstrumentationRegistry;
25 | import android.support.test.runner.AndroidJUnit4;
26 | import java.util.List;
27 | import org.junit.Before;
28 | import org.junit.Test;
29 | import org.junit.runner.RunWith;
30 |
31 | @RunWith(AndroidJUnit4.class)
32 | public final class CiphertextStorageAndroidTest {
33 |
34 | private Context context;
35 |
36 | /**
37 | * Initializes test case-specific state.
38 | */
39 | @Before
40 | public void setUp() {
41 | context = InstrumentationRegistry.getTargetContext();
42 | }
43 |
44 | @Test
45 | public void testSaveAndGet() {
46 | byte[][] mockCiphers = new byte[][]{"cipher 1".getBytes(), "cipher 2".getBytes()};
47 | CiphertextStorage storage =
48 | new CiphertextStorage(context, Utils.getInstance(), "keychain 1");
49 | // Clear any remaining ciphertexts from previous tests.
50 | storage.clear();
51 |
52 | // Empty storage shouldn't have any ciphertexts.
53 | assertTrue(storage.get().isEmpty());
54 |
55 | // Saved ciphers should be returned in the order they were saved.
56 | for (byte[] mockCipher : mockCiphers) {
57 | storage.save(mockCipher);
58 | }
59 | List gotCiphers = storage.get();
60 | for (int i = 0; i < gotCiphers.size(); i++) {
61 | assertArrayEquals(mockCiphers[i], gotCiphers.get(i));
62 | }
63 |
64 | // After calling clear, the storage should be empty.
65 | storage.clear();
66 | assertTrue(storage.get().isEmpty());
67 | }
68 |
69 | @Test
70 | public void testMultipleKeychainIds() {
71 | CiphertextStorage storage1 =
72 | new CiphertextStorage(context, Utils.getInstance(), "keychain 1");
73 | CiphertextStorage storage2 =
74 | new CiphertextStorage(context, Utils.getInstance(), "keychain 2");
75 | // Clear any remaining ciphertexts from previous tests.
76 | storage1.clear();
77 | storage2.clear();
78 |
79 | // Initially both stores should be empty.
80 | assertTrue(storage1.get().isEmpty());
81 | assertTrue(storage2.get().isEmpty());
82 |
83 | // Save and get with store 1.
84 | byte[] mockCipher = "cipher 1".getBytes();
85 | storage1.save(mockCipher);
86 | assertEquals(1, storage1.get().size());
87 |
88 | // Store 2 still shouldn't have any ciphers.
89 | assertTrue(storage2.get().isEmpty());
90 |
91 | // Clear store 1.
92 | storage1.clear();
93 | assertTrue(storage1.get().isEmpty());
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/lib/src/test/java/com/google/capillary/TestUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import com.google.crypto.tink.BinaryKeysetReader;
20 | import com.google.crypto.tink.CleartextKeysetHandle;
21 | import com.google.crypto.tink.KeysetHandle;
22 | import com.google.crypto.tink.PublicKeySign;
23 | import com.google.crypto.tink.PublicKeyVerify;
24 | import com.google.crypto.tink.signature.PublicKeySignFactory;
25 | import com.google.crypto.tink.signature.PublicKeyVerifyFactory;
26 | import java.io.ByteArrayOutputStream;
27 | import java.io.IOException;
28 | import java.io.InputStream;
29 | import java.security.GeneralSecurityException;
30 | import java.security.KeyFactory;
31 | import java.security.NoSuchAlgorithmException;
32 | import java.security.PrivateKey;
33 | import java.security.PublicKey;
34 | import java.security.spec.InvalidKeySpecException;
35 | import java.security.spec.PKCS8EncodedKeySpec;
36 | import java.security.spec.X509EncodedKeySpec;
37 |
38 | final class TestUtils {
39 |
40 | static PublicKeySign createTestSenderSigner()
41 | throws GeneralSecurityException, IOException {
42 | KeysetHandle signingKeyHandle = CleartextKeysetHandle
43 | .read(BinaryKeysetReader.withBytes(TestUtils.getBytes("signing_key.dat")));
44 | return PublicKeySignFactory.getPrimitive(signingKeyHandle);
45 | }
46 |
47 | static PublicKeyVerify createTestSenderVerifier()
48 | throws GeneralSecurityException, IOException {
49 | KeysetHandle verificationKeyHandle = CleartextKeysetHandle
50 | .read(BinaryKeysetReader.withBytes(TestUtils.getBytes("verification_key.dat")));
51 | return PublicKeyVerifyFactory.getPrimitive(verificationKeyHandle);
52 | }
53 |
54 | static PublicKey createTestRsaPublicKey()
55 | throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
56 | return KeyFactory.getInstance("RSA").generatePublic(
57 | new X509EncodedKeySpec(TestUtils.getBytes("rsa_public_key.dat")));
58 | }
59 |
60 | static PrivateKey createTestRsaPrivateKey()
61 | throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
62 | return KeyFactory.getInstance("RSA").generatePrivate(
63 | new PKCS8EncodedKeySpec(TestUtils.getBytes("rsa_private_key.dat")));
64 | }
65 |
66 | static byte[] getBytes(String fileName) throws IOException {
67 | InputStream in = null;
68 | try {
69 | in = TestUtils.class.getResourceAsStream(fileName);
70 | ByteArrayOutputStream bout = new ByteArrayOutputStream();
71 | byte[] buf = new byte[1024];
72 | int nread;
73 | while ((nread = in.read(buf)) > 0) {
74 | bout.write(buf, 0, nread);
75 | }
76 | bout.close();
77 | return bout.toByteArray();
78 | } finally {
79 | if (in != null) {
80 | try {
81 | in.close();
82 | } catch (IOException expected) {
83 | // This is expected.
84 | }
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/google/capillary/EncrypterManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import com.google.capillary.internal.CapillaryCiphertext;
20 | import com.google.capillary.internal.CapillaryPublicKey;
21 | import com.google.crypto.tink.HybridEncrypt;
22 | import com.google.protobuf.ByteString;
23 | import com.google.protobuf.InvalidProtocolBufferException;
24 | import java.security.GeneralSecurityException;
25 |
26 | /**
27 | * Encapsulates the process of encrypting plaintexts into Capillary ciphertexts.
28 | *
29 | *
Any class that extends EncrypterManager allows the following usage pattern:
30 | *
36 | */
37 | public abstract class EncrypterManager {
38 |
39 | private boolean isLoaded;
40 | private CapillaryPublicKey capillaryPublicKey;
41 | private HybridEncrypt encrypter;
42 |
43 | /**
44 | * Loads a serialized Capillary public key.
45 | *
46 | * @param publicKey the serialized Capillary public key.
47 | * @throws GeneralSecurityException if the given Capillary public key cannot be loaded.
48 | */
49 | public synchronized void loadPublicKey(byte[] publicKey) throws GeneralSecurityException {
50 | try {
51 | capillaryPublicKey = CapillaryPublicKey.parseFrom(publicKey);
52 | } catch (InvalidProtocolBufferException e) {
53 | throw new GeneralSecurityException("unable to parse public key", e);
54 | }
55 | encrypter = rawLoadPublicKey(capillaryPublicKey.getKeyBytes().toByteArray());
56 | isLoaded = true;
57 | }
58 |
59 | /**
60 | * Creates a {@link HybridEncrypt} for a raw public key embedded in a Capillary public key.
61 | */
62 | abstract HybridEncrypt rawLoadPublicKey(byte[] rawPublicKey) throws GeneralSecurityException;
63 |
64 | /**
65 | * Encryptes the given plaintext into a Capillary ciphertext.
66 | *
67 | * @param data the plaintext.
68 | * @return the Capillary ciphertext.
69 | * @throws GeneralSecurityException if the encryption fails.
70 | */
71 | public synchronized byte[] encrypt(byte[] data) throws GeneralSecurityException {
72 | if (!isLoaded) {
73 | throw new GeneralSecurityException("public key is not loaded");
74 | }
75 | byte[] ciphertext = encrypter.encrypt(data, null);
76 | return CapillaryCiphertext.newBuilder()
77 | .setKeychainUniqueId(capillaryPublicKey.getKeychainUniqueId())
78 | .setKeySerialNumber((capillaryPublicKey.getSerialNumber()))
79 | .setIsAuthKey(capillaryPublicKey.getIsAuth())
80 | .setCiphertext(ByteString.copyFrom(ciphertext))
81 | .build().toByteArray();
82 | }
83 |
84 | /**
85 | * Clears the loaded Capillary public key.
86 | */
87 | public synchronized void clearPublicKey() {
88 | isLoaded = false;
89 | capillaryPublicKey = null;
90 | encrypter = null;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lib/src/test/java/com/google/capillary/RsaEcdsaHybridEncryptTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import static org.junit.Assert.assertArrayEquals;
20 | import static org.junit.Assert.fail;
21 |
22 | import com.google.capillary.RsaEcdsaConstants.Padding;
23 | import com.google.crypto.tink.HybridDecrypt;
24 | import com.google.crypto.tink.HybridEncrypt;
25 | import com.google.crypto.tink.PublicKeySign;
26 | import com.google.crypto.tink.PublicKeyVerify;
27 | import com.google.crypto.tink.subtle.Random;
28 | import java.io.IOException;
29 | import java.security.GeneralSecurityException;
30 | import java.security.PrivateKey;
31 | import java.security.PublicKey;
32 | import org.junit.Test;
33 | import org.junit.runner.RunWith;
34 | import org.junit.runners.JUnit4;
35 |
36 | @RunWith(JUnit4.class)
37 | public final class RsaEcdsaHybridEncryptTest {
38 |
39 | private final PublicKeySign senderSigner;
40 | private final PublicKey recipientPublicKey;
41 |
42 | /**
43 | * Creates a new {@link RsaEcdsaHybridEncryptTest} instance.
44 | */
45 | public RsaEcdsaHybridEncryptTest() throws GeneralSecurityException, IOException {
46 | Config.initialize();
47 | senderSigner = TestUtils.createTestSenderSigner();
48 | recipientPublicKey = TestUtils.createTestRsaPublicKey();
49 | }
50 |
51 | @Test
52 | public void testEncryptDecrypt() throws IOException, GeneralSecurityException {
53 | PublicKeyVerify senderVerifier = TestUtils.createTestSenderVerifier();
54 | PrivateKey recipientPrivateKey = TestUtils.createTestRsaPrivateKey();
55 |
56 | // Try Encryption/Decryption for each padding mode.
57 | for (Padding padding : Padding.values()) {
58 | // Try Encryption/Decryption for plaintext sizes 64, 128, 256, 512, and 1048 bytes.
59 | for (int plaintextSize : new int[]{64, 128, 256, 512, 1048}) {
60 | byte[] plaintext = Random.randBytes(plaintextSize);
61 | HybridEncrypt hybridEncrypt = new RsaEcdsaHybridEncrypt.Builder()
62 | .withSenderSigner(senderSigner)
63 | .withRecipientPublicKey(recipientPublicKey)
64 | .withPadding(padding)
65 | .build();
66 | byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null);
67 | HybridDecrypt hybridDecrypt = new RsaEcdsaHybridDecrypt.Builder()
68 | .withSenderVerifier(senderVerifier)
69 | .withRecipientPrivateKey(recipientPrivateKey)
70 | .withPadding(padding)
71 | .build();
72 | byte[] decryptedPlaintext = hybridDecrypt.decrypt(ciphertext, null);
73 | assertArrayEquals(plaintext, decryptedPlaintext);
74 | }
75 | }
76 | }
77 |
78 | @Test
79 | public void testNonNullContextInfo() {
80 | HybridEncrypt hybridEncrypt = new RsaEcdsaHybridEncrypt.Builder()
81 | .withSenderSigner(senderSigner)
82 | .withRecipientPublicKey(recipientPublicKey)
83 | .withPadding(Padding.PKCS1)
84 | .build();
85 |
86 | byte[] plaintext = Random.randBytes(20);
87 | byte[] contextInfo = new byte[0];
88 |
89 | try {
90 | hybridEncrypt.encrypt(plaintext, contextInfo);
91 | fail("Expected GeneralSecurityException");
92 | } catch (GeneralSecurityException ex) {
93 | // This is expected.
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/lib-android/src/main/java/com/google/capillary/android/Utils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import android.app.KeyguardManager;
20 | import android.content.Context;
21 | import android.os.Build.VERSION;
22 | import android.os.Build.VERSION_CODES;
23 | import com.google.capillary.AuthModeUnavailableException;
24 | import java.io.IOException;
25 | import java.security.GeneralSecurityException;
26 | import java.security.KeyStore;
27 |
28 | /**
29 | * Provides some useful Android-specific helper functions.
30 | */
31 | class Utils {
32 |
33 | private static final String KEYSTORE_ANDROID = "AndroidKeyStore";
34 |
35 | private static Utils instance;
36 |
37 | private Utils() {
38 | }
39 |
40 | /**
41 | * Returns the singleton instance.
42 | */
43 | static synchronized Utils getInstance() {
44 | if (instance == null) {
45 | instance = new Utils();
46 | }
47 | return instance;
48 | }
49 |
50 | /**
51 | * Creates a storage context that is protected by device-specific credentials.
52 | *
53 | *
This method only has an effect on API levels 24 and above.
54 | */
55 | Context getDeviceProtectedStorageContext(Context context) {
56 | if (VERSION.SDK_INT >= VERSION_CODES.N) {
57 | return context.createDeviceProtectedStorageContext();
58 | }
59 | return context;
60 | }
61 |
62 | /**
63 | * Checks if the device supports generating authenticated Capillary keys or throws an exception if
64 | * it doesn't.
65 | */
66 | void checkAuthModeIsAvailable(Context context) throws AuthModeUnavailableException {
67 | boolean isScreenLockEnabled = isScreenLockEnabled(context);
68 | if (isScreenLockEnabled && !isScreenLocked(context)) {
69 | return;
70 | }
71 | if (!isScreenLockEnabled) {
72 | throw new AuthModeUnavailableException(
73 | "the device is not secured with a PIN, pattern, or password");
74 | }
75 | throw new AuthModeUnavailableException("the device is locked");
76 | }
77 |
78 | /**
79 | * Checks if the device screen lock is enabled. Returns the status as a boolean.
80 | */
81 | private boolean isScreenLockEnabled(Context context) {
82 | KeyguardManager keyguardManager =
83 | (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
84 | assert keyguardManager != null;
85 | if (VERSION.SDK_INT >= VERSION_CODES.M) {
86 | return keyguardManager.isDeviceSecure();
87 | }
88 | return keyguardManager.isKeyguardSecure();
89 | }
90 |
91 | /**
92 | * Checks if the device is locked. Returns the status as a boolean.
93 | */
94 | boolean isScreenLocked(Context context) {
95 | KeyguardManager keyguardManager =
96 | (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
97 | assert keyguardManager != null;
98 | if (VERSION.SDK_INT >= VERSION_CODES.M) {
99 | return keyguardManager.isDeviceLocked();
100 | }
101 | return keyguardManager.isKeyguardLocked();
102 | }
103 |
104 | KeyStore loadKeyStore() throws GeneralSecurityException {
105 | KeyStore keyStore = KeyStore.getInstance(KEYSTORE_ANDROID);
106 | try {
107 | keyStore.load(null);
108 | } catch (IOException e) {
109 | throw new GeneralSecurityException("unable to load keystore", e);
110 | }
111 | return keyStore;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/tools/src/main/java/com/google/capillary/tools/EcdsaKeyPairGenerator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.tools;
18 |
19 | import com.google.crypto.tink.BinaryKeysetWriter;
20 | import com.google.crypto.tink.CleartextKeysetHandle;
21 | import com.google.crypto.tink.Config;
22 | import com.google.crypto.tink.KeysetHandle;
23 | import com.google.crypto.tink.proto.KeyTemplate;
24 | import com.google.crypto.tink.signature.SignatureConfig;
25 | import com.google.crypto.tink.signature.SignatureKeyTemplates;
26 | import java.io.File;
27 | import java.io.IOException;
28 | import java.security.GeneralSecurityException;
29 | import org.apache.commons.cli.CommandLine;
30 | import org.apache.commons.cli.CommandLineParser;
31 | import org.apache.commons.cli.DefaultParser;
32 | import org.apache.commons.cli.Option;
33 | import org.apache.commons.cli.Options;
34 | import org.apache.commons.cli.ParseException;
35 |
36 | /**
37 | * A utility program to generate ECDSA public/private key pairs.
38 | *
39 | *
To generate keys, run the program with the following command line arguments:
40 | *
41 | *
ecdsa_public_key_path: The path to store the generated public key.
42 | *
ecdsa_private_key_path: The path to store the generated private key.
43 | *
44 | */
45 | public final class EcdsaKeyPairGenerator {
46 |
47 | private static final String ECDSA_PUBLIC_KEY_PATH_OPTION = "ecdsa_public_key_path";
48 | private static final String ECDSA_PRIVATE_KEY_PATH_OPTION = "ecdsa_private_key_path";
49 | private static final KeyTemplate SIGNATURE_KEY_TEMPLATE = SignatureKeyTemplates.ECDSA_P256;
50 |
51 | /**
52 | * Parses the command line arguments and generates an ECDSA private/private key pair.
53 | */
54 | public static void main(String[] args) throws Exception {
55 | Config.register(SignatureConfig.TINK_1_1_0);
56 | CommandLine commandLine = generateCommandLine(args);
57 | generateEcdsaKeyPair(
58 | new File(commandLine.getOptionValue(ECDSA_PUBLIC_KEY_PATH_OPTION)),
59 | new File(commandLine.getOptionValue(ECDSA_PRIVATE_KEY_PATH_OPTION)));
60 | }
61 |
62 | private static CommandLine generateCommandLine(String[] commandLineArguments)
63 | throws ParseException {
64 | Option ecdsaPublicKeyPath = Option.builder()
65 | .longOpt(ECDSA_PUBLIC_KEY_PATH_OPTION)
66 | .desc("The path to store ECDSA public key.")
67 | .hasArg()
68 | .required()
69 | .build();
70 | Option ecdsaPrivateKeyPath = Option.builder()
71 | .longOpt(ECDSA_PRIVATE_KEY_PATH_OPTION)
72 | .desc("The path to store ECDSA private key.")
73 | .hasArg()
74 | .required()
75 | .build();
76 |
77 | Options options = new Options();
78 | options.addOption(ecdsaPublicKeyPath);
79 | options.addOption(ecdsaPrivateKeyPath);
80 |
81 | CommandLineParser cmdLineParser = new DefaultParser();
82 | return cmdLineParser.parse(options, commandLineArguments);
83 | }
84 |
85 | private static void generateEcdsaKeyPair(File publicKeyFile, File privatekeyFile)
86 | throws GeneralSecurityException, IOException {
87 | KeysetHandle privateKeyHandle = KeysetHandle.generateNew(SIGNATURE_KEY_TEMPLATE);
88 | CleartextKeysetHandle.write(privateKeyHandle, BinaryKeysetWriter.withFile(privatekeyFile));
89 | KeysetHandle publicKeyHandle = privateKeyHandle.getPublicKeysetHandle();
90 | CleartextKeysetHandle.write(publicKeyHandle, BinaryKeysetWriter.withFile(publicKeyFile));
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lib/src/test/java/com/google/capillary/EncrypterManagerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import static org.junit.Assert.assertArrayEquals;
20 | import static org.junit.Assert.assertEquals;
21 | import static org.junit.Assert.fail;
22 |
23 | import com.google.capillary.internal.CapillaryCiphertext;
24 | import com.google.capillary.internal.CapillaryPublicKey;
25 | import com.google.crypto.tink.HybridEncrypt;
26 | import com.google.protobuf.InvalidProtocolBufferException;
27 | import java.security.GeneralSecurityException;
28 | import org.junit.Before;
29 | import org.junit.Test;
30 | import org.junit.runner.RunWith;
31 | import org.junit.runners.JUnit4;
32 |
33 | @RunWith(JUnit4.class)
34 | public final class EncrypterManagerTest {
35 |
36 | private static final String MOCK_CIPHERTEXT = "mock ciphertext";
37 | private static final String PLAINTEXT = "plaintext";
38 |
39 | private final CapillaryPublicKey publicKey = CapillaryPublicKey.newBuilder()
40 | .setKeychainUniqueId("demo_keychain")
41 | .setSerialNumber(22)
42 | .setIsAuth(true)
43 | .build();
44 |
45 | private EncrypterManager encrypterManager;
46 |
47 | /**
48 | * Initializes test case-specific state.
49 | */
50 | @Before
51 | public void setUp() {
52 | encrypterManager = new EncrypterManager() {
53 | @Override
54 | HybridEncrypt rawLoadPublicKey(byte[] rawPublicKey) throws GeneralSecurityException {
55 | return (plaintext, contextInfo) -> MOCK_CIPHERTEXT.getBytes();
56 | }
57 | };
58 | }
59 |
60 | @Test
61 | public void testLoadValidPublicKey() throws GeneralSecurityException {
62 | encrypterManager.loadPublicKey(publicKey.toByteArray());
63 | }
64 |
65 | @Test
66 | public void testLoadMalformedPublicKey() {
67 | byte[] publicKey = "malformed public key".getBytes();
68 |
69 | try {
70 | encrypterManager.loadPublicKey(publicKey);
71 | fail("Did not throw GeneralSecurityException");
72 | } catch (GeneralSecurityException e) {
73 | // This is expected.
74 | }
75 | }
76 |
77 | @Test
78 | public void testClearPublicKey() throws GeneralSecurityException {
79 | encrypterManager.loadPublicKey(publicKey.toByteArray());
80 |
81 | encrypterManager.clearPublicKey();
82 | }
83 |
84 | @Test
85 | public void testEncryptionWithPublicKey()
86 | throws GeneralSecurityException, InvalidProtocolBufferException {
87 | encrypterManager.loadPublicKey(publicKey.toByteArray());
88 |
89 | byte[] capillaryCipher = encrypterManager.encrypt(PLAINTEXT.getBytes());
90 |
91 | CapillaryCiphertext capillaryCipherParsed = CapillaryCiphertext.parseFrom(capillaryCipher);
92 | assertEquals(publicKey.getKeychainUniqueId(), capillaryCipherParsed.getKeychainUniqueId());
93 | assertEquals(publicKey.getSerialNumber(), capillaryCipherParsed.getKeySerialNumber());
94 | assertEquals(publicKey.getIsAuth(), capillaryCipherParsed.getIsAuthKey());
95 | assertArrayEquals(
96 | MOCK_CIPHERTEXT.getBytes(), capillaryCipherParsed.getCiphertext().toByteArray());
97 | }
98 |
99 | @Test
100 | public void testEncryptionWithNoPublicKey() {
101 | try {
102 | encrypterManager.encrypt(PLAINTEXT.getBytes());
103 | fail("Did not throw GeneralSecurityException");
104 | } catch (GeneralSecurityException e) {
105 | // This is expected.
106 | }
107 | }
108 |
109 | @Test
110 | public void testEncryptionWithUnloadedPublicKey() throws GeneralSecurityException {
111 | encrypterManager.loadPublicKey(publicKey.toByteArray());
112 | encrypterManager.clearPublicKey();
113 |
114 | try {
115 | encrypterManager.encrypt(PLAINTEXT.getBytes());
116 | fail("Did not throw GeneralSecurityException");
117 | } catch (GeneralSecurityException e) {
118 | // This is expected.
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/demo/android/src/main/java/com/google/capillary/demo/android/DemoCapillaryHandler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.android;
18 |
19 | import android.content.Context;
20 | import android.util.Log;
21 | import com.google.capillary.android.CapillaryHandler;
22 | import com.google.capillary.android.CapillaryHandlerErrorCode;
23 | import com.google.capillary.demo.common.AddOrUpdatePublicKeyRequest;
24 | import com.google.capillary.demo.common.DemoServiceGrpc;
25 | import com.google.capillary.demo.common.DemoServiceGrpc.DemoServiceBlockingStub;
26 | import com.google.capillary.demo.common.KeyAlgorithm;
27 | import com.google.capillary.demo.common.SecureNotification;
28 | import com.google.capillary.demo.common.SendMessageRequest;
29 | import com.google.protobuf.ByteString;
30 | import com.google.protobuf.InvalidProtocolBufferException;
31 | import io.grpc.ManagedChannel;
32 |
33 | /**
34 | * Implements the {@link CapillaryHandler} interface to register generated Capillary public keys
35 | * with the server and to show decrypted messages as notifications. The communication with the
36 | * application is done via gRPC.
37 | */
38 | final class DemoCapillaryHandler implements CapillaryHandler {
39 |
40 | private static final String TAG = DemoCapillaryHandler.class.getSimpleName();
41 |
42 | private final Context context;
43 | private final DemoServiceBlockingStub blockingStub;
44 |
45 | /**
46 | * Creates a new handler instance.
47 | */
48 | DemoCapillaryHandler(Context context, ManagedChannel channel) {
49 | this.context = context;
50 | this.blockingStub = DemoServiceGrpc.newBlockingStub(channel);
51 | }
52 |
53 | /**
54 | * Shows the decrypted message as an Android notification.
55 | */
56 | @Override
57 | public void handleData(boolean isAuthKey, byte[] data, Object extra) {
58 | try {
59 | SecureNotification secureNotification = SecureNotification.parseFrom(data);
60 | Utils.showNotification(context, secureNotification);
61 | } catch (InvalidProtocolBufferException e) {
62 | e.printStackTrace();
63 | }
64 | }
65 |
66 | /**
67 | * Sends the generated Capillary ciphertext to the server.
68 | */
69 | @Override
70 | public void handlePublicKey(boolean isAuthKey, byte[] publicKey, Object extra) {
71 | AddOrUpdatePublicKeyRequest request = AddOrUpdatePublicKeyRequest.newBuilder()
72 | .setUserId(Utils.getUserId(context))
73 | .setAlgorithm((KeyAlgorithm) extra)
74 | .setIsAuth(isAuthKey)
75 | .setKeyBytes(ByteString.copyFrom(publicKey)).build();
76 | blockingStub.addOrUpdatePublicKey(request);
77 | }
78 |
79 | /**
80 | * This Capillary public key was generated due to a failure in decrypting a Capillary ciphertext.
81 | * Therefore, this method first registers the new Capillary public key with the application
82 | * server, and then requests the Capillary ciphertext to be resend.
83 | */
84 | @Override
85 | public void handlePublicKey(
86 | boolean isAuthKey, byte[] publicKey, byte[] ciphertext, Object extra) {
87 | handlePublicKey(isAuthKey, publicKey, extra);
88 | KeyAlgorithm keyAlgorithm = (KeyAlgorithm) extra;
89 | SendMessageRequest request = SendMessageRequest.newBuilder()
90 | .setUserId(Utils.getUserId(context))
91 | .setKeyAlgorithm(keyAlgorithm)
92 | .setIsAuthKey(isAuthKey)
93 | .setData(Utils.createSecureMessageBytes("retry msg", keyAlgorithm, isAuthKey)).build();
94 | blockingStub.sendMessage(request);
95 | }
96 |
97 | /**
98 | * This indicates that a Capillary ciphertext was saved to be decrypted later.
99 | */
100 | @Override
101 | public void authCiphertextSavedForLater(byte[] ciphertext, Object extra) {
102 | KeyAlgorithm keyAlgorithm = (KeyAlgorithm) extra;
103 | Log.d(TAG, String.format("ciphertext saved: KeyAlgorithm=%s", keyAlgorithm));
104 | }
105 |
106 | /**
107 | * This indicates an error.
108 | */
109 | @Override
110 | public void error(CapillaryHandlerErrorCode errorCode, byte[] ciphertext, Object extra) {
111 | KeyAlgorithm keyAlgorithm = (KeyAlgorithm) extra;
112 | Log.d(TAG, String.format("error occurred: code=%s, KeyAlgorithm=%s", errorCode, keyAlgorithm));
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/demo/server/src/main/java/com/google/capillary/demo/server/DemoDb.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.server;
18 |
19 | import com.google.capillary.NoSuchKeyException;
20 | import com.google.capillary.demo.common.AddOrUpdatePublicKeyRequest;
21 | import com.google.capillary.demo.common.AddOrUpdateUserRequest;
22 | import com.google.capillary.demo.common.KeyAlgorithm;
23 | import java.sql.Connection;
24 | import java.sql.DriverManager;
25 | import java.sql.PreparedStatement;
26 | import java.sql.ResultSet;
27 | import java.sql.SQLException;
28 |
29 | /**
30 | * Provides an interface to store and retrieve app data in a SQLite database.
31 | */
32 | final class DemoDb {
33 |
34 | private static final String CMD_PUT_PUBLIC_KEY =
35 | "INSERT OR REPLACE INTO PublicKeys(user_id, algorithm, is_auth, key_bytes) "
36 | + "VALUES(?, ?, ?, ?)";
37 | private static final String CMD_GET_KEY_BYTES =
38 | "SELECT key_bytes FROM PublicKeys "
39 | + "WHERE user_id = ? AND algorithm = ? AND is_auth = ?";
40 | private static final String CMD_PUT_USER =
41 | "INSERT OR REPLACE INTO Users(user_id, token) "
42 | + "VALUES(?, ?)";
43 | private static final String CMD_GET_TOKEN =
44 | "SELECT token FROM Users "
45 | + "WHERE user_id = ?";
46 |
47 | private final String databasePath;
48 |
49 | /**
50 | * Initializes a new {@link DemoDb}.
51 | */
52 | DemoDb(String databasePath) {
53 | this.databasePath = databasePath;
54 | }
55 |
56 | /**
57 | * Inserts or updates the given recipient Capillary public key in the SQLite database.
58 | */
59 | void addOrUpdatePublicKey(AddOrUpdatePublicKeyRequest request) throws SQLException {
60 | try (
61 | Connection connection = DriverManager.getConnection(databasePath);
62 | PreparedStatement statement = connection.prepareStatement(CMD_PUT_PUBLIC_KEY)
63 | ) {
64 | statement.setString(1, request.getUserId());
65 | statement.setInt(2, request.getAlgorithmValue());
66 | statement.setInt(3, request.getIsAuth() ? 1 : 0);
67 | statement.setBytes(4, request.getKeyBytes().toByteArray());
68 | statement.executeUpdate();
69 | }
70 | }
71 |
72 | /**
73 | * Returns the Capillary public key of the given user and key parameters.
74 | */
75 | byte[] getKeyBytes(String userId, KeyAlgorithm algorithm, boolean isAuth)
76 | throws SQLException, NoSuchKeyException {
77 | try (
78 | Connection connection = DriverManager.getConnection(databasePath);
79 | PreparedStatement statement = connection.prepareStatement(CMD_GET_KEY_BYTES)
80 | ) {
81 | statement.setString(1, userId);
82 | statement.setInt(2, algorithm.getNumber());
83 | statement.setInt(3, isAuth ? 1 : 0);
84 | try (ResultSet resultSet = statement.executeQuery()) {
85 | if (!resultSet.next()) {
86 | throw new NoSuchKeyException(
87 | String.format(
88 | "no public key found for user_id = %s, algorithm = %s, and is_auth = %s",
89 | userId, algorithm.name(), isAuth));
90 | }
91 | return resultSet.getBytes(1);
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * Inserts or updates the given user info in the SQLite database.
98 | */
99 | void addOrUpdateUser(AddOrUpdateUserRequest request) throws SQLException {
100 | try (
101 | Connection connection = DriverManager.getConnection(databasePath);
102 | PreparedStatement statement = connection.prepareStatement(CMD_PUT_USER)
103 | ) {
104 | statement.setString(1, request.getUserId());
105 | statement.setString(2, request.getToken());
106 | statement.executeUpdate();
107 | }
108 | }
109 |
110 | /**
111 | * Returns the FCM token of the given user ID.
112 | */
113 | String getToken(String userId) throws SQLException, NoSuchUserException {
114 | try (
115 | Connection connection = DriverManager.getConnection(databasePath);
116 | PreparedStatement statement = connection.prepareStatement(CMD_GET_TOKEN)
117 | ) {
118 | statement.setString(1, userId);
119 | try (ResultSet resultSet = statement.executeQuery()) {
120 | if (!resultSet.next()) {
121 | throw new NoSuchUserException(String.format("no user found for user_id = %s", userId));
122 | }
123 | return resultSet.getString(1);
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/lib-android/src/androidTest/java/com/google/capillary/android/AndroidKeyStoreRsaUtilsAndroidTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import static org.junit.Assert.assertEquals;
20 | import static org.junit.Assert.assertNotNull;
21 | import static org.junit.Assert.fail;
22 |
23 | import android.content.Context;
24 | import android.support.test.InstrumentationRegistry;
25 | import android.support.test.runner.AndroidJUnit4;
26 | import com.google.capillary.NoSuchKeyException;
27 | import java.security.GeneralSecurityException;
28 | import java.security.InvalidAlgorithmParameterException;
29 | import java.security.KeyStore;
30 | import java.security.KeyStoreException;
31 | import java.security.NoSuchAlgorithmException;
32 | import java.security.NoSuchProviderException;
33 | import java.security.UnrecoverableKeyException;
34 | import org.junit.Before;
35 | import org.junit.Test;
36 | import org.junit.runner.RunWith;
37 |
38 | @RunWith(AndroidJUnit4.class)
39 | public final class AndroidKeyStoreRsaUtilsAndroidTest {
40 |
41 | private static final String KEYCHAIN_ID = "keychain 1";
42 |
43 | private Context context;
44 | private KeyStore keyStore;
45 |
46 | /**
47 | * Initializes test case-specific state.
48 | */
49 | @Before
50 | public void setUp() throws GeneralSecurityException {
51 | keyStore = Utils.getInstance().loadKeyStore();
52 | context = InstrumentationRegistry.getTargetContext();
53 | TestUtils.clearKeyStore();
54 | }
55 |
56 | @Test
57 | public void testGenerateKeyPair()
58 | throws KeyStoreException,
59 | InvalidAlgorithmParameterException,
60 | NoSuchAlgorithmException,
61 | NoSuchProviderException {
62 | String keychainId1 = KEYCHAIN_ID;
63 | final String keychainId2 = "keychain 2";
64 |
65 | assertEquals(0, keyStore.size());
66 |
67 | AndroidKeyStoreRsaUtils.generateKeyPair(context, keychainId1, false);
68 | assertEquals(1, keyStore.size());
69 |
70 | AndroidKeyStoreRsaUtils.generateKeyPair(context, keychainId1, true);
71 | assertEquals(2, keyStore.size());
72 |
73 | AndroidKeyStoreRsaUtils.generateKeyPair(context, keychainId2, false);
74 | assertEquals(3, keyStore.size());
75 |
76 | AndroidKeyStoreRsaUtils.generateKeyPair(context, keychainId2, true);
77 | assertEquals(4, keyStore.size());
78 | }
79 |
80 | @Test
81 | public void testGetPublicKey()
82 | throws KeyStoreException,
83 | InvalidAlgorithmParameterException,
84 | NoSuchAlgorithmException,
85 | NoSuchProviderException,
86 | NoSuchKeyException {
87 | try {
88 | AndroidKeyStoreRsaUtils.getPublicKey(keyStore, KEYCHAIN_ID, false);
89 | fail("Did not throw NoSuchKeyException");
90 | } catch (NoSuchKeyException e) {
91 | // This is expected.
92 | }
93 |
94 | AndroidKeyStoreRsaUtils.generateKeyPair(context, KEYCHAIN_ID, false);
95 |
96 | assertNotNull(AndroidKeyStoreRsaUtils.getPublicKey(keyStore, KEYCHAIN_ID, false));
97 | }
98 |
99 | @Test
100 | public void testGetPrivateKey()
101 | throws UnrecoverableKeyException,
102 | NoSuchAlgorithmException,
103 | KeyStoreException,
104 | NoSuchProviderException,
105 | InvalidAlgorithmParameterException,
106 | NoSuchKeyException {
107 | try {
108 | AndroidKeyStoreRsaUtils.getPrivateKey(keyStore, KEYCHAIN_ID, false);
109 | fail("Did not throw NoSuchKeyException");
110 | } catch (NoSuchKeyException e) {
111 | // This is expected.
112 | }
113 |
114 | AndroidKeyStoreRsaUtils.generateKeyPair(context, KEYCHAIN_ID, false);
115 |
116 | assertNotNull(AndroidKeyStoreRsaUtils.getPrivateKey(keyStore, KEYCHAIN_ID, false));
117 | }
118 |
119 | @Test
120 | public void testDeleteKeyPair()
121 | throws KeyStoreException,
122 | InvalidAlgorithmParameterException,
123 | NoSuchAlgorithmException,
124 | NoSuchProviderException,
125 | NoSuchKeyException {
126 | try {
127 | AndroidKeyStoreRsaUtils.deleteKeyPair(keyStore, KEYCHAIN_ID, false);
128 | fail("Did not throw NoSuchKeyException");
129 | } catch (NoSuchKeyException e) {
130 | // This is expected.
131 | }
132 |
133 | AndroidKeyStoreRsaUtils.generateKeyPair(context, KEYCHAIN_ID, false);
134 | assertEquals(1, keyStore.size());
135 |
136 | AndroidKeyStoreRsaUtils.deleteKeyPair(keyStore, KEYCHAIN_ID, false);
137 | assertEquals(0, keyStore.size());
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/lib/src/test/java/com/google/capillary/RsaEcdsaHybridDecryptTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import static org.junit.Assert.fail;
20 |
21 | import com.google.capillary.RsaEcdsaConstants.Padding;
22 | import com.google.crypto.tink.HybridDecrypt;
23 | import com.google.crypto.tink.HybridEncrypt;
24 | import com.google.crypto.tink.PublicKeySign;
25 | import com.google.crypto.tink.PublicKeyVerify;
26 | import com.google.crypto.tink.subtle.Random;
27 | import java.io.IOException;
28 | import java.security.GeneralSecurityException;
29 | import java.security.PrivateKey;
30 | import java.security.PublicKey;
31 | import java.util.Arrays;
32 | import org.junit.Test;
33 | import org.junit.runner.RunWith;
34 | import org.junit.runners.JUnit4;
35 |
36 | @RunWith(JUnit4.class)
37 | public final class RsaEcdsaHybridDecryptTest {
38 |
39 | private final PublicKeySign senderSigner;
40 | private final PublicKeyVerify senderVerifier;
41 | private final PublicKey recipientPublicKey;
42 | private final PrivateKey recipientPrivateKey;
43 |
44 | /**
45 | * Creates a new {@link RsaEcdsaHybridDecryptTest} instance.
46 | */
47 | public RsaEcdsaHybridDecryptTest() throws GeneralSecurityException, IOException {
48 | Config.initialize();
49 | senderSigner = TestUtils.createTestSenderSigner();
50 | senderVerifier = TestUtils.createTestSenderVerifier();
51 | recipientPublicKey = TestUtils.createTestRsaPublicKey();
52 | recipientPrivateKey = TestUtils.createTestRsaPrivateKey();
53 | }
54 |
55 | @Test
56 | public void testModifyCiphertext() throws GeneralSecurityException {
57 | byte[] plaintext = Random.randBytes(20);
58 |
59 | // Try Encryption/Decryption for each padding mode.
60 | for (Padding padding : Padding.values()) {
61 | HybridEncrypt hybridEncrypt = new RsaEcdsaHybridEncrypt.Builder()
62 | .withSenderSigner(senderSigner)
63 | .withRecipientPublicKey(recipientPublicKey)
64 | .withPadding(padding)
65 | .build();
66 | HybridDecrypt hybridDecrypt = new RsaEcdsaHybridDecrypt.Builder()
67 | .withSenderVerifier(senderVerifier)
68 | .withRecipientPrivateKey(recipientPrivateKey)
69 | .withPadding(padding)
70 | .build();
71 | byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null);
72 |
73 | // Flip bits.
74 | for (int b = 0; b < ciphertext.length; b++) {
75 | for (int bit = 0; bit < 8; bit++) {
76 | byte[] modified = Arrays.copyOf(ciphertext, ciphertext.length);
77 | modified[b] = (byte) (modified[b] ^ (1 << bit));
78 | try {
79 | hybridDecrypt.decrypt(modified, null);
80 | // 3rd and 5th bytes of tink ECDSA signatures are malleable.
81 | int tinkSignatureBytePos = b - RsaEcdsaConstants.SIGNATURE_LENGTH_BYTES_LENGTH + 1;
82 | if (tinkSignatureBytePos == 3 || tinkSignatureBytePos == 5) {
83 | continue;
84 | }
85 | fail("Decrypting modified ciphertext should fail.");
86 | } catch (GeneralSecurityException ex) {
87 | // This is expected.
88 | }
89 | }
90 | }
91 |
92 | // Truncate ciphertext.
93 | for (int length = 0; length < ciphertext.length; length++) {
94 | byte[] modified = Arrays.copyOf(ciphertext, length);
95 | try {
96 | hybridDecrypt.decrypt(modified, null);
97 | fail("Decrypting modified ciphertext should fail");
98 | } catch (GeneralSecurityException ex) {
99 | // This is expected.
100 | }
101 | }
102 | }
103 | }
104 |
105 | @Test
106 | public void testNonNullContextInfo() throws GeneralSecurityException {
107 | HybridEncrypt hybridEncrypt = new RsaEcdsaHybridEncrypt.Builder()
108 | .withSenderSigner(senderSigner)
109 | .withRecipientPublicKey(recipientPublicKey)
110 | .withPadding(Padding.PKCS1)
111 | .build();
112 | HybridDecrypt hybridDecrypt = new RsaEcdsaHybridDecrypt.Builder()
113 | .withSenderVerifier(senderVerifier)
114 | .withRecipientPrivateKey(recipientPrivateKey)
115 | .withPadding(Padding.PKCS1)
116 | .build();
117 |
118 | byte[] plaintext = Random.randBytes(20);
119 | byte[] ciphertext = hybridEncrypt.encrypt(plaintext, null);
120 | byte[] contextInfo = new byte[0];
121 |
122 | try {
123 | hybridDecrypt.decrypt(ciphertext, contextInfo);
124 | fail("Expected GeneralSecurityException");
125 | } catch (GeneralSecurityException ex) {
126 | // This is expected.
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/demo/server/src/main/java/com/google/capillary/demo/server/FcmSender.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.server;
18 |
19 | import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
20 | import com.google.gson.Gson;
21 | import com.google.gson.GsonBuilder;
22 | import com.google.gson.JsonObject;
23 | import java.io.DataOutputStream;
24 | import java.io.FileInputStream;
25 | import java.io.IOException;
26 | import java.io.InputStream;
27 | import java.net.HttpURLConnection;
28 | import java.net.URL;
29 | import java.util.Arrays;
30 | import java.util.Map;
31 | import java.util.Scanner;
32 | import java.util.logging.Logger;
33 |
34 | /**
35 | * Provides an interface to send arbitrary data maps to devices via FCM notifications.
36 | */
37 | final class FcmSender {
38 |
39 | private static final Logger logger = Logger.getLogger(FcmSender.class.getName());
40 | private static final String BASE_URL = "https://fcm.googleapis.com";
41 | private static final String FCM_SEND_ENDPOINT_PATTERN = "/v1/projects/%s/messages:send";
42 | private static final String MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
43 | private static final String[] SCOPES = {MESSAGING_SCOPE};
44 |
45 | private final String fcmSendEndpoint;
46 | private final GoogleCredential googleCredential;
47 |
48 | /**
49 | * Initializes a new {@link FcmSender} with the given FCM service account credentials.
50 | */
51 | FcmSender(String projectId, String serviceAccountCredentialsPath) throws IOException {
52 | fcmSendEndpoint = String.format(FCM_SEND_ENDPOINT_PATTERN, projectId);
53 | try (FileInputStream serviceAccountCredentials =
54 | new FileInputStream(serviceAccountCredentialsPath)) {
55 | googleCredential = GoogleCredential
56 | .fromStream(serviceAccountCredentials)
57 | .createScoped(Arrays.asList(SCOPES));
58 | }
59 | }
60 |
61 | /**
62 | * Sends notification message to FCM for delivery to registered devices.
63 | */
64 | void sendDataMessage(String token, Map dataMap) throws IOException {
65 | JsonObject requestJson = buildRequest(token, dataMap);
66 | logger.info("FCM request JSON for data message:");
67 | prettyPrint(requestJson);
68 | sendMessage(requestJson);
69 | }
70 |
71 | private static JsonObject buildRequest(String token, Map dataMap) {
72 | JsonObject dataJson = new JsonObject();
73 | for (Map.Entry entry : dataMap.entrySet()) {
74 | dataJson.addProperty(entry.getKey(), entry.getValue());
75 | }
76 |
77 | JsonObject messageJson = new JsonObject();
78 | messageJson.addProperty("token", token);
79 | messageJson.add("data", dataJson);
80 |
81 | JsonObject requestJson = new JsonObject();
82 | requestJson.add("message", messageJson);
83 |
84 | return requestJson;
85 | }
86 |
87 | private static void prettyPrint(JsonObject jsonObject) {
88 | Gson gson = new GsonBuilder().setPrettyPrinting().create();
89 | logger.info(gson.toJson(jsonObject));
90 | }
91 |
92 | private void sendMessage(JsonObject requestJson) throws IOException {
93 | HttpURLConnection connection = getConnection();
94 | connection.setDoOutput(true);
95 | DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
96 | outputStream.writeBytes(requestJson.toString());
97 | outputStream.flush();
98 | outputStream.close();
99 |
100 | int responseCode = connection.getResponseCode();
101 | if (responseCode == 200) {
102 | String response = inputstreamToString(connection.getInputStream());
103 | logger.info("Message sent to Firebase for delivery, response:");
104 | logger.info(response);
105 | } else {
106 | logger.info("Unable to send message to Firebase:");
107 | String response = inputstreamToString(connection.getErrorStream());
108 | logger.info(response);
109 | }
110 | }
111 |
112 | private HttpURLConnection getConnection() throws IOException {
113 | URL url = new URL(BASE_URL + fcmSendEndpoint);
114 | HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
115 | httpUrlConnection.setRequestProperty("Authorization", "Bearer " + getAccessToken());
116 | httpUrlConnection.setRequestProperty("Content-Type", "application/json; UTF-8");
117 | return httpUrlConnection;
118 | }
119 |
120 | private static String inputstreamToString(InputStream inputStream) throws IOException {
121 | StringBuilder stringBuilder = new StringBuilder();
122 | Scanner scanner = new Scanner(inputStream);
123 | while (scanner.hasNext()) {
124 | stringBuilder.append(scanner.nextLine());
125 | }
126 | return stringBuilder.toString();
127 | }
128 |
129 | private String getAccessToken() throws IOException {
130 | googleCredential.refreshToken();
131 | return googleCredential.getAccessToken();
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/lib/src/main/java/com/google/capillary/HybridRsaUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary;
18 |
19 | import com.google.capillary.RsaEcdsaConstants.Padding;
20 | import com.google.capillary.internal.HybridRsaCiphertext;
21 | import com.google.crypto.tink.Aead;
22 | import com.google.crypto.tink.BinaryKeysetReader;
23 | import com.google.crypto.tink.BinaryKeysetWriter;
24 | import com.google.crypto.tink.CleartextKeysetHandle;
25 | import com.google.crypto.tink.KeysetHandle;
26 | import com.google.crypto.tink.aead.AeadFactory;
27 | import com.google.crypto.tink.aead.AeadKeyTemplates;
28 | import com.google.crypto.tink.proto.KeyTemplate;
29 | import com.google.protobuf.ByteString;
30 | import com.google.protobuf.InvalidProtocolBufferException;
31 | import java.io.ByteArrayOutputStream;
32 | import java.io.IOException;
33 | import java.security.GeneralSecurityException;
34 | import java.security.PrivateKey;
35 | import java.security.PublicKey;
36 | import javax.crypto.Cipher;
37 | import javax.crypto.spec.OAEPParameterSpec;
38 |
39 | public final class HybridRsaUtils {
40 |
41 | private static final KeyTemplate SYMMETRIC_KEY_TEMPLATE = AeadKeyTemplates.AES128_GCM;
42 | private static final byte[] emptyEad = new byte[0];
43 |
44 | /**
45 | * Encrypts the given plaintext using RSA hybrid encryption.
46 | *
47 | * @param plaintext the plaintext to encrypt.
48 | * @param publicKey the RSA public key.
49 | * @param padding the RSA padding to use.
50 | * @param oaepParams the {@link OAEPParameterSpec} to use for OAEP padding.
51 | * @return the ciphertext.
52 | * @throws GeneralSecurityException if encryption fails.
53 | */
54 | public static byte[] encrypt(
55 | byte[] plaintext, PublicKey publicKey, Padding padding, OAEPParameterSpec oaepParams)
56 | throws GeneralSecurityException {
57 | // Initialize RSA encryption cipher.
58 | Cipher rsaCipher = Cipher.getInstance(padding.getTransformation());
59 | if (padding == Padding.OAEP) {
60 | rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParams);
61 | } else {
62 | rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
63 | }
64 |
65 | // Generate symmetric key and its ciphertext.
66 | KeysetHandle symmetricKeyHandle = KeysetHandle.generateNew(SYMMETRIC_KEY_TEMPLATE);
67 | ByteArrayOutputStream symmetricKeyOutputStream = new ByteArrayOutputStream();
68 | try {
69 | CleartextKeysetHandle.write(
70 | symmetricKeyHandle, BinaryKeysetWriter.withOutputStream(symmetricKeyOutputStream));
71 | } catch (IOException e) {
72 | throw new GeneralSecurityException("hybrid rsa encryption failed: ", e);
73 | }
74 | byte[] symmetricKeyBytes = symmetricKeyOutputStream.toByteArray();
75 | byte[] symmetricKeyCiphertext = rsaCipher.doFinal(symmetricKeyBytes);
76 |
77 | // Generate payload ciphertext.
78 | Aead aead = AeadFactory.getPrimitive(symmetricKeyHandle);
79 | byte[] payloadCiphertext = aead.encrypt(plaintext, emptyEad);
80 |
81 | return HybridRsaCiphertext.newBuilder()
82 | .setSymmetricKeyCiphertext(ByteString.copyFrom(symmetricKeyCiphertext))
83 | .setPayloadCiphertext(ByteString.copyFrom(payloadCiphertext))
84 | .build().toByteArray();
85 | }
86 |
87 | /**
88 | * Decrypts the given ciphertext using RSA hybrid decryption.
89 | *
90 | * @param ciphertext the ciphertext to decrypt.
91 | * @param privateKey the RSA private key.
92 | * @param padding the RSA padding to use.
93 | * @param oaepParams the {@link OAEPParameterSpec} to use for OAEP padding.
94 | * @return the plaintext.
95 | * @throws GeneralSecurityException if decryption fails.
96 | */
97 | public static byte[] decrypt(
98 | byte[] ciphertext, PrivateKey privateKey, Padding padding, OAEPParameterSpec oaepParams)
99 | throws GeneralSecurityException {
100 | // Parse encrypted payload bytes.
101 | HybridRsaCiphertext hybridRsaCiphertext;
102 | try {
103 | hybridRsaCiphertext = HybridRsaCiphertext.parseFrom(ciphertext);
104 | } catch (InvalidProtocolBufferException e) {
105 | throw new GeneralSecurityException("hybrid rsa decryption failed: ", e);
106 | }
107 |
108 | // Initialize RSA decryption cipher.
109 | Cipher rsaCipher = Cipher.getInstance(padding.getTransformation());
110 | if (padding == Padding.OAEP) {
111 | rsaCipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParams);
112 | } else {
113 | rsaCipher.init(Cipher.DECRYPT_MODE, privateKey);
114 | }
115 |
116 | // Retrieve symmetric key.
117 | byte[] symmetricKeyCiphertext = hybridRsaCiphertext.getSymmetricKeyCiphertext().toByteArray();
118 | byte[] symmetricKeyBytes = rsaCipher.doFinal(symmetricKeyCiphertext);
119 | KeysetHandle symmetricKeyHandle;
120 | try {
121 | symmetricKeyHandle =
122 | CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(symmetricKeyBytes));
123 | } catch (IOException e) {
124 | throw new GeneralSecurityException("hybrid rsa decryption failed: ", e);
125 | }
126 |
127 | // Retrieve and return plaintext.
128 | Aead aead = AeadFactory.getPrimitive(symmetricKeyHandle);
129 | byte[] payloadCiphertext = hybridRsaCiphertext.getPayloadCiphertext().toByteArray();
130 | return aead.decrypt(payloadCiphertext, emptyEad);
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/lib-android/src/main/java/com/google/capillary/android/RsaEcdsaKeyManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import android.content.Context;
20 | import com.google.capillary.NoSuchKeyException;
21 | import com.google.capillary.RsaEcdsaHybridDecrypt;
22 | import com.google.capillary.internal.WrappedRsaEcdsaPublicKey;
23 | import com.google.crypto.tink.BinaryKeysetReader;
24 | import com.google.crypto.tink.CleartextKeysetHandle;
25 | import com.google.crypto.tink.HybridDecrypt;
26 | import com.google.crypto.tink.KeysetHandle;
27 | import com.google.crypto.tink.PublicKeyVerify;
28 | import com.google.crypto.tink.signature.PublicKeyVerifyFactory;
29 | import com.google.protobuf.ByteString;
30 | import java.io.IOException;
31 | import java.io.InputStream;
32 | import java.security.GeneralSecurityException;
33 | import java.security.InvalidAlgorithmParameterException;
34 | import java.security.KeyStore;
35 | import java.security.NoSuchAlgorithmException;
36 | import java.security.NoSuchProviderException;
37 | import java.security.PrivateKey;
38 | import java.util.HashMap;
39 | import java.util.Map;
40 |
41 | /**
42 | * An implementation of {@link KeyManager} that supports RSA-ECDSA keys.
43 | */
44 | public final class RsaEcdsaKeyManager extends KeyManager {
45 |
46 | // This prefix should be unique to each implementation of KeyManager.
47 | private static final String KEY_CHAIN_ID_PREFIX = "rsa_ecdsa_";
48 |
49 | private static Map instances = new HashMap<>();
50 |
51 | private final KeyStore keyStore;
52 |
53 | private PublicKeyVerify senderVerifier;
54 |
55 | private RsaEcdsaKeyManager(
56 | Context context, Utils utils, String keychainId, InputStream senderVerificationKey)
57 | throws GeneralSecurityException, IOException {
58 | super(context, utils, KEY_CHAIN_ID_PREFIX + keychainId);
59 | KeysetHandle verificationKeyHandle = CleartextKeysetHandle
60 | .read(BinaryKeysetReader.withInputStream(senderVerificationKey));
61 | senderVerifier = PublicKeyVerifyFactory.getPrimitive(verificationKeyHandle);
62 | keyStore = utils.loadKeyStore();
63 | }
64 |
65 | /**
66 | * Returns the singleton {@link RsaEcdsaKeyManager} instance for the given keychain ID.
67 | *
68 | *
Please note that the {@link InputStream} {@code senderVerificationKey} will not be closed.
69 | *
70 | * @param context the app context.
71 | * @param keychainId the ID of the key manager.
72 | * @param senderVerificationKey the sender's ECDSA verification key.
73 | * @return the singleton {@link RsaEcdsaKeyManager} instance.
74 | * @throws GeneralSecurityException if the ECDSA verification key could not be initialized.
75 | * @throws IOException if the ECDSA verification key could not be read.
76 | */
77 | public static synchronized RsaEcdsaKeyManager getInstance(
78 | Context context, String keychainId, InputStream senderVerificationKey)
79 | throws GeneralSecurityException, IOException {
80 | if (instances.containsKey(keychainId)) {
81 | RsaEcdsaKeyManager instance = instances.get(keychainId);
82 | instance.updateSenderVerifier(senderVerificationKey);
83 | return instance;
84 | }
85 | RsaEcdsaKeyManager newInstance =
86 | new RsaEcdsaKeyManager(context, Utils.getInstance(), keychainId, senderVerificationKey);
87 | instances.put(keychainId, newInstance);
88 | return newInstance;
89 | }
90 |
91 | private synchronized void updateSenderVerifier(InputStream senderVerificationKey)
92 | throws GeneralSecurityException, IOException {
93 | KeysetHandle verificationKeyHandle = CleartextKeysetHandle
94 | .read(BinaryKeysetReader.withInputStream(senderVerificationKey));
95 | senderVerifier = PublicKeyVerifyFactory.getPrimitive(verificationKeyHandle);
96 | }
97 |
98 | @Override
99 | synchronized void rawGenerateKeyPair(boolean isAuth)
100 | throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
101 | AndroidKeyStoreRsaUtils.generateKeyPair(context, keychainId, isAuth);
102 | }
103 |
104 | @Override
105 | synchronized byte[] rawGetPublicKey(boolean isAuth)
106 | throws NoSuchKeyException, GeneralSecurityException {
107 | byte[] publicKeyBytes =
108 | AndroidKeyStoreRsaUtils.getPublicKey(keyStore, keychainId, isAuth).getEncoded();
109 | return WrappedRsaEcdsaPublicKey.newBuilder()
110 | .setPadding(AndroidKeyStoreRsaUtils.getCompatibleRsaPadding().name())
111 | .setKeyBytes(ByteString.copyFrom(publicKeyBytes))
112 | .build().toByteArray();
113 | }
114 |
115 | @Override
116 | synchronized HybridDecrypt rawGetDecrypter(boolean isAuth)
117 | throws NoSuchKeyException, GeneralSecurityException {
118 | PrivateKey recipientPrivateKey =
119 | AndroidKeyStoreRsaUtils.getPrivateKey(keyStore, keychainId, isAuth);
120 | return new RsaEcdsaHybridDecrypt.Builder()
121 | .withRecipientPrivateKey(recipientPrivateKey)
122 | .withSenderVerifier(senderVerifier)
123 | .withPadding(AndroidKeyStoreRsaUtils.getCompatibleRsaPadding())
124 | .build();
125 | }
126 |
127 | @Override
128 | synchronized void rawDeleteKeyPair(boolean isAuth)
129 | throws NoSuchKeyException, GeneralSecurityException {
130 | AndroidKeyStoreRsaUtils.deleteKeyPair(keyStore, keychainId, isAuth);
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/demo/server/src/main/java/com/google/capillary/demo/server/DemoServiceImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.server;
18 |
19 | import com.google.capillary.EncrypterManager;
20 | import com.google.capillary.NoSuchKeyException;
21 | import com.google.capillary.demo.common.AddOrUpdatePublicKeyRequest;
22 | import com.google.capillary.demo.common.AddOrUpdateUserRequest;
23 | import com.google.capillary.demo.common.Constants;
24 | import com.google.capillary.demo.common.DemoServiceGrpc;
25 | import com.google.capillary.demo.common.KeyAlgorithm;
26 | import com.google.capillary.demo.common.SendMessageRequest;
27 | import com.google.crypto.tink.subtle.Base64;
28 | import com.google.protobuf.Empty;
29 | import io.grpc.Status;
30 | import io.grpc.stub.StreamObserver;
31 | import java.io.IOException;
32 | import java.security.GeneralSecurityException;
33 | import java.sql.SQLException;
34 | import java.util.HashMap;
35 | import java.util.Map;
36 | import java.util.concurrent.Executors;
37 | import java.util.concurrent.ScheduledExecutorService;
38 | import java.util.concurrent.TimeUnit;
39 | import java.util.logging.Logger;
40 |
41 | /**
42 | * The implementation of DemoService service.
43 | *
44 | *
See capillary_demo_common.proto for details of the methods.
45 | */
46 | final class DemoServiceImpl extends DemoServiceGrpc.DemoServiceImplBase {
47 |
48 | private static final Logger logger = Logger.getLogger(DemoServiceImpl.class.getName());
49 |
50 | private final DemoDb db;
51 | private final EncrypterManager rsaEcdsaEncrypterManager;
52 | private final EncrypterManager webPushEncrypterManager;
53 | private final FcmSender fcmSender;
54 | private final ScheduledExecutorService executorService =
55 | Executors.newSingleThreadScheduledExecutor();
56 |
57 | /**
58 | * Initialize gRPC service implementation.
59 | */
60 | DemoServiceImpl(
61 | DemoDb db,
62 | EncrypterManager rsaEcdsaEncrypterManager,
63 | EncrypterManager webPushEncrypterManager,
64 | FcmSender fcmSender) {
65 | this.db = db;
66 | this.rsaEcdsaEncrypterManager = rsaEcdsaEncrypterManager;
67 | this.webPushEncrypterManager = webPushEncrypterManager;
68 | this.fcmSender = fcmSender;
69 | }
70 |
71 | @Override
72 | public void addOrUpdateUser(AddOrUpdateUserRequest req, StreamObserver responseObserver) {
73 | logger.info("addOrUpdateUser called with:");
74 | logger.info(req.toString());
75 |
76 | try {
77 | db.addOrUpdateUser(req);
78 | } catch (SQLException e) {
79 | e.printStackTrace();
80 | responseObserver.onError(
81 | Status.INTERNAL.withDescription("unable to add user").asException());
82 | return;
83 | }
84 |
85 | responseObserver.onNext(Empty.getDefaultInstance());
86 | responseObserver.onCompleted();
87 | }
88 |
89 | @Override
90 | public void addOrUpdatePublicKey(
91 | AddOrUpdatePublicKeyRequest req, StreamObserver responseObserver) {
92 | logger.info("addOrUpdatePublicKey called with:");
93 | logger.info(req.toString());
94 |
95 | try {
96 | db.addOrUpdatePublicKey(req);
97 | } catch (SQLException e) {
98 | e.printStackTrace();
99 | responseObserver.onError(
100 | Status.INTERNAL.withDescription("unable to add public key").asException());
101 | return;
102 | }
103 |
104 | responseObserver.onNext(Empty.getDefaultInstance());
105 | responseObserver.onCompleted();
106 | }
107 |
108 | @Override
109 | public void sendMessage(SendMessageRequest req, StreamObserver responseObserver) {
110 | logger.info("sendMessage called with:");
111 | logger.info(req.toString());
112 |
113 | try {
114 | // Get public key.
115 | byte[] publicKey = db.getKeyBytes(req.getUserId(), req.getKeyAlgorithm(), req.getIsAuthKey());
116 |
117 | // Generate ciphertext.
118 | EncrypterManager encrypterManager = getEncrypterManager(req.getKeyAlgorithm());
119 | encrypterManager.loadPublicKey(publicKey);
120 | byte[] ciphertext = encrypterManager.encrypt(req.getData().toByteArray());
121 | encrypterManager.clearPublicKey();
122 | String ciphertextString = Base64.encode(ciphertext);
123 |
124 | // Get FCM token.
125 | String token = db.getToken(req.getUserId());
126 |
127 | // Create the data map to be sent as a JSON object.
128 | Map dataMap = new HashMap<>();
129 | dataMap.put(Constants.CAPILLARY_CIPHERTEXT_KEY, ciphertextString);
130 | dataMap.put(Constants.CAPILLARY_KEY_ALGORITHM_KEY, req.getKeyAlgorithm().name());
131 |
132 | // Send the data map after the requested delay.
133 | executorService.schedule(() -> {
134 | try {
135 | fcmSender.sendDataMessage(token, dataMap);
136 | } catch (IOException e) {
137 | e.printStackTrace();
138 | }
139 | }, req.getDelaySeconds(), TimeUnit.SECONDS);
140 | } catch (NoSuchUserException | NoSuchKeyException e) {
141 | e.printStackTrace();
142 | responseObserver.onError(Status.NOT_FOUND.withDescription(e.getMessage()).asException());
143 | return;
144 | } catch (GeneralSecurityException | SQLException e) {
145 | e.printStackTrace();
146 | responseObserver.onError(
147 | Status.INTERNAL.withDescription("unable to send message").asException());
148 | return;
149 | }
150 |
151 | responseObserver.onNext(Empty.getDefaultInstance());
152 | responseObserver.onCompleted();
153 | }
154 |
155 | private EncrypterManager getEncrypterManager(KeyAlgorithm keyAlgorithm) {
156 | switch (keyAlgorithm) {
157 | case RSA_ECDSA:
158 | return rsaEcdsaEncrypterManager;
159 | case WEB_PUSH:
160 | return webPushEncrypterManager;
161 | default:
162 | throw new IllegalArgumentException("unsupported key algorithm");
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/lib-android/src/main/java/com/google/capillary/android/AndroidKeyStoreRsaUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import android.content.Context;
20 | import android.os.Build.VERSION;
21 | import android.os.Build.VERSION_CODES;
22 | import android.security.KeyPairGeneratorSpec;
23 | import android.security.keystore.KeyGenParameterSpec;
24 | import android.security.keystore.KeyProperties;
25 | import com.google.capillary.NoSuchKeyException;
26 | import com.google.capillary.RsaEcdsaConstants.Padding;
27 | import java.math.BigInteger;
28 | import java.security.InvalidAlgorithmParameterException;
29 | import java.security.KeyPairGenerator;
30 | import java.security.KeyStore;
31 | import java.security.KeyStoreException;
32 | import java.security.NoSuchAlgorithmException;
33 | import java.security.NoSuchProviderException;
34 | import java.security.PrivateKey;
35 | import java.security.PublicKey;
36 | import java.security.UnrecoverableKeyException;
37 | import java.security.spec.AlgorithmParameterSpec;
38 | import java.security.spec.RSAKeyGenParameterSpec;
39 | import javax.security.auth.x500.X500Principal;
40 | import org.joda.time.LocalDate;
41 |
42 | /**
43 | * AndroidKeyStoreRsaUtils provides utility methods to generate RSA key pairs in Android Keystore
44 | * and perform crypto operations with those keys. Currently, this class supports Android API levels
45 | * 19-27. Support for Android API levels 28+ (e.g., StrongBox Keymaster) will be added as those API
46 | * levels are publicly released.
47 | */
48 | final class AndroidKeyStoreRsaUtils {
49 |
50 | private static final String AUTH_KEY_ALIAS_SUFFIX = "_capillary_rsa_auth";
51 | private static final String NO_AUTH_KEY_ALIAS_SUFFIX = "_capillary_rsa_no_auth";
52 | private static final String KEYSTORE_ANDROID = "AndroidKeyStore";
53 | private static final int KEY_SIZE = 2048;
54 | private static final int KEY_DURATION_YEARS = 100;
55 | // Allow any screen unlock event to be valid for up to 1 hour.
56 | private static final int UNLOCK_DURATION_SECONDS = 60 * 60;
57 |
58 | static void generateKeyPair(Context context, String keychainId, boolean isAuth)
59 | throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
60 | String keyAlias = toKeyAlias(keychainId, isAuth);
61 | RSAKeyGenParameterSpec rsaSpec =
62 | new RSAKeyGenParameterSpec(KEY_SIZE, RSAKeyGenParameterSpec.F4);
63 | AlgorithmParameterSpec spec;
64 |
65 | // API levels 23 and above should use KeyGenParameterSpec to build RSA keys.
66 | if (VERSION.SDK_INT >= VERSION_CODES.M) {
67 | KeyGenParameterSpec.Builder specBuilder =
68 | new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_DECRYPT)
69 | .setAlgorithmParameterSpec(rsaSpec)
70 | .setDigests(KeyProperties.DIGEST_SHA256)
71 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP);
72 | if (isAuth) {
73 | specBuilder.setUserAuthenticationRequired(true);
74 | specBuilder.setUserAuthenticationValidityDurationSeconds(UNLOCK_DURATION_SECONDS);
75 | }
76 | spec = specBuilder.build();
77 | } else { // API levels 22 and below have to use KeyPairGeneratorSpec to build RSA keys.
78 | LocalDate startDate = LocalDate.now();
79 | LocalDate endDate = startDate.plusYears(KEY_DURATION_YEARS);
80 | KeyPairGeneratorSpec.Builder specBuilder = new KeyPairGeneratorSpec.Builder(context)
81 | .setAlias(keyAlias)
82 | .setSubject(new X500Principal("CN=" + keyAlias))
83 | .setSerialNumber(BigInteger.ONE)
84 | .setStartDate(startDate.toDate())
85 | .setEndDate(endDate.toDate());
86 | // Only API levels 19 and above allow specifying RSA key parameters.
87 | if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
88 | specBuilder.setAlgorithmParameterSpec(rsaSpec);
89 | specBuilder.setKeySize(KEY_SIZE);
90 | }
91 | if (isAuth) {
92 | specBuilder.setEncryptionRequired();
93 | }
94 | spec = specBuilder.build();
95 | }
96 |
97 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", KEYSTORE_ANDROID);
98 | keyPairGenerator.initialize(spec);
99 | keyPairGenerator.generateKeyPair();
100 | }
101 |
102 | static PublicKey getPublicKey(KeyStore keyStore, String keychainId, boolean isAuth)
103 | throws NoSuchKeyException, KeyStoreException {
104 | String alias = toKeyAlias(keychainId, isAuth);
105 | checkKeyExists(keyStore, alias);
106 | return keyStore.getCertificate(alias).getPublicKey();
107 | }
108 |
109 | static PrivateKey getPrivateKey(KeyStore keyStore, String keychainId, boolean isAuth)
110 | throws UnrecoverableKeyException,
111 | NoSuchAlgorithmException,
112 | KeyStoreException,
113 | NoSuchKeyException {
114 | String alias = toKeyAlias(keychainId, isAuth);
115 | checkKeyExists(keyStore, alias);
116 | return (PrivateKey) keyStore.getKey(alias, null);
117 | }
118 |
119 | static void deleteKeyPair(KeyStore keyStore, String keychainId, boolean isAuth)
120 | throws NoSuchKeyException, KeyStoreException {
121 | String alias = toKeyAlias(keychainId, isAuth);
122 | checkKeyExists(keyStore, alias);
123 | keyStore.deleteEntry(alias);
124 | }
125 |
126 | private static String toKeyAlias(String keychainId, boolean isAuth) {
127 | String suffix = isAuth ? AUTH_KEY_ALIAS_SUFFIX : NO_AUTH_KEY_ALIAS_SUFFIX;
128 | return keychainId + suffix;
129 | }
130 |
131 | static void checkKeyExists(KeyStore keyStore, String keychainId, boolean isAuth)
132 | throws NoSuchKeyException, KeyStoreException {
133 | checkKeyExists(keyStore, toKeyAlias(keychainId, isAuth));
134 | }
135 |
136 | private static void checkKeyExists(KeyStore keyStore, String alias)
137 | throws NoSuchKeyException, KeyStoreException {
138 | if (!keyStore.containsAlias(alias)) {
139 | throw new NoSuchKeyException("android key store has no rsa key pair with alias " + alias);
140 | }
141 | }
142 |
143 | static Padding getCompatibleRsaPadding() {
144 | return VERSION.SDK_INT >= VERSION_CODES.M ? Padding.OAEP : Padding.PKCS1;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/lib-android/src/androidTest/java/com/google/capillary/android/TestHandler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import java.util.Arrays;
20 | import java.util.LinkedList;
21 | import java.util.List;
22 |
23 | /**
24 | * Implementation of {@link CapillaryHandler} for integration testing.
25 | */
26 | final class TestHandler implements CapillaryHandler {
27 |
28 | List handleDataRequests = new LinkedList<>();
29 | List handlePublicKeyRequests = new LinkedList<>();
30 | List authCiphertextSavedForLaterRequests = new LinkedList<>();
31 | List errorRequests = new LinkedList<>();
32 |
33 | static class HandleDataRequest {
34 |
35 | boolean isAuthKey;
36 | byte[] data;
37 | Object extra;
38 |
39 | HandleDataRequest(boolean isAuthKey, byte[] data, Object extra) {
40 | this.isAuthKey = isAuthKey;
41 | this.data = data;
42 | this.extra = extra;
43 | }
44 |
45 | @Override
46 | public int hashCode() {
47 | int result = (isAuthKey ? 1 : 0);
48 | result = 31 * result + Arrays.hashCode(data);
49 | result = 31 * result + extra.hashCode();
50 | return result;
51 | }
52 |
53 | @Override
54 | public boolean equals(Object o) {
55 | if (this == o) {
56 | return true;
57 | }
58 | if (o == null || getClass() != o.getClass()) {
59 | return false;
60 | }
61 |
62 | HandleDataRequest that = (HandleDataRequest) o;
63 |
64 | return isAuthKey == that.isAuthKey
65 | && Arrays.equals(data, that.data)
66 | && extra.equals(that.extra);
67 | }
68 | }
69 |
70 | static class HandlePublicKeyRequest {
71 |
72 | boolean isAuthKey;
73 | byte[] publicKey;
74 | byte[] ciphertext;
75 | Object extra;
76 |
77 | HandlePublicKeyRequest(
78 | boolean isAuthKey, byte[] publicKey, byte[] ciphertext, Object extra) {
79 | this(isAuthKey, publicKey, extra);
80 | this.ciphertext = ciphertext;
81 | }
82 |
83 | HandlePublicKeyRequest(boolean isAuthKey, byte[] publicKey, Object extra) {
84 | this.isAuthKey = isAuthKey;
85 | this.publicKey = publicKey;
86 | this.extra = extra;
87 | }
88 |
89 | @Override
90 | public boolean equals(Object o) {
91 | if (this == o) {
92 | return true;
93 | }
94 | if (o == null || getClass() != o.getClass()) {
95 | return false;
96 | }
97 |
98 | HandlePublicKeyRequest that = (HandlePublicKeyRequest) o;
99 |
100 | return isAuthKey == that.isAuthKey
101 | && Arrays.equals(publicKey, that.publicKey)
102 | && Arrays.equals(ciphertext, that.ciphertext)
103 | && extra.equals(that.extra);
104 | }
105 |
106 | @Override
107 | public int hashCode() {
108 | int result = (isAuthKey ? 1 : 0);
109 | result = 31 * result + Arrays.hashCode(publicKey);
110 | result = 31 * result + Arrays.hashCode(ciphertext);
111 | result = 31 * result + extra.hashCode();
112 | return result;
113 | }
114 | }
115 |
116 | static class AuthCiphertextSavedForLaterRequest {
117 |
118 | byte[] ciphertext;
119 | Object extra;
120 |
121 | AuthCiphertextSavedForLaterRequest(byte[] ciphertext, Object extra) {
122 | this.ciphertext = ciphertext;
123 | this.extra = extra;
124 | }
125 |
126 | @Override
127 | public boolean equals(Object o) {
128 | if (this == o) {
129 | return true;
130 | }
131 | if (o == null || getClass() != o.getClass()) {
132 | return false;
133 | }
134 |
135 | AuthCiphertextSavedForLaterRequest that = (AuthCiphertextSavedForLaterRequest) o;
136 |
137 | return Arrays.equals(ciphertext, that.ciphertext) && extra.equals(that.extra);
138 | }
139 |
140 | @Override
141 | public int hashCode() {
142 | int result = Arrays.hashCode(ciphertext);
143 | result = 31 * result + extra.hashCode();
144 | return result;
145 | }
146 | }
147 |
148 | static class ErrorRequest {
149 |
150 | CapillaryHandlerErrorCode errorCode;
151 | byte[] ciphertext;
152 | Object extra;
153 |
154 | ErrorRequest(CapillaryHandlerErrorCode errorCode, byte[] ciphertext, Object extra) {
155 | this.errorCode = errorCode;
156 | this.ciphertext = ciphertext;
157 | this.extra = extra;
158 | }
159 |
160 | @Override
161 | public boolean equals(Object o) {
162 | if (this == o) {
163 | return true;
164 | }
165 | if (o == null || getClass() != o.getClass()) {
166 | return false;
167 | }
168 |
169 | ErrorRequest that = (ErrorRequest) o;
170 |
171 | return errorCode == that.errorCode
172 | && Arrays.equals(ciphertext, that.ciphertext)
173 | && extra.equals(that.extra);
174 | }
175 |
176 | @Override
177 | public int hashCode() {
178 | int result = errorCode.hashCode();
179 | result = 31 * result + Arrays.hashCode(ciphertext);
180 | result = 31 * result + extra.hashCode();
181 | return result;
182 | }
183 | }
184 |
185 | @Override
186 | public void handleData(boolean isAuthKey, byte[] data, Object extra) {
187 | handleDataRequests.add(new HandleDataRequest(isAuthKey, data, extra));
188 | }
189 |
190 | @Override
191 | public void handlePublicKey(boolean isAuthKey, byte[] publicKey, Object extra) {
192 | handlePublicKeyRequests.add(new HandlePublicKeyRequest(isAuthKey, publicKey, extra));
193 | }
194 |
195 | @Override
196 | public void handlePublicKey(
197 | boolean isAuthKey, byte[] publicKey, byte[] ciphertext, Object extra) {
198 | handlePublicKeyRequests.add(
199 | new HandlePublicKeyRequest(isAuthKey, publicKey, ciphertext, extra));
200 | }
201 |
202 | @Override
203 | public void authCiphertextSavedForLater(byte[] ciphertext, Object extra) {
204 | authCiphertextSavedForLaterRequests
205 | .add(new AuthCiphertextSavedForLaterRequest(ciphertext, extra));
206 | }
207 |
208 | @Override
209 | public void error(CapillaryHandlerErrorCode errorCode, byte[] ciphertext, Object extra) {
210 | errorRequests.add(new ErrorRequest(errorCode, ciphertext, extra));
211 | }
212 |
213 | void reset() {
214 | handleDataRequests.clear();
215 | handlePublicKeyRequests.clear();
216 | authCiphertextSavedForLaterRequests.clear();
217 | errorRequests.clear();
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/demo/server/src/main/java/com/google/capillary/demo/server/DemoServer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.demo.server;
18 |
19 | import com.google.capillary.Config;
20 | import com.google.capillary.RsaEcdsaEncrypterManager;
21 | import com.google.capillary.WebPushEncrypterManager;
22 | import io.grpc.BindableService;
23 | import io.grpc.Server;
24 | import io.grpc.ServerBuilder;
25 | import java.io.File;
26 | import java.io.FileInputStream;
27 | import java.io.IOException;
28 | import java.security.GeneralSecurityException;
29 | import java.sql.SQLException;
30 | import java.util.logging.Logger;
31 | import org.apache.commons.cli.CommandLine;
32 | import org.apache.commons.cli.CommandLineParser;
33 | import org.apache.commons.cli.DefaultParser;
34 | import org.apache.commons.cli.Option;
35 | import org.apache.commons.cli.Options;
36 | import org.apache.commons.cli.ParseException;
37 |
38 | /**
39 | * The starting point of the demo app server.
40 | */
41 | public final class DemoServer {
42 |
43 | private static final Logger logger = Logger.getLogger(DemoServer.class.getName());
44 | private static final String PORT_OPTION = "port";
45 | private static final String DATABASE_PATH_OPTION = "database_path";
46 | private static final String ECDSA_PRIVATE_KEY_PATH_OPTION = "ecdsa_private_key_path";
47 | private static final String TLS_CERT_PATH_OPTION = "tls_cert_path";
48 | private static final String TLS_PRIVATE_KEY_PATH_OPTION = "tls_private_key_path";
49 | private static final String SERVICE_ACCOUNT_CREDENTIALS_PATH_OPTION =
50 | "service_account_credentials_path";
51 | private static final String FIREBASE_PROJECT_ID_OPTION = "firebase_project_id";
52 |
53 | private Server server;
54 |
55 | /**
56 | * Launches the server.
57 | *
58 | * @param args the command line args.
59 | * @throws Exception as a catch-all exception in main method.
60 | */
61 | public static void main(String[] args) throws Exception {
62 | // Initialize the Capillary library.
63 | Config.initialize();
64 |
65 | // Obtain command line options.
66 | CommandLine cmd = generateCommandLine(args);
67 |
68 | // Initialize and start gRPC server.
69 | DemoServer server = new DemoServer();
70 | server.start(cmd);
71 | server.blockUntilShutdown();
72 | }
73 |
74 | private static CommandLine generateCommandLine(String[] commandLineArguments)
75 | throws ParseException {
76 | Option port = Option.builder()
77 | .longOpt(PORT_OPTION)
78 | .desc("The port to use.")
79 | .hasArg()
80 | .required()
81 | .type(Integer.class)
82 | .build();
83 | Option firebaseProjectId = Option.builder()
84 | .longOpt(FIREBASE_PROJECT_ID_OPTION)
85 | .desc("The ID of the Firebase project.")
86 | .hasArg()
87 | .required()
88 | .build();
89 | Option serviceAccountCredentialsPath = Option.builder()
90 | .longOpt(SERVICE_ACCOUNT_CREDENTIALS_PATH_OPTION)
91 | .desc("The path to Firebase service account credentials.")
92 | .hasArg()
93 | .required()
94 | .build();
95 | Option ecdsaPrivateKeyPath = Option.builder()
96 | .longOpt(ECDSA_PRIVATE_KEY_PATH_OPTION)
97 | .desc("The path to ecdsa private key.")
98 | .hasArg()
99 | .required()
100 | .build();
101 | Option tlsCertPath = Option.builder()
102 | .longOpt(TLS_CERT_PATH_OPTION)
103 | .desc("The path to tls cert.")
104 | .hasArg()
105 | .required()
106 | .build();
107 | Option tlsPrivateKeyPath = Option.builder()
108 | .longOpt(TLS_PRIVATE_KEY_PATH_OPTION)
109 | .desc("The path to tls private key.")
110 | .hasArg()
111 | .required()
112 | .build();
113 | Option databasePath = Option.builder()
114 | .longOpt(DATABASE_PATH_OPTION)
115 | .desc("The path to sqlite database.")
116 | .hasArg()
117 | .required()
118 | .build();
119 |
120 | Options options = new Options();
121 | options.addOption(port);
122 | options.addOption(firebaseProjectId);
123 | options.addOption(serviceAccountCredentialsPath);
124 | options.addOption(ecdsaPrivateKeyPath);
125 | options.addOption(tlsPrivateKeyPath);
126 | options.addOption(tlsCertPath);
127 | options.addOption(databasePath);
128 |
129 | CommandLineParser cmdLineParser = new DefaultParser();
130 | return cmdLineParser.parse(options, commandLineArguments);
131 | }
132 |
133 | private void start(CommandLine cmd) throws IOException, GeneralSecurityException, SQLException {
134 | // The port on which the server should run.
135 | int port = Integer.valueOf(cmd.getOptionValue(PORT_OPTION));
136 | // The FCM message sender.
137 | FcmSender fcmSender = new FcmSender(
138 | cmd.getOptionValue(FIREBASE_PROJECT_ID_OPTION),
139 | cmd.getOptionValue(SERVICE_ACCOUNT_CREDENTIALS_PATH_OPTION));
140 | // The Capillary encrypter managers.
141 | RsaEcdsaEncrypterManager rsaEcdsaEncrypterManager;
142 | try (FileInputStream senderSigningKey =
143 | new FileInputStream(cmd.getOptionValue(ECDSA_PRIVATE_KEY_PATH_OPTION))) {
144 | rsaEcdsaEncrypterManager = new RsaEcdsaEncrypterManager(senderSigningKey);
145 | }
146 | WebPushEncrypterManager webPushEncrypterManager = new WebPushEncrypterManager();
147 | // The {certificate, private key} pair to use for gRPC TLS.
148 | File tlsCertFile = new File(cmd.getOptionValue(TLS_CERT_PATH_OPTION));
149 | File tlsPrivateKeyFile = new File(cmd.getOptionValue(TLS_PRIVATE_KEY_PATH_OPTION));
150 | // The interface to demo SQLite DB.
151 | DemoDb db = new DemoDb(
152 | "jdbc:sqlite:" + cmd.getOptionValue(DATABASE_PATH_OPTION));
153 | // The demo service.
154 | BindableService demoService =
155 | new DemoServiceImpl(db, rsaEcdsaEncrypterManager, webPushEncrypterManager, fcmSender);
156 | // Create and start the gRPC server instance.
157 | server = ServerBuilder.forPort(port)
158 | .useTransportSecurity(tlsCertFile, tlsPrivateKeyFile)
159 | .addService(demoService)
160 | .build()
161 | .start();
162 | logger.info("Server started, listening on " + port);
163 |
164 | Runtime.getRuntime().addShutdownHook(new Thread(() -> {
165 | // Use stderr here since the logger may have been reset by its JVM shutdown hook.
166 | System.err.println("*** shutting down gRPC server since JVM is shutting down");
167 | shutdown();
168 | System.err.println("*** server shut down");
169 | }));
170 | }
171 |
172 | // Await termination on the main thread since the gRPC library uses daemon threads.
173 | private void blockUntilShutdown() throws InterruptedException {
174 | if (server != null) {
175 | server.awaitTermination();
176 | }
177 | }
178 |
179 | private void shutdown() {
180 | if (server != null) {
181 | server.shutdown();
182 | }
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/demo/android/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
32 |
33 |
42 |
43 |
52 |
53 |
62 |
63 |
72 |
73 |
82 |
83 |
92 |
93 |
101 |
102 |
111 |
112 |
119 |
120 |
130 |
131 |
140 |
141 |
150 |
151 |
161 |
162 |
171 |
172 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/lib-android/src/main/java/com/google/capillary/android/DecrypterManager.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.google.capillary.android;
18 |
19 | import android.content.Context;
20 | import android.os.Build.VERSION;
21 | import android.os.Build.VERSION_CODES;
22 | import android.security.keystore.KeyPermanentlyInvalidatedException;
23 | import android.security.keystore.UserNotAuthenticatedException;
24 | import com.google.capillary.AuthModeUnavailableException;
25 | import com.google.capillary.NoSuchKeyException;
26 | import com.google.capillary.internal.CapillaryCiphertext;
27 | import com.google.crypto.tink.HybridDecrypt;
28 | import com.google.protobuf.InvalidProtocolBufferException;
29 | import java.security.GeneralSecurityException;
30 | import java.security.UnrecoverableKeyException;
31 | import java.util.List;
32 |
33 | /**
34 | * Encapsulates the process of decrypting Capillary ciphertexts.
35 | */
36 | public final class DecrypterManager {
37 |
38 | private final Context context;
39 | private final CiphertextStorage ciphertextStorage;
40 | private final KeyManager keyManager;
41 | private final Utils utils;
42 |
43 | /**
44 | * Creates a new {@link DecrypterManager} instance backed by the given key manager instance.
45 | */
46 | DecrypterManager(
47 | Context context, KeyManager keyManager, CiphertextStorage ciphertextStorage, Utils utils) {
48 | this.context = context;
49 | this.ciphertextStorage = ciphertextStorage;
50 | this.keyManager = keyManager;
51 | this.utils = utils;
52 | }
53 |
54 | /**
55 | * Attempts to decrypt any Capillary ciphertexts that were saved to be decrypted later.
56 | *
57 | * @param handler the Capillary handler instance.
58 | * @param extra the extra parameters to be passed back to the provided handler.
59 | */
60 | public synchronized void decryptSaved(CapillaryHandler handler, Object extra) {
61 | List ciphertexts = ciphertextStorage.get();
62 | ciphertextStorage.clear();
63 | for (byte[] data : ciphertexts) {
64 | decrypt(data, handler, extra);
65 | }
66 | }
67 |
68 | /**
69 | * Attempts to decrypt the given Capillary ciphertext.
70 | *
71 | *
If the decryption is successful, the plaintext will be returned via the provided Capillary
72 | * handler. If the decryption key requires authentication, but the user has not yet authenticated,
73 | * the ciphertext will be saved to be decrypted later. If the decryption key is missing or
74 | * corrupted, a new Capillary key pair will be generated and the generated Capillary public key
75 | * will be passed back via handler. If any of the above steps fail, a related error code from
76 | * {@link CapillaryHandlerErrorCode} will be returned via the provided Capillary handler.
77 | *
78 | * @param ciphertext the Capillary ciphertext.
79 | * @param handler the Capillary handler instance.
80 | * @param extra the extra parameters to be passed back to the provided handler.
81 | */
82 | public synchronized void decrypt(byte[] ciphertext, CapillaryHandler handler, Object extra) {
83 | // Parse the given ciphertext bytes.
84 | CapillaryCiphertext capillaryCiphertext;
85 | try {
86 | capillaryCiphertext = CapillaryCiphertext.parseFrom(ciphertext);
87 | } catch (InvalidProtocolBufferException e) {
88 | handler.error(CapillaryHandlerErrorCode.MALFORMED_CIPHERTEXT, ciphertext, extra);
89 | return;
90 | }
91 |
92 | // Save the ciphertext for later if the decryption key is not ready.
93 | if (capillaryCiphertext.getIsAuthKey() && utils.isScreenLocked(context)) {
94 | ciphertextStorage.save(ciphertext);
95 | handler.authCiphertextSavedForLater(ciphertext, extra);
96 | return;
97 | }
98 |
99 | byte[] rawCiphertext = capillaryCiphertext.getCiphertext().toByteArray();
100 | byte[] data;
101 |
102 | // Attempt decryption.
103 | try {
104 | HybridDecrypt decrypter = keyManager.getDecrypter(
105 | capillaryCiphertext.getKeychainUniqueId(),
106 | capillaryCiphertext.getKeySerialNumber(),
107 | capillaryCiphertext.getIsAuthKey());
108 | data = decrypter.decrypt(rawCiphertext, null);
109 | } catch (AuthModeUnavailableException e) {
110 | handler.error(CapillaryHandlerErrorCode.AUTH_CIPHER_IN_NO_AUTH_DEVICE, ciphertext, extra);
111 | return;
112 | } catch (Exception e) { // Needs to catch Exception here to support multiple API levels.
113 | // In API levels 23 and above, this happens when the user has enabled authentication, but
114 | // hasn't yet authenticated in the lock screen (e.g., added a new unlock code, but hasn't
115 | // locked the device yet.).
116 | if (VERSION.SDK_INT >= VERSION_CODES.M && e instanceof UserNotAuthenticatedException) {
117 | ciphertextStorage.save(ciphertext);
118 | handler.authCiphertextSavedForLater(ciphertext, extra);
119 | return;
120 | }
121 | // All these exceptions refer to missing or corrupt decryption keys.
122 | if (e instanceof NoSuchKeyException // Thrown by Capillary library.
123 | // Thrown in API levels 23, 24, 25.
124 | || (VERSION.SDK_INT >= VERSION_CODES.M && e instanceof KeyPermanentlyInvalidatedException)
125 | // Thrown in API levels 26, 27.
126 | || (VERSION.SDK_INT >= VERSION_CODES.O && e instanceof UnrecoverableKeyException)) {
127 | regenerateKeyAndRequestMessage(capillaryCiphertext, handler, extra);
128 | return;
129 | }
130 | // Reaching here implies an unknown error has occurred.
131 | handler.error(CapillaryHandlerErrorCode.UNKNOWN_ERROR, ciphertext, extra);
132 | return;
133 | }
134 |
135 | // Return plaintext via the provided handler.
136 | handler.handleData(capillaryCiphertext.getIsAuthKey(), data, extra);
137 | }
138 |
139 | private void regenerateKeyAndRequestMessage(
140 | CapillaryCiphertext capillaryCiphertext, CapillaryHandler handler, Object extra) {
141 | boolean isAuthKey = capillaryCiphertext.getIsAuthKey();
142 | byte[] ciphertext = capillaryCiphertext.toByteArray();
143 | try {
144 | // Attempt to generate a new Capillary key pair.
145 | boolean isKeyPairGenerated =
146 | keyManager.generateKeyPair(capillaryCiphertext.getKeySerialNumber() + 1, isAuthKey);
147 | // Failure to generate the key here implies that the given Capillary ciphertext was generated
148 | // using an older Capillary public key. So, pass the appropriate error code back via the given
149 | // handler. The client should re-register the current Capillary public key with the
150 | // application server to avoid this error happening again.
151 | if (!isKeyPairGenerated) {
152 | handler.error(CapillaryHandlerErrorCode.STALE_CIPHERTEXT, ciphertext, extra);
153 | return;
154 | }
155 | // Return the newly generated Capillary public key via the provided handler.
156 | handler.handlePublicKey(isAuthKey, keyManager.getPublicKey(isAuthKey), ciphertext, extra);
157 | } catch (AuthModeUnavailableException | NoSuchKeyException | GeneralSecurityException e) {
158 | // None of these error should occur. If they do, that indicates an unknown error.
159 | handler.error(CapillaryHandlerErrorCode.UNKNOWN_ERROR, ciphertext, extra);
160 | }
161 | }
162 | }
163 |
--------------------------------------------------------------------------------