├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── Jenkinsfile ├── LICENSE.txt ├── README.md ├── app ├── .gitignore ├── build.gradle ├── lint.xml ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── realm │ │ ├── TestHelper.java │ │ ├── android │ │ └── SecureUserStoreTests.java │ │ └── rule │ │ └── TestRealmConfigurationFactory.java │ └── main │ ├── AndroidManifest.xml │ └── java │ └── io │ └── realm │ └── android │ ├── CipherClient.java │ ├── SecureUserStore.java │ └── internal │ └── android │ └── crypto │ ├── CipherFactory.java │ ├── SyncCrypto.java │ ├── SyncCryptoFactory.java │ ├── api_18 │ └── SyncCryptoApi18Impl.java │ ├── api_23 │ └── SyncCryptoApi23Impl.java │ ├── api_legacy │ └── SyncCryptoLegacy.java │ ├── ciper │ ├── CipherJB.java │ ├── CipherLegacy.java │ └── CipherMM.java │ └── misc │ ├── Base64.java │ └── PRNGFixes.java ├── build.gradle ├── config ├── checkstyle │ ├── checkstyle-suppressions.xml │ └── checkstyle.xml ├── findbugs │ └── findbugs-filter.xml └── pmd │ └── ruleset.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── logo.png ├── settings.gradle └── version.txt /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > We LOVE to help with any issues or bug you have! 2 | 3 | > **Questions**: If you have questions about how to use Realm, please ask on [SO](http://stackoverflow.com/questions/ask?tags=realm) - we monitor the Realm tag. 4 | 5 | > **Feature Request**: Just fill in the first two sections below. 6 | 7 | > **Bugs**: To help you as fast as possible with an issue or bug please describe your issue and the steps you have taken to reproduce it in as many details as possible. 8 | > 9 | > Thanks for helping us help you :-) 10 | > 11 | > Remove this and above before submitting. 12 | 13 | #### Goal 14 | 15 | > What do you want to achieve? 16 | 17 | #### Expected Results 18 | 19 | > ? 20 | 21 | #### Actual Results 22 | 23 | > E.g. full stack trace with exception 24 | 25 | #### Steps & Code to Reproduce 26 | 27 | > Describe your current debugging efforts. 28 | 29 | #### Code Sample 30 | 31 | ```java 32 | 33 | > Your code here. Bigger samples should ideally be as separate Android Studio project, 34 | > in gists/repositories or privately at help@realm.io) 35 | 36 | ``` 37 | 38 | #### Version of Realm and tooling 39 | Library version(s): ? 40 | 41 | Realm version(s): ? 42 | 43 | Android Studio version: ? 44 | 45 | Which Android version and device: ? 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle build artifacts 2 | build 3 | realm/build 4 | 5 | # Gradle cache 6 | .gradle 7 | 8 | # Gradle local properties 9 | local.properties 10 | 11 | # Core 12 | core 13 | core-* 14 | realm-sync-android-* 15 | 16 | # Android Studio 17 | .idea 18 | *.iml 19 | 20 | # DS_Store 21 | .DS_Store 22 | */.DS_Store 23 | 24 | # JNI libs 25 | realm_version_check.timestamp 26 | 27 | # Build artifacts 28 | *.so 29 | *.d 30 | *.o 31 | 32 | # Backup files 33 | *.bak 34 | *~ 35 | 36 | # Navigation Editor 37 | .navigation 38 | 39 | # Distribution files 40 | realm*.jar 41 | realm*.aar 42 | distribution/*/realm 43 | distribution/javadoc 44 | distribution/version.txt 45 | distribution/RealmGridViewExample/app/src 46 | distribution/RealmIntroExample/app/src 47 | distribution/RealmMigrationExample/app/src 48 | 49 | # Generated JNI headers 50 | realm/realm-library/src/main/cpp/jni_include 51 | # Downloaded core 52 | realm/realm-library/distribution 53 | # Cmake output 54 | realm/realm-library/.externalNativeBuild 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.1 (12/04/2017) 2 | 3 | ### Bug Fixes 4 | 5 | * Fixing duplicate Key creation 6 | * Using unique key alias (per package name) 7 | * Expose KeyStore helper method (Unlock) 8 | 9 | ### Enhancements 10 | 11 | * Add more tests coverage 12 | 13 | ## 1.0.0 14 | 15 | * Initial release. 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Filing Issues 4 | 5 | Whether you find a bug, typo or an API call that could be clarified, please [file an issue](https://github.com/realm/sync-user-encryption/issues) on our GitHub repository. 6 | 7 | When filing an issue, please provide as much of the following information as possible in order to help us fix it: 8 | 9 | 1. **Goals** 10 | 2. **Expected results** 11 | 3. **Actual results** 12 | 4. **Steps to reproduce** 13 | 5. **Code sample that highlights the issue** (link to full Android Studio projects that we can compile ourselves are ideal) 14 | 6. **Version of Realm/Android Studio/OS** 15 | 16 | If you'd like to send us sensitive sample code to help troubleshoot your issue, you can email directly. 17 | 18 | ## Contributing Enhancements 19 | 20 | We love contributions to Realm! If you'd like to contribute code, documentation, or any other improvements, please [file a Pull Request](https://github.com/realm/sync-user-encryption/pulls) on our GitHub repository. Make sure to accept our [CLA](#CLA)! 21 | 22 | ### CLA 23 | 24 | Realm welcomes all contributions! The only requirement we have is that, like many other projects, we need to have a [Contributor License Agreement](https://en.wikipedia.org/wiki/Contributor_License_Agreement) (CLA) in place before we can accept any external code. Our own CLA is a modified version of the Apache Software Foundation’s CLA. 25 | 26 | [Please submit your CLA electronically using our Google form](https://docs.google.com/forms/d/1bVp-Wp5nmNFz9Nx-ngTmYBVWVdwTyKj4T0WtfVm0Ozs/viewform?fbzx=4154977190905366979) so we can accept your submissions. The GitHub username you file there will need to match that of your Pull Requests. If you have any questions or cannot file the CLA electronically, you can email . 27 | 28 | ## Repository Guidelines 29 | 30 | ### Code Style 31 | 32 | While we haven't described our code style yet, please just follow the existing style you see in the files you change. 33 | 34 | ### Unit Tests 35 | 36 | All PR's must be accompanied by related unit tests. All bug fixes must have a unit test proving that the bug is fixed. 37 | 38 | When writing unit tests, use the following guide lines: 39 | 40 | 1) Unit tests must be written using JUnit4. 41 | 42 | 2) All tests for a class should be grouped in a class called `Tests`, unless the functionality is cross- 43 | cutting like [`RxJavaTests`](https://github.com/realm/realm-java/blob/master/realm/realm-library/src/androidTest/java/io/realm/RxJavaTests.java) 44 | or [`RealmAsyncQueryTests`](https://github.com/realm/realm-java/blob/master/realm/realm-library/src/androidTest/java/io/realm/RealmAsyncQueryTests.java). 45 | 46 | 3) Test methods should use camelCase and underscore `_` between logical sections to increase method name readability. 47 | Methods should ideally start with the name of the method being tested. Patterns like: `_`, 48 | `__` or `` are encouraged. 49 | 50 | 4) All unit tests creating Realms must do so using the [`TestRealmConfigurationFactory`](https://github.com/realm/realm-java/blob/master/realm/realm-library/src/androidTest/java/io/realm/rule/TestRealmConfigurationFactory.java) 51 | or [`RunInLooperThread`](https://github.com/realm/realm-java/blob/master/realm/realm-library/src/androidTest/java/io/realm/rule/RunInLooperThread.java) 52 | test rules. This ensures that all Realms are properly closed and deleted between each test. 53 | 54 | 5) Use the `@RunInLooperThread` rule for any test that depends on Realms notification system. 55 | 56 | 6) Input-parameters should be boundary tested. Especially `Null/NotNull`, but also the state of Realm objects like 57 | unmanaged objects, deleted objects, objects from other threads. 58 | 59 | 7) Unit tests are not required to only have 1 test. It is acceptable to combine multiple tests into one unit test, but 60 | if it fails, it should be clear why it failed. E.g. you can group related tests with the same setup like negative 61 | tests. If you do so, make sure to separate each "subtest" with a comment stating what you test. 62 | 63 | 8) Use only `@Test(expected = xxx.class)` if the test case contains one line. If the test contains multiple 64 | lines and it is the last line that is tested, use the `ExceptedException` rule instead. In all other cases, use 65 | the following pattern: 66 | 67 | try { 68 | somethingThatThrowsIllegalArgument(); 69 | } catch (IllegalArgumentException ignored) { 70 | } 71 | 72 | 9) Use comments to make the intent of the unit test easily understandable at a glance. A simple one line comment is 73 | often easier to read `thanALongCamelCasedSentenceThatAttemptsToDescribeWhatHappens`. Describe the test steps inside 74 | the method, if it's not glaringly obvious. 75 | 76 | This is an example of how a unit test class could look like: 77 | 78 | @RunWith(AndroidJUnit4.class) 79 | public class RealmTests { 80 | 81 | @Rule 82 | public final TestRealmConfigurationFactory configFactory = new TestRealmConfigurationFactory(); 83 | 84 | @Rule 85 | public final RunInLooperThread looperThread = new RunInLooperThread(); 86 | 87 | private Realm realm; 88 | 89 | @Before 90 | public void setUp() { 91 | RealmConfiguration config = configFactory.createConfiguration(); 92 | realm = Realm.getInstance(config); 93 | } 94 | 95 | @After 96 | public void tearDown() { 97 | if (realm != null) { 98 | realm.close(); 99 | } 100 | } 101 | 102 | @Test(expected = IllegalStateException.class) 103 | public void createObject_outsideTransaction() { 104 | realm.createObject(Foo.class); 105 | } 106 | 107 | @Test 108 | public void createObject_illegalInput { 109 | // Class not part of the schema 110 | try { 111 | realm.createObject(Foo.class); 112 | } catch (IllegalArgumentException ignored) { 113 | } 114 | 115 | // Null class 116 | try { 117 | realm.createObject(null); 118 | } catch (IllegalArgumentException ignored) { 119 | } 120 | } 121 | 122 | @Test 123 | @RunTestInLooperThread 124 | public void addChangeListener_notifiedOnLocalCommit() { 125 | realm.addChangeListener(new RealmChangeListener() { 126 | @Override 127 | public void onChange() { 128 | assert(1, realm.allObjects(Foo.class).size()); 129 | looperThread.testComplete(); 130 | } 131 | }); 132 | 133 | realm.beginTransaction(); 134 | realm.createObject(Foo.class); 135 | realm.commitTransaction(); 136 | } 137 | } 138 | 139 | ### Javadoc 140 | 141 | All public classes and methods must have Javadoc describing their purpose. 142 | 143 | ```java 144 | /** 145 | * Checks if given field is equal to the provided value. 146 | * 147 | *
148 |  * {@code
149 |  *   // A multi-line code sample should be formatted like this.
150 |  *   // Please wrap the code element in a 
 tag.
151 |  * }
152 |  * 
153 | * 154 | * @param fieldName the field to compare. 155 | * @param fieldValue the value to compare with. 156 | * @param caseSensitive if {@code true}, substring matching is case sensitive. Setting this to {@code false} works for English locale characters only. 157 | * @param caseSensitive if true, substring matching is case sensitive. Setting this to false only works for English 158 | * locale characters. 159 | * @return the query object. 160 | * @throws java.lang.IllegalArgumentException if one or more arguments do not match class or field type. 161 | * @throws IllegalArgumentException if field name doesn't exists, it doesn't contain a list of links or the type 162 | * of the object represented by the DynamicRealmObject doesn't match. 163 | * @deprecated Please use {@link #average(String)} instead. 164 | * @see #endGroup() 165 | */ 166 | public RealmQuery equalTo(String fieldName, String fieldValue, boolean caseSensitive) { 167 | // ... 168 | } 169 | ``` 170 | 171 | * Method descriptions begin with a verb phrase, e.g. "Checks" instead of "Check". 172 | * Capitalize the first letter of the method and @deprecated descriptions. Everything else starts with lower case. 173 | * Empty line between method description and the rest. 174 | * End all descriptions with a period `.` (except @see). 175 | * Reference other Realm classes using `{@link ...}`. 176 | * Wrap Java values in `{@code ...}`. 177 | * @throws description must start with "if". 178 | * Never list generic exceptions like `RuntimeException`, `Exception` or `Error`. Always reference the specific error. 179 | * Line-length maximum is 120 chars. Parameter descriptions that go above this, should be split into multiple lines and indented. Otherwise do not use indentation (contrary to Oracle guidelines). 180 | 181 | Above is based on the official guidelines from Oracle regarding Javadoc: http://www.oracle.com/technetwork/articles/java/index-137868.html 182 | 183 | ### Branch Strategy 184 | 185 | We have two branches for shared development: `master` and `releases`. We make releases from each. 186 | 187 | `master`: 188 | 189 | * The `master` branch is where major/minor versions are released from. 190 | * It is for new features and/or breaking changes. 191 | 192 | `releases`: 193 | 194 | * The releases branch is where patch versions are released from. 195 | * It is mainly for bug fixes. 196 | * Every commit is automatically merged to `master`. 197 | * Minor changes (e.g. to documentation, tests, and the build system) may not affect end users but should still be merged to `releases` to avoid diverging too far from `master` and to reduce the likelihood of merge conflicts. 198 | 199 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | # Locales 4 | RUN locale-gen en_US.UTF-8 5 | ENV LANG "en_US.UTF-8" 6 | ENV LANGUAGE "en_US.UTF-8" 7 | ENV LC_ALL "en_US.UTF-8" 8 | 9 | # Set the environment variables 10 | ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64 11 | ENV ANDROID_HOME /opt/android-sdk-linux 12 | # Need by cmake 13 | ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools 14 | ENV PATH ${PATH}:${NDK_HOME} 15 | ENV NDK_CCACHE /usr/bin/ccache 16 | 17 | # The 32 bit binaries because aapt requires it 18 | # `file` is need by the script that creates NDK toolchains 19 | # Keep the packages in alphabetical order to make it easy to avoid duplication 20 | RUN DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386 \ 21 | && apt-get update -qq \ 22 | && apt-get install -y bsdmainutils \ 23 | build-essential \ 24 | ccache \ 25 | curl \ 26 | file \ 27 | git \ 28 | libc6:i386 \ 29 | libgcc1:i386 \ 30 | libncurses5:i386 \ 31 | libstdc++6:i386 \ 32 | libz1:i386 \ 33 | openjdk-8-jdk-headless \ 34 | s3cmd \ 35 | unzip \ 36 | wget \ 37 | zip \ 38 | && apt-get clean 39 | 40 | # Install the Android SDK 41 | RUN cd /opt && \ 42 | wget -q https://dl.google.com/android/repository/tools_r25.1.7-linux.zip -O android-tools-linux.zip && \ 43 | unzip android-tools-linux.zip -d ${ANDROID_HOME} && \ 44 | rm -f android-tools-linux.zip 45 | 46 | # Grab what's needed in the SDK 47 | # ↓ updates tools to at least 25.1.7, but that prints 'Nothing was installed' (so I don't check the outputs). 48 | RUN echo y | android update sdk --no-ui --all --filter tools > /dev/null 49 | RUN echo y | android update sdk --no-ui --all --filter platform-tools | grep 'package installed' 50 | RUN echo y | android update sdk --no-ui --all --filter build-tools-24.0.3 | grep 'package installed' 51 | RUN echo y | android update sdk --no-ui --all --filter extra-android-m2repository | grep 'package installed' 52 | RUN echo y | android update sdk --no-ui --all --filter android-24 | grep 'package installed' 53 | 54 | # Make the SDK universally readable 55 | RUN chmod -R a+rX ${ANDROID_HOME} -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | import groovy.json.JsonOutput 4 | 5 | def buildSuccess = false 6 | try { 7 | node('android') { 8 | // Allocate a custom workspace to avoid having % in the path (it breaks ld) 9 | ws('/tmp/realm-android-user-store') { 10 | stage('SCM') { 11 | checkout([ 12 | $class: 'GitSCM', 13 | branches: scm.branches, 14 | gitTool: 'native git', 15 | extensions: scm.extensions + [[$class: 'CleanCheckout']], 16 | userRemoteConfigs: scm.userRemoteConfigs 17 | ]) 18 | } 19 | 20 | def buildEnv 21 | stage('Docker build') { 22 | buildEnv = docker.build 'realm-android-user-store:snapshot' 23 | } 24 | 25 | buildEnv.inside("-e HOME=/tmp -e _JAVA_OPTIONS=-Duser.home=/tmp --privileged -v /dev/bus/usb:/dev/bus/usb -v ${env.HOME}/gradle-cache:/tmp/.gradle -v ${env.HOME}/.android:/tmp/.android -v ${env.HOME}/ccache:/tmp/.ccache") { 26 | stage('Build & Test') { 27 | try { 28 | gradle 'assemble javadoc check connectedCheck' 29 | if (env.BRANCH_NAME == 'master') { 30 | stage('Collect metrics') { 31 | collectAarMetrics() 32 | } 33 | } 34 | } finally { 35 | storeJunitResults '**/TEST-*.xml' 36 | step([$class: 'LintPublisher']) 37 | } 38 | } 39 | } 40 | } 41 | } 42 | currentBuild.rawBuild.setResult(Result.SUCCESS) 43 | buildSuccess = true 44 | } catch(Exception e) { 45 | currentBuild.rawBuild.setResult(Result.FAILURE) 46 | buildSuccess = false 47 | throw e 48 | } 49 | 50 | 51 | def String startLogCatCollector() { 52 | sh '''adb logcat -c 53 | adb logcat -v time > "logcat.txt" & 54 | echo $! > pid 55 | ''' 56 | return readFile("pid").trim() 57 | } 58 | 59 | def stopLogCatCollector(String backgroundPid, boolean archiveLog) { 60 | sh "kill ${backgroundPid}" 61 | if (archiveLog) { 62 | zip([ 63 | 'zipFile': 'logcat.zip', 64 | 'archive': true, 65 | 'glob' : 'logcat.txt' 66 | ]) 67 | } 68 | sh 'rm logcat.txt ' 69 | } 70 | 71 | def sendMetrics(String metricName, String metricValue, Map tags) { 72 | def tagsString = getTagsString(tags) 73 | withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: '5b8ad2d9-61a4-43b5-b4df-b8ff6b1f16fa', passwordVariable: 'influx_pass', usernameVariable: 'influx_user']]) { 74 | sh "curl -i -XPOST 'https://greatscott-pinheads-70.c.influxdb.com:8086/write?db=realm' --data-binary '${metricName},${tagsString} value=${metricValue}i' --user '${env.influx_user}:${env.influx_pass}'" 75 | } 76 | } 77 | 78 | @NonCPS 79 | def getTagsString(Map tags) { 80 | return tags.collect { k,v -> "$k=$v" }.join(',') 81 | } 82 | 83 | def storeJunitResults(String path) { 84 | step([ 85 | $class: 'JUnitResultArchiver', 86 | testResults: path 87 | ]) 88 | } 89 | 90 | def collectAarMetrics() { 91 | sh """set -xe 92 | cd app/build/outputs/aar 93 | unzip secure-userstore-release.aar -d unzipped 94 | find \$ANDROID_HOME -name dx | sort -r | head -n 1 > dx 95 | \$(cat dx) --dex --output=temp.dex unzipped/classes.jar 96 | cat temp.dex | head -c 92 | tail -c 4 | hexdump -e '1/4 \"%d\"' > methods 97 | """ 98 | 99 | def methods = readFile('app/build/outputs/aar/methods') 100 | sendMetrics('secure_userstore_methods', methods, [:]) 101 | 102 | def aarFile = findFiles(glob: 'app/build/outputs/aar/secure-userstore-release.aar')[0] 103 | sendMetrics('secure_userstore_aar_size', aarFile.length as String, [:]) 104 | } 105 | 106 | def gradle(String commands) { 107 | sh "chmod +x gradlew && ./gradlew ${commands} --stacktrace" 108 | } 109 | 110 | def gradle(String relativePath, String commands) { 111 | sh "cd ${relativePath} && chmod +x gradlew && ./gradlew ${commands} --stacktrace" 112 | } 113 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | TABLE OF CONTENTS 2 | 3 | 1. Apache License version 2.0 4 | 2. Realm Components 5 | 3. Export Compliance 6 | 7 | 1. ------------------------------------------------------------------------------- 8 | 9 | Apache License 10 | Version 2.0, January 2004 11 | http://www.apache.org/licenses/ 12 | 13 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 14 | 15 | 1. Definitions. 16 | 17 | "License" shall mean the terms and conditions for use, reproduction, 18 | and distribution as defined by Sections 1 through 9 of this document. 19 | 20 | "Licensor" shall mean the copyright owner or entity authorized by 21 | the copyright owner that is granting the License. 22 | 23 | "Legal Entity" shall mean the union of the acting entity and all 24 | other entities that control, are controlled by, or are under common 25 | control with that entity. For the purposes of this definition, 26 | "control" means (i) the power, direct or indirect, to cause the 27 | direction or management of such entity, whether by contract or 28 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 29 | outstanding shares, or (iii) beneficial ownership of such entity. 30 | 31 | "You" (or "Your") shall mean an individual or Legal Entity 32 | exercising permissions granted by this License. 33 | 34 | "Source" form shall mean the preferred form for making modifications, 35 | including but not limited to software source code, documentation 36 | source, and configuration files. 37 | 38 | "Object" form shall mean any form resulting from mechanical 39 | transformation or translation of a Source form, including but 40 | not limited to compiled object code, generated documentation, 41 | and conversions to other media types. 42 | 43 | "Work" shall mean the work of authorship, whether in Source or 44 | Object form, made available under the License, as indicated by a 45 | copyright notice that is included in or attached to the work 46 | (an example is provided in the Appendix below). 47 | 48 | "Derivative Works" shall mean any work, whether in Source or Object 49 | form, that is based on (or derived from) the Work and for which the 50 | editorial revisions, annotations, elaborations, or other modifications 51 | represent, as a whole, an original work of authorship. For the purposes 52 | of this License, Derivative Works shall not include works that remain 53 | separable from, or merely link (or bind by name) to the interfaces of, 54 | the Work and Derivative Works thereof. 55 | 56 | "Contribution" shall mean any work of authorship, including 57 | the original version of the Work and any modifications or additions 58 | to that Work or Derivative Works thereof, that is intentionally 59 | submitted to Licensor for inclusion in the Work by the copyright owner 60 | or by an individual or Legal Entity authorized to submit on behalf of 61 | the copyright owner. For the purposes of this definition, "submitted" 62 | means any form of electronic, verbal, or written communication sent 63 | to the Licensor or its representatives, including but not limited to 64 | communication on electronic mailing lists, source code control systems, 65 | and issue tracking systems that are managed by, or on behalf of, the 66 | Licensor for the purpose of discussing and improving the Work, but 67 | excluding communication that is conspicuously marked or otherwise 68 | designated in writing by the copyright owner as "Not a Contribution." 69 | 70 | "Contributor" shall mean Licensor and any individual or Legal Entity 71 | on behalf of whom a Contribution has been received by Licensor and 72 | subsequently incorporated within the Work. 73 | 74 | 2. Grant of Copyright License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | copyright license to reproduce, prepare Derivative Works of, 78 | publicly display, publicly perform, sublicense, and distribute the 79 | Work and such Derivative Works in Source or Object form. 80 | 81 | 3. Grant of Patent License. Subject to the terms and conditions of 82 | this License, each Contributor hereby grants to You a perpetual, 83 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 84 | (except as stated in this section) patent license to make, have made, 85 | use, offer to sell, sell, import, and otherwise transfer the Work, 86 | where such license applies only to those patent claims licensable 87 | by such Contributor that are necessarily infringed by their 88 | Contribution(s) alone or by combination of their Contribution(s) 89 | with the Work to which such Contribution(s) was submitted. If You 90 | institute patent litigation against any entity (including a 91 | cross-claim or counterclaim in a lawsuit) alleging that the Work 92 | or a Contribution incorporated within the Work constitutes direct 93 | or contributory patent infringement, then any patent licenses 94 | granted to You under this License for that Work shall terminate 95 | as of the date such litigation is filed. 96 | 97 | 4. Redistribution. You may reproduce and distribute copies of the 98 | Work or Derivative Works thereof in any medium, with or without 99 | modifications, and in Source or Object form, provided that You 100 | meet the following conditions: 101 | 102 | (a) You must give any other recipients of the Work or 103 | Derivative Works a copy of this License; and 104 | 105 | (b) You must cause any modified files to carry prominent notices 106 | stating that You changed the files; and 107 | 108 | (c) You must retain, in the Source form of any Derivative Works 109 | that You distribute, all copyright, patent, trademark, and 110 | attribution notices from the Source form of the Work, 111 | excluding those notices that do not pertain to any part of 112 | the Derivative Works; and 113 | 114 | (d) If the Work includes a "NOTICE" text file as part of its 115 | distribution, then any Derivative Works that You distribute must 116 | include a readable copy of the attribution notices contained 117 | within such NOTICE file, excluding those notices that do not 118 | pertain to any part of the Derivative Works, in at least one 119 | of the following places: within a NOTICE text file distributed 120 | as part of the Derivative Works; within the Source form or 121 | documentation, if provided along with the Derivative Works; or, 122 | within a display generated by the Derivative Works, if and 123 | wherever such third-party notices normally appear. The contents 124 | of the NOTICE file are for informational purposes only and 125 | do not modify the License. You may add Your own attribution 126 | notices within Derivative Works that You distribute, alongside 127 | or as an addendum to the NOTICE text from the Work, provided 128 | that such additional attribution notices cannot be construed 129 | as modifying the License. 130 | 131 | You may add Your own copyright statement to Your modifications and 132 | may provide additional or different license terms and conditions 133 | for use, reproduction, or distribution of Your modifications, or 134 | for any such Derivative Works as a whole, provided Your use, 135 | reproduction, and distribution of the Work otherwise complies with 136 | the conditions stated in this License. 137 | 138 | 5. Submission of Contributions. Unless You explicitly state otherwise, 139 | any Contribution intentionally submitted for inclusion in the Work 140 | by You to the Licensor shall be under the terms and conditions of 141 | this License, without any additional terms or conditions. 142 | Notwithstanding the above, nothing herein shall supersede or modify 143 | the terms of any separate license agreement you may have executed 144 | with Licensor regarding such Contributions. 145 | 146 | 6. Trademarks. This License does not grant permission to use the trade 147 | names, trademarks, service marks, or product names of the Licensor, 148 | except as required for reasonable and customary use in describing the 149 | origin of the Work and reproducing the content of the NOTICE file. 150 | 151 | 7. Disclaimer of Warranty. Unless required by applicable law or 152 | agreed to in writing, Licensor provides the Work (and each 153 | Contributor provides its Contributions) on an "AS IS" BASIS, 154 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 155 | implied, including, without limitation, any warranties or conditions 156 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 157 | PARTICULAR PURPOSE. You are solely responsible for determining the 158 | appropriateness of using or redistributing the Work and assume any 159 | risks associated with Your exercise of permissions under this License. 160 | 161 | 8. Limitation of Liability. In no event and under no legal theory, 162 | whether in tort (including negligence), contract, or otherwise, 163 | unless required by applicable law (such as deliberate and grossly 164 | negligent acts) or agreed to in writing, shall any Contributor be 165 | liable to You for damages, including any direct, indirect, special, 166 | incidental, or consequential damages of any character arising as a 167 | result of this License or out of the use or inability to use the 168 | Work (including but not limited to damages for loss of goodwill, 169 | work stoppage, computer failure or malfunction, or any and all 170 | other commercial damages or losses), even if such Contributor 171 | has been advised of the possibility of such damages. 172 | 173 | 9. Accepting Warranty or Additional Liability. While redistributing 174 | the Work or Derivative Works thereof, You may choose to offer, 175 | and charge a fee for, acceptance of support, warranty, indemnity, 176 | or other liability obligations and/or rights consistent with this 177 | License. However, in accepting such obligations, You may act only 178 | on Your own behalf and on Your sole responsibility, not on behalf 179 | of any other Contributor, and only if You agree to indemnify, 180 | defend, and hold each Contributor harmless for any liability 181 | incurred by, or claims asserted against, such Contributor by reason 182 | of your accepting any such warranty or additional liability. 183 | 184 | END OF TERMS AND CONDITIONS 185 | 186 | 2. ------------------------------------------------------------------------------- 187 | 188 | REALM COMPONENTS 189 | 190 | This software contains components with separate copyright and license terms. 191 | Your use of these components is subject to the terms and conditions of the 192 | following licenses. 193 | 194 | For the Realm Platform Extensions component 195 | 196 | Realm Platform Extensions License 197 | 198 | Copyright (c) 2011-2016 Realm Inc All rights reserved 199 | 200 | Redistribution and use in binary form, with or without modification, is 201 | permitted provided that the following conditions are met: 202 | 203 | 1. You agree not to attempt to decompile, disassemble, reverse engineer or 204 | otherwise discover the source code from which the binary code was derived. 205 | You may, however, access and obtain a separate license for most of the 206 | source code from which this Software was created, at 207 | http://realm.io/pricing/. 208 | 209 | 2. Redistributions in binary form must reproduce the above copyright notice, 210 | this list of conditions and the following disclaimer in the documentation 211 | and/or other materials provided with the distribution. 212 | 213 | 3. Neither the name of the copyright holder nor the names of its 214 | contributors may be used to endorse or promote products derived from this 215 | software without specific prior written permission. 216 | 217 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 218 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 219 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 220 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 221 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 222 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 223 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 224 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 225 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 226 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 227 | POSSIBILITY OF SUCH DAMAGE. 228 | 229 | 3. ------------------------------------------------------------------------------- 230 | 231 | EXPORT COMPLIANCE 232 | 233 | You understand that the Software may contain cryptographic functions that may be 234 | subject to export restrictions, and you represent and warrant that you are not 235 | located in a country that is subject to United States export restriction or embargo, 236 | including Cuba, Iran, North Korea, Sudan, Syria or the Crimea region, and that you 237 | are not on the Department of Commerce list of Denied Persons, Unverified Parties, 238 | or affiliated with a Restricted Entity. 239 | 240 | You agree to comply with all export, re-export and import restrictions and 241 | regulations of the Department of Commerce or other agency or authority of the 242 | United States or other applicable countries. You also agree not to transfer, or 243 | authorize the transfer of, directly or indirectly, the Software to any prohibited 244 | country, including Cuba, Iran, North Korea, Sudan, Syria or the Crimea region, 245 | or to any person or organization on or affiliated with the Department of 246 | Commerce lists of Denied Persons, Unverified Parties or Restricted Entities, or 247 | otherwise in violation of any such restrictions or regulations. 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Realm](logo.png) 2 | 3 | Realm is a mobile database that runs directly inside phones, tablets or wearables. 4 | 5 | This repository holds the library to allow _Sync_ client under Android [Realm Object Server](https://realm.io/docs/realm-object-server/) to encrypt the 6 | token saved in `SharedPreferences`, once the user is authenticated. The encryption uses the Android KeyStore available 7 | to generate and uses `RSA` and `AES` keys for encryption operations. 8 | 9 | ## Getting Started 10 | 11 | This library only works together with Realm Java. Please see the [detailed instructions in our docs](https://realm.io/docs/java/#installation) 12 | to add Realm to your project. 13 | 14 | To add the this library to your project, add the following to you app's dependencies: 15 | 16 | ``` 17 | repositories { 18 | jcenter() 19 | } 20 | 21 | dependencies { 22 | compile 'io.realm:secure-userstore:1.0.1' 23 | } 24 | ``` 25 | 26 | This library is only compatible with Realm Java 0.90 and above. 27 | 28 | ## Documentation 29 | 30 | Documentation for Realm can be found at [realm.io/docs/java](https://realm.io/docs/java). 31 | The API reference is located at [realm.io/docs/java/api](https://realm.io/docs/java/api). 32 | 33 | ## Getting Help 34 | 35 | - **Need help with your code?**: Look for previous questions on the [#realm tag](https://stackoverflow.com/questions/tagged/realm?sort=newest) — or [ask a new question](http://stackoverflow.com/questions/ask?tags=realm). We activtely monitor & answer questions on SO! 36 | - **Have a bug to report?** [Open an issue](https://github.com/realm/realm-java/issues/new). If possible, include the version of Realm, a full log, the Realm file, and a project that shows the issue. 37 | - **Have a feature request?** [Open an issue](https://github.com/realm/realm-java/issues/new). Tell us what the feature should do, and why you want the feature. 38 | - Sign up for our [**Community Newsletter**](http://eepurl.com/VEKCn) to get regular tips, learn about other use-cases and get alerted of blogposts and tutorials about Realm. 39 | 40 | ## Using Snapshots 41 | 42 | If you want to test recent bugfixes or features that have not been packaged in an official release yet, you can use a **-SNAPSHOT** release of the current development version of Realm via Gradle, available on [OJO](http://oss.jfrog.org/oss-snapshot-local/io/realm/realm-android/) 43 | 44 | ```gradle 45 | repositories { 46 | maven { 47 | url 'http://oss.jfrog.org/artifactory/oss-snapshot-local' 48 | } 49 | } 50 | 51 | dependencies { 52 | compile 'io.realm:secure-userstore:' 53 | } 54 | ``` 55 | 56 | See [version.txt](version.txt) for the latest version number. 57 | 58 | ## Building Realm Sync User Encryption 59 | 60 | In case you don't want to use the precompiled version, you can build the library yourself from source. 61 | 62 | Prerequisites: 63 | 64 | * Download/the [**JDK 7**](http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html) or [**JDK 8**](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) from Oracle and install it. 65 | * Download & install the Android SDK, **Android 6.0 (API 23)** (for example through Android Studio’s **Android SDK Manager**) 66 | 67 | Once you have completed all the pre-requisites building the library is done with a simple command 68 | 69 | ``` 70 | ./gradlew assemble 71 | ``` 72 | 73 | That command will generate: 74 | 75 | * an `aar` file for the library in `app/build/outputs/aar/android-sync-user-encryption-release.aar` 76 | 77 | 78 | you can also install the `aar` into your local maven repository. 79 | 80 | ``` 81 | ./gradlew publishAARPublicationToMavenLocal 82 | ``` 83 | 84 | ## Contributing 85 | 86 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more details! 87 | 88 | This project adheres to the [Contributor Covenant Code of Conduct](https://realm.io/conduct). 89 | By participating, you are expected to uphold this code. Please report 90 | unacceptable behavior to [info@realm.io](mailto:info@realm.io). 91 | 92 | ## License 93 | 94 | Realm Sync User Encryption is published under the Apache 2.0 license. 95 | 96 | ## Feedback 97 | 98 | **_If you use Realm or the library and are happy with it, all we ask is that you please consider sending out a tweet mentioning [@realm](http://twitter.com/realm), or email [help@realm.io](mailto:help@realm.io) to let us know about it!_** 99 | 100 | **_And if you don't like it, please let us know what you would like improved, so we can fix it!_** 101 | 102 | ![analytics](https://ga-beacon.appspot.com/UA-50247013-2/android-sync-user-encryption/README?pixel) 103 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven-publish' 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | apply plugin: 'com.jfrog.artifactory' 5 | apply plugin: 'com.jfrog.bintray' 6 | apply plugin: 'findbugs' 7 | apply plugin: 'pmd' 8 | apply plugin: 'checkstyle' 9 | 10 | android { 11 | compileSdkVersion 24 12 | buildToolsVersion "24.0.3" 13 | defaultConfig { 14 | minSdkVersion 9 15 | targetSdkVersion 24 16 | versionCode 1 17 | versionName "1.0" 18 | project.archivesBaseName = "secure-userstore" 19 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 20 | } 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | } 28 | 29 | repositories { 30 | maven { url "${System.env.HOME}/.m2/repository" } 31 | } 32 | dependencies { 33 | provided "io.realm:realm-android-library-object-server:${realmVersion}" 34 | androidTestCompile ('com.android.support.test.espresso:espresso-core:2.2.2', { 35 | exclude group: 'com.android.support', module: 'support-annotations' 36 | }) 37 | androidTestCompile 'com.android.support:support-annotations:24.2.1' 38 | androidTestCompile "io.realm:realm-android-library-object-server:${realmVersion}" 39 | } 40 | 41 | task clean(type: Delete) { 42 | delete rootProject.buildDir 43 | } 44 | 45 | task findbugs(type: FindBugs) { 46 | dependsOn assemble 47 | group = 'Verification' 48 | 49 | ignoreFailures = false 50 | effort = "default" 51 | reportLevel = "medium" 52 | excludeFilter = file("${projectDir}/../config/findbugs/findbugs-filter.xml") 53 | classes = files("${projectDir}/build/intermediates/classes") 54 | source = fileTree('src/main/java/') 55 | classpath = files() 56 | reports { 57 | xml.enabled = false 58 | html.enabled = true 59 | xml { 60 | destination "$project.buildDir/findbugs/findbugs-output.xml" 61 | } 62 | html { 63 | destination "$project.buildDir/findbugs/findbugs-output.html" 64 | } 65 | } 66 | } 67 | 68 | task pmd(type: Pmd) { 69 | group = 'Verification' 70 | print "${projectDir}" 71 | source = fileTree('src/main/java') 72 | ruleSetFiles = files("${projectDir}/../config/pmd/ruleset.xml") 73 | ruleSets = [] // This needs to be here to remove the default checks 74 | 75 | reports { 76 | xml.enabled = false 77 | html.enabled = true 78 | } 79 | } 80 | 81 | task checkstyle(type: Checkstyle) { 82 | group = 'Test' 83 | 84 | source 'src' 85 | include '**/*.java' 86 | exclude '**/gen/**' 87 | exclude '**/R.java' 88 | exclude '**/BuildConfig.java' 89 | 90 | def configProps = ['proj.module.dir': projectDir.absolutePath] 91 | configProperties configProps 92 | 93 | configFile = file("${projectDir}/../config/checkstyle/checkstyle.xml") 94 | 95 | // empty classpath 96 | classpath = files() 97 | } 98 | 99 | task javadoc(type: Javadoc) { 100 | source = android.sourceSets.main.java.srcDirs 101 | 102 | options { 103 | title = "Realm Secure UserStore ${project.version}" 104 | memberLevel = JavadocMemberLevel.PUBLIC 105 | docEncoding = 'UTF-8' 106 | encoding = 'UTF-8' 107 | charSet = 'UTF-8' 108 | locale = 'en_US' 109 | 110 | links "http://docs.oracle.com/javase/7/docs/api/" 111 | links "https://realm.io/docs/java/${rootProject.realmVersion}/api/" 112 | linksOffline "http://developer.android.com/reference/", "${project.android.sdkDirectory}/docs/reference" 113 | } 114 | exclude '**/BuildConfig.java' 115 | exclude '**/R.java' 116 | } 117 | 118 | task sourcesJar(type: Jar) { 119 | from android.sourceSets.main.java.srcDirs 120 | classifier = 'sources' 121 | } 122 | 123 | task javadocJar(type: Jar, dependsOn: javadoc) { 124 | classifier = 'javadoc' 125 | from javadoc.destinationDir 126 | } 127 | 128 | install { 129 | repositories.mavenInstaller { 130 | pom { 131 | project { 132 | packaging 'aar' 133 | 134 | // Add your description here 135 | name 'secure-userstore' 136 | description 'Encrypt the user token obtained from Realm Object Server. Realm is a mobile database: a replacement for SQLite & ORMs.' 137 | url 'http://realm.io' 138 | 139 | // Set your license 140 | licenses { 141 | license { 142 | name 'The Apache Software License, Version 2.0' 143 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 144 | distribution 'repo' 145 | } 146 | } 147 | issueManagement { 148 | system 'github' 149 | url 'https://github.com/realm/realm-android-user-store/issues' 150 | } 151 | scm { 152 | url 'scm:https://github.com/realm/realm-java' 153 | connection 'scm:git@github.com/realm/realm-android-user-store.git' 154 | developerConnection 'scm:git@github.com/realm/realm-android-user-store.git' 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | publishing { 162 | publications { 163 | AAR(MavenPublication) { 164 | groupId 'io.realm' 165 | artifactId 'secure-userstore' 166 | version project.version 167 | artifact file("${rootDir}/app/build/outputs/aar/secure-userstore-release.aar") 168 | artifact sourcesJar 169 | artifact javadocJar 170 | } 171 | } 172 | } 173 | 174 | bintray { 175 | user = project.hasProperty('bintrayUser') ? bintrayUser : 'noUser' 176 | key = project.hasProperty('bintrayKey') ? bintrayKey : 'noKey' 177 | 178 | dryRun = false 179 | publish = false 180 | 181 | configurations = ['archives'] 182 | 183 | pkg { 184 | repo = 'maven' 185 | name = 'secure-userstore' 186 | desc = 'Realm Secure UserStore' 187 | websiteUrl = 'http://realm.io' 188 | issueTrackerUrl = 'https://github.com/realm/realm-android-user-store/issues' 189 | vcsUrl = 'https://github.com/realm/realm-android-user-store.git' 190 | licenses = ['Apache-2.0'] 191 | labels = ['android', 'realm'] 192 | publicDownloadNumbers = false 193 | } 194 | } 195 | 196 | artifactory { 197 | contextUrl = 'https://oss.jfrog.org/artifactory' 198 | publish { 199 | repository { 200 | repoKey = 'oss-snapshot-local' 201 | username = project.hasProperty('bintrayUser') ? bintrayUser : 'noUser' 202 | password = project.hasProperty('bintrayKey') ? bintrayKey : 'noKey' 203 | maven = true 204 | } 205 | defaults { 206 | publishConfigs('archives') 207 | publishPom = true 208 | publishIvy = false 209 | } 210 | } 211 | } 212 | 213 | artifacts { 214 | archives javadocJar 215 | archives sourcesJar 216 | } 217 | 218 | // See https://github.com/chrisbanes/gradle-mvn-push/pull/13 219 | afterEvaluate { 220 | javadoc.classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 221 | javadoc.classpath += project.android.libraryVariants.toList().first().javaCompile.classpath 222 | } -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Nabil/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/realm/TestHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Realm Inc. 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 | * http://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 io.realm; 18 | 19 | import android.annotation.SuppressLint; 20 | 21 | import org.json.JSONArray; 22 | import org.json.JSONException; 23 | import org.json.JSONObject; 24 | 25 | import java.lang.reflect.InvocationTargetException; 26 | import java.lang.reflect.Method; 27 | import java.util.UUID; 28 | 29 | import io.realm.internal.objectserver.ObjectServerUser; 30 | import io.realm.internal.objectserver.Token; 31 | 32 | public class TestHelper { 33 | public static String USER_TOKEN = UUID.randomUUID().toString(); 34 | public static String REALM_TOKEN = UUID.randomUUID().toString(); 35 | 36 | private final static Method SYNC_MANAGER_RESET_METHOD; 37 | static { 38 | try { 39 | SYNC_MANAGER_RESET_METHOD = SyncManager.class.getDeclaredMethod("reset"); 40 | SYNC_MANAGER_RESET_METHOD.setAccessible(true); 41 | } catch (NoSuchMethodException e) { 42 | throw new AssertionError(e); 43 | } 44 | } 45 | 46 | public static SyncUser createTestUser() { 47 | return createTestUser(Long.MAX_VALUE); 48 | } 49 | 50 | 51 | public static SyncUser createTestUser(long expires) { 52 | return createTestUser(expires, "JohnDoe"); 53 | } 54 | 55 | @SuppressLint("SdCardPath") 56 | public static SyncUser createTestUser(long expires, String identity) { 57 | Token userToken = new Token(USER_TOKEN, identity, null, expires, null); 58 | Token accessToken = new Token(REALM_TOKEN, identity, "/foo", expires, new Token.Permission[] {Token.Permission.DOWNLOAD }); 59 | ObjectServerUser.AccessDescription desc = new ObjectServerUser.AccessDescription(accessToken, "/data/data/myapp/files/default", false); 60 | 61 | JSONObject obj = new JSONObject(); 62 | try { 63 | JSONArray realmList = new JSONArray(); 64 | JSONObject realmDesc = new JSONObject(); 65 | realmDesc.put("uri", "realm://objectserver.realm.io/default"); 66 | realmDesc.put("description", desc.toJson()); 67 | realmList.put(realmDesc); 68 | 69 | obj.put("authUrl", "http://objectserver.realm.io/auth"); 70 | obj.put("userToken", userToken.toJson()); 71 | obj.put("realms", realmList); 72 | return SyncUser.fromJson(obj.toString()); 73 | } catch (JSONException e) { 74 | throw new RuntimeException(e); 75 | } 76 | } 77 | 78 | public static void resetSyncMetadata() { 79 | try { 80 | SYNC_MANAGER_RESET_METHOD.invoke(null); 81 | } catch (InvocationTargetException e) { 82 | throw new AssertionError(e); 83 | } catch (IllegalAccessException e) { 84 | throw new AssertionError(e); 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/realm/android/SecureUserStoreTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Realm Inc. 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 | * http://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 io.realm.android; 18 | 19 | import android.app.KeyguardManager; 20 | import android.content.Context; 21 | import android.os.Build; 22 | import android.support.annotation.RequiresApi; 23 | import android.support.test.InstrumentationRegistry; 24 | import android.support.test.runner.AndroidJUnit4; 25 | 26 | import org.junit.After; 27 | import org.junit.Before; 28 | import org.junit.Rule; 29 | import org.junit.Test; 30 | import org.junit.runner.RunWith; 31 | 32 | import java.security.KeyStoreException; 33 | import java.util.ArrayList; 34 | import java.util.Collections; 35 | import java.util.Comparator; 36 | import java.util.List; 37 | 38 | import io.realm.Realm; 39 | import io.realm.RealmConfiguration; 40 | import io.realm.SyncManager; 41 | import io.realm.SyncUser; 42 | import io.realm.TestHelper; 43 | import io.realm.rule.TestRealmConfigurationFactory; 44 | 45 | import static org.junit.Assert.assertEquals; 46 | import static org.junit.Assert.assertNotEquals; 47 | import static org.junit.Assert.assertNull; 48 | import static org.junit.Assert.assertTrue; 49 | import static org.junit.Assert.fail; 50 | 51 | @RunWith(AndroidJUnit4.class) 52 | public class SecureUserStoreTests { 53 | @Rule 54 | public final TestRealmConfigurationFactory configFactory = new TestRealmConfigurationFactory(); 55 | 56 | private Realm realm; 57 | private SecureUserStore userStore; 58 | 59 | @Before 60 | public void setUp() throws KeyStoreException { 61 | Realm.init(InstrumentationRegistry.getTargetContext()); 62 | 63 | // This will set the 'm_metadata_manager' in 'sync_manager.cpp' to be 'null' 64 | // causing the SyncUser to remain in memory. 65 | // They're actually not persisted into disk. 66 | // move this call to `tearDown` to clean in-memory & on-disk users 67 | // once https://github.com/realm/realm-object-store/issues/207 is resolved 68 | TestHelper.resetSyncMetadata(); 69 | 70 | RealmConfiguration realmConfig = configFactory.createConfiguration(); 71 | realm = Realm.getInstance(realmConfig); 72 | 73 | userStore = new SecureUserStore(InstrumentationRegistry.getTargetContext()); 74 | assertTrue("KeyStore Should be Unlocked before running tests on device!", userStore.isKeystoreUnlocked()); 75 | SyncManager.setUserStore(userStore); 76 | } 77 | 78 | @After 79 | public void tearDown() { 80 | if (realm != null) { 81 | realm.close(); 82 | } 83 | } 84 | 85 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) 86 | @Test 87 | public void keystore_unlocked() throws KeyStoreException { 88 | if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN) { 89 | fail("Test not supported under this Android API level"); 90 | } else { 91 | // check if the keystore is unlocked before proceeding 92 | KeyguardManager kgm = (KeyguardManager) InstrumentationRegistry.getTargetContext().getSystemService(Context.KEYGUARD_SERVICE); 93 | // is KeyguardSecure excludes the slide lock case. 94 | boolean isKeyguardSecure = (kgm != null) && kgm.isKeyguardSecure(); 95 | 96 | CipherClient client = new CipherClient(InstrumentationRegistry.getTargetContext()); 97 | 98 | assertEquals(isKeyguardSecure, client.isKeystoreUnlocked()); 99 | assertTrue(isKeyguardSecure); 100 | assertTrue(client.isKeystoreUnlocked()); 101 | } 102 | } 103 | 104 | @Test 105 | public void get() throws KeyStoreException { 106 | SyncUser user = TestHelper.createTestUser(); 107 | 108 | userStore.put(user); 109 | SyncUser decrypted_entry = userStore.get(user.getIdentity()); 110 | assertEquals(user, decrypted_entry); 111 | } 112 | 113 | @Test 114 | public void get_shouldNotCreateDuplicateKeyStore() throws KeyStoreException { 115 | SyncUser user = TestHelper.createTestUser(); 116 | userStore.put(user); 117 | 118 | // Using a new instance, should not cause a creation of a new RSA key (SyncCrypto#create_key_if_not_available) 119 | userStore = new SecureUserStore(InstrumentationRegistry.getTargetContext()); 120 | 121 | SyncUser decrypted_entry = userStore.get(user.getIdentity()); 122 | assertEquals(user, decrypted_entry); 123 | 124 | user.logout(); 125 | assertNull(userStore.get(user.getIdentity())); 126 | } 127 | 128 | @Test 129 | public void getCurrent() throws KeyStoreException { 130 | SyncUser user = TestHelper.createTestUser(); 131 | 132 | userStore.put(user); 133 | SyncUser decrypted_entry = userStore.getCurrent(); 134 | assertEquals(user, decrypted_entry); 135 | } 136 | 137 | @Test 138 | public void getCurrent_ShouldThrowIfMultiple() throws KeyStoreException { 139 | SyncUser user1 = TestHelper.createTestUser(Long.MAX_VALUE, "user1"); 140 | SyncUser user2 = TestHelper.createTestUser(Long.MAX_VALUE, "user2"); 141 | 142 | assertNotEquals(user1, user2); 143 | 144 | userStore.put(user1); 145 | userStore.put(user2); 146 | 147 | SyncUser decrypted_entry = null; 148 | try { 149 | decrypted_entry = userStore.getCurrent(); 150 | fail(); 151 | } catch (IllegalStateException expected) { 152 | assertEquals("Current user is not valid if more that one valid, logged-in user exists.", expected.getMessage()); 153 | } 154 | assertNull(decrypted_entry); 155 | } 156 | 157 | @Test 158 | public void remove() throws KeyStoreException { 159 | SyncUser user = TestHelper.createTestUser(); 160 | 161 | userStore.put(user); 162 | SyncUser decrypted_entry = userStore.getCurrent(); 163 | assertEquals(user, decrypted_entry); 164 | 165 | userStore.remove(user.getIdentity()); 166 | 167 | assertNull(userStore.getCurrent()); 168 | } 169 | 170 | @Test 171 | public void allUsers() throws KeyStoreException { 172 | SyncUser user1 = TestHelper.createTestUser(Long.MAX_VALUE, "user1"); 173 | SyncUser user2 = TestHelper.createTestUser(Long.MAX_VALUE, "user2"); 174 | 175 | assertNotEquals(user1, user2); 176 | 177 | userStore.put(user1); 178 | userStore.put(user2); 179 | 180 | user2.logout(); 181 | SyncUser currentUser = userStore.getCurrent(); 182 | assertEquals(user1, currentUser); 183 | 184 | SyncUser user3 = TestHelper.createTestUser(Long.MAX_VALUE, "user3"); 185 | userStore.put(user3); 186 | 187 | List syncUsers = new ArrayList(userStore.allUsers()); 188 | Collections.sort(syncUsers, new Comparator() { 189 | @Override 190 | public int compare(SyncUser o1, SyncUser o2) { 191 | return o1.getIdentity().compareTo(o2.getIdentity()); 192 | } 193 | }); 194 | assertEquals(2, syncUsers.size()); 195 | assertEquals(user1, syncUsers.get(0)); 196 | assertEquals(user3, syncUsers.get(1)); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/realm/rule/TestRealmConfigurationFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.rule; 18 | 19 | import android.support.test.InstrumentationRegistry; 20 | 21 | import org.junit.rules.TemporaryFolder; 22 | import org.junit.runner.Description; 23 | import org.junit.runners.model.Statement; 24 | 25 | import java.util.Collections; 26 | import java.util.Map; 27 | import java.util.Set; 28 | import java.util.concurrent.ConcurrentHashMap; 29 | 30 | import io.realm.Realm; 31 | import io.realm.RealmConfiguration; 32 | 33 | /** 34 | * Rule that creates the {@link RealmConfiguration } in a temporary directory and deletes the Realm created with that 35 | * configuration once the test finishes. Be sure to close all Realm instances before finishing the test. Otherwise 36 | * {@link Realm#deleteRealm(RealmConfiguration)} will throw an exception in the {@link #after()} method. 37 | * The temp directory will be deleted regardless if the {@link Realm#deleteRealm(RealmConfiguration)} fails or not. 38 | */ 39 | public class TestRealmConfigurationFactory extends TemporaryFolder { 40 | private Map map = new ConcurrentHashMap(); 41 | private Set configurations = Collections.newSetFromMap(map); 42 | protected boolean unitTestFailed = false; 43 | 44 | @Override 45 | public Statement apply(final Statement base, Description description) { 46 | return new Statement() { 47 | @Override 48 | public void evaluate() throws Throwable { 49 | before(); 50 | try { 51 | base.evaluate(); 52 | } catch (Throwable throwable) { 53 | unitTestFailed = true; 54 | throw throwable; 55 | } finally { 56 | after(); 57 | } 58 | } 59 | }; 60 | } 61 | 62 | @Override 63 | protected void before() throws Throwable { 64 | super.before(); 65 | Realm.init(InstrumentationRegistry.getTargetContext()); 66 | } 67 | 68 | @Override 69 | protected void after() { 70 | try { 71 | for (RealmConfiguration configuration : configurations) { 72 | Realm.deleteRealm(configuration); 73 | } 74 | } catch (IllegalStateException e) { 75 | // Only throw the exception caused by deleting the opened Realm if the test case itself doesn't throw. 76 | if (!unitTestFailed) { 77 | throw e; 78 | } 79 | } finally { 80 | // This will delete the temp directory. 81 | super.after(); 82 | } 83 | } 84 | 85 | public RealmConfiguration createConfiguration() { 86 | RealmConfiguration configuration = new RealmConfiguration.Builder() 87 | .directory(getRoot()) 88 | .build(); 89 | 90 | configurations.add(configuration); 91 | return configuration; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/CipherClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android; 18 | 19 | import android.content.Context; 20 | 21 | import java.security.KeyStoreException; 22 | 23 | import io.realm.android.internal.android.crypto.SyncCrypto; 24 | import io.realm.android.internal.android.crypto.SyncCryptoFactory; 25 | 26 | /** 27 | * A Helper to use the crypto API, it allows encryption/decryption and has methods to help test if the KeyStore is locked and help unlocked it. 28 | * This hides the complexity of different Android API to achieve those operations. 29 | * 30 | * This support Android API 9 and forwards. 31 | * This cipher uses the KeyStore provided by Android, hence we to need to be sure that the KeyStore is available 32 | * before doing any {@link #encrypt(String)}/{@link #decrypt(String)} by calling {@link #isKeystoreUnlocked()} then 33 | * {@link #unlockKeystore()}, note that the latter will open the system {@link android.app.Activity} to set a password/PIN/Pattern required 34 | * to unlock the screen & the KeyStore. 35 | */ 36 | public class CipherClient { 37 | private final SyncCrypto syncCrypto; 38 | 39 | public CipherClient(Context context) throws KeyStoreException { 40 | syncCrypto = SyncCryptoFactory.get(context); 41 | } 42 | 43 | /** 44 | * Takes some plain text {@link String} and return the encrypted version 45 | * of this {@link String} using the Android Key Store. 46 | * 47 | * @param user represents the Token of a {@link io.realm.SyncUser}. 48 | * @return the encrypted Token. 49 | * @throws KeyStoreException in case the Key Store is locked or other error. 50 | */ 51 | public String encrypt(String user) throws KeyStoreException { 52 | if (syncCrypto.is_keystore_unlocked()) { 53 | syncCrypto.create_key_if_not_available(); 54 | try { 55 | return syncCrypto.encrypt(user); 56 | } catch (KeyStoreException ex) { 57 | throw new KeyStoreException(ex); 58 | } 59 | } else { 60 | throw new KeyStoreException("Trying to use SecureUserStore without an unlocked KeyStore"); 61 | } 62 | } 63 | 64 | /** 65 | * Takes a previously {@link #encrypt(String)} to decrypted it 66 | * using the Android Key Store. 67 | * 68 | * @param user_encrypted represents the encrypted Token of a {@link io.realm.SyncUser}. 69 | * @return the decrypted Token. 70 | * @throws KeyStoreException in case the KeyStore is locked or other error. 71 | */ 72 | public String decrypt(String user_encrypted) throws KeyStoreException { 73 | if (syncCrypto.is_keystore_unlocked()) { 74 | try { 75 | return syncCrypto.decrypt(user_encrypted); 76 | } catch (KeyStoreException ex) { 77 | throw new KeyStoreException(ex); 78 | } 79 | } else { 80 | throw new KeyStoreException("Trying to use SecureUserStore without an unlocked KeyStore"); 81 | } 82 | } 83 | 84 | 85 | /** 86 | * Checks whether the Android KeyStore is available. 87 | * This should be called before {@link #encrypt(String)} or {@link #decrypt(String)} as those need the KeyStore unlocked. 88 | * @return {@code true} if the Android KeyStore in unlocked. 89 | * @throws KeyStoreException in case of error. 90 | */ 91 | public boolean isKeystoreUnlocked () throws KeyStoreException { 92 | return syncCrypto.is_keystore_unlocked(); 93 | } 94 | 95 | /** 96 | * Helps unlock the KeyStore this will launch the appropriate {@link android.content.Intent} 97 | * to start the platform system {@link android.app.Activity} to create/unlock the KeyStore. 98 | * 99 | * @throws KeyStoreException in case of error. 100 | */ 101 | public void unlockKeystore () throws KeyStoreException { 102 | syncCrypto.unlock_keystore(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/SecureUserStore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android; 18 | 19 | import android.content.Context; 20 | 21 | import java.security.KeyStoreException; 22 | import java.util.ArrayList; 23 | import java.util.Collection; 24 | import java.util.Collections; 25 | 26 | import io.realm.RealmFileUserStore; 27 | import io.realm.SyncUser; 28 | import io.realm.log.RealmLog; 29 | 30 | /** 31 | * Encrypt and decrypt the token ({@link SyncUser}) using Android built in KeyStore capabilities. 32 | * According to the Android API this picks the right algorithm to perform the operations. 33 | * Prior to API 18 there were no AndroidKeyStore API, but the Linux daemon existed, so it's possible 34 | * with the help of this code: https://github.com/nelenkov/android-keystore to work with. 35 | * 36 | * On API > = 18, we generate an AES key to encrypt we then generate and uses the RSA key inside the KeyStore 37 | * to encrypt the AES key that we store along the encrypted data inside the Realm Object Store. 38 | * 39 | * This throws a {@link KeyStoreException} in case of an error or KeyStore being unavailable (unlocked). 40 | * 41 | * See also: io.realm.internal.android.crypto.class.CipherClient 42 | * @see Android KeyStore 43 | */ 44 | public class SecureUserStore extends RealmFileUserStore { 45 | private final CipherClient cipherClient; 46 | 47 | public SecureUserStore(final Context context) throws KeyStoreException { 48 | cipherClient = new CipherClient(context); 49 | } 50 | 51 | /** 52 | * Encrypt then save a {@link SyncUser} object. If another user already exists, it will be replaced. 53 | * {@link SyncUser#getIdentity()} is used as a unique identifier of a given {@link SyncUser}. 54 | * 55 | * @param user {@link SyncUser} object to store. 56 | */ 57 | @Override 58 | public void put(SyncUser user) { 59 | try { 60 | String userSerialisedAndEncrypted = cipherClient.encrypt(user.toJson()); 61 | nativeUpdateOrCreateUser(user.getIdentity(), userSerialisedAndEncrypted, user.getAuthenticationUrl().toString()); 62 | } catch (KeyStoreException e) { 63 | RealmLog.error(e); 64 | } 65 | } 66 | 67 | /** 68 | * Retrieves and decrypts the current {@link SyncUser}. 69 | *

70 | * This method will throw an exception if more than one valid, logged in users exist. 71 | * @return {@link SyncUser} object or {@code null} if not found. 72 | */ 73 | @Override 74 | public SyncUser getCurrent() { 75 | String userJson = nativeGetCurrentUser(); 76 | return toDecryptedSyncUserOrNull(userJson); 77 | } 78 | 79 | /** 80 | * Retrieves and decrypts the specified {@link SyncUser}. 81 | *

82 | * This method will throw an exception if more than one valid, logged in users exist. 83 | * @return {@link SyncUser} object or {@code null} if not found. 84 | */ 85 | @Override 86 | public SyncUser get(String identity) { 87 | String userJson = nativeGetUser(identity); 88 | return toDecryptedSyncUserOrNull(userJson); 89 | } 90 | 91 | /** 92 | * Retries all {@link SyncUser}. 93 | * @return Active (logged-in) users. 94 | */ 95 | @Override 96 | public Collection allUsers() { 97 | String[] allUsers = nativeGetAllUsers(); 98 | if (allUsers != null && allUsers.length > 0) { 99 | ArrayList users = new ArrayList(allUsers.length); 100 | for (String userJson : allUsers) { 101 | String userSerialisedAndDecrypted = null; 102 | try { 103 | userSerialisedAndDecrypted = cipherClient.decrypt(userJson); 104 | } catch (KeyStoreException e) { 105 | RealmLog.error(e); 106 | // returning null will probably penalise the other Users 107 | } 108 | users.add(SyncUser.fromJson(userSerialisedAndDecrypted)); 109 | } 110 | return users; 111 | } 112 | return Collections.emptyList(); 113 | } 114 | 115 | /** 116 | * Checks whether the Android KeyStore is available. 117 | * This should be called before {@link #get(String)}, {@link #allUsers()})}, {@link #getCurrent()} or {@link #put(SyncUser)} as those need the KeyStore unlocked. 118 | * @return {@code true} if the Android KeyStore in unlocked. 119 | * @throws KeyStoreException in case of error. 120 | */ 121 | public boolean isKeystoreUnlocked () throws KeyStoreException { 122 | return cipherClient.isKeystoreUnlocked(); 123 | } 124 | 125 | /** 126 | * Helps unlock the KeyStore this will launch the appropriate {@link android.content.Intent} 127 | * to start the platform system {@link android.app.Activity} to create/unlock the KeyStore. 128 | * 129 | * @throws KeyStoreException in case of error. 130 | */ 131 | public void unlockKeystore () throws KeyStoreException { 132 | cipherClient.unlockKeystore(); 133 | } 134 | 135 | private SyncUser toDecryptedSyncUserOrNull(String userEncryptedJson) { 136 | if (userEncryptedJson != null) { 137 | try { 138 | String userSerialisedAndDecrypted = cipherClient.decrypt(userEncryptedJson); 139 | return SyncUser.fromJson(userSerialisedAndDecrypted); 140 | } catch (KeyStoreException e) { 141 | RealmLog.error(e); 142 | return null; 143 | } 144 | } 145 | return null; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/CipherFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android.internal.android.crypto; 18 | 19 | import android.os.Build; 20 | 21 | import java.security.NoSuchAlgorithmException; 22 | import java.security.NoSuchProviderException; 23 | 24 | import javax.crypto.Cipher; 25 | import javax.crypto.NoSuchPaddingException; 26 | 27 | import io.realm.android.internal.android.crypto.ciper.CipherJB; 28 | import io.realm.android.internal.android.crypto.ciper.CipherLegacy; 29 | import io.realm.android.internal.android.crypto.ciper.CipherMM; 30 | 31 | /** 32 | * Return an appropriate {@link Cipher} given the version of Android. 33 | * Ex: on API 23 OpenSSL is replaced by BoringSSL. 34 | */ 35 | public class CipherFactory { 36 | 37 | private static final boolean IS_JB43 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; 38 | private static final boolean IS_MM = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; 39 | private static final boolean IS_GINGERBREAD = Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD; 40 | 41 | public static Cipher get() throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException { 42 | if (IS_MM) { 43 | return CipherMM.get(); 44 | } else if (IS_JB43) { 45 | return CipherJB.get(); 46 | } else if (IS_GINGERBREAD) { 47 | return CipherLegacy.get(); 48 | } else { 49 | throw new IllegalArgumentException("Not supported yet"); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/SyncCrypto.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android.internal.android.crypto; 18 | 19 | import android.app.Activity; 20 | 21 | import java.security.KeyStoreException; 22 | 23 | /** 24 | * Define methods that Android API should expose regardless of the API version. 25 | */ 26 | public interface SyncCrypto { 27 | /** 28 | * Encrypt the plain text using an {@code AES} key. 29 | * The {@code AES} key is encrypted using an {@code RSA} (stored in the Android KeyStore) key then saved 30 | * along the encrypted text using a delimiter. 31 | * 32 | * All operations require the KeyStore to be unlocked (authorized by the user authenticating with fingerprint/PIN/Pattern). 33 | * 34 | * @param plainText text to encrypt. 35 | * @return concatenated {@link String} of the encrypted text plus the encrypted {@code AES} used 36 | * to encrypt the text. 37 | * @throws KeyStoreException for any encryption error. 38 | */ 39 | String encrypt(String plainText) throws KeyStoreException; 40 | 41 | /** 42 | * Decrypt the text previously encrypted with {@link #encrypt(String)}. 43 | * 44 | * This will first extract the encrypted {@code AES} key, then decrypt it using 45 | * the previously generated {@code RSA} private key. The decrypted {@code AES} key will 46 | * be used tp decrypt the text. 47 | * 48 | * All operations require the KeyStore to be unlocked (authorized by the user authenticating with fingerprint/PIN/Pattern). 49 | * 50 | * @param cipherText encrypted text. 51 | * @return decrypted text. 52 | * @throws KeyStoreException for any decryption error. 53 | */ 54 | String decrypt(String cipherText) throws KeyStoreException; 55 | 56 | /** 57 | * Generates an asymmetric key pair ({@code RSA}) in the Android Keystore. 58 | * 59 | * All operations require the KeyStore to be unlocked (authorized by the user authenticating with fingerprint/PIN/Pattern). 60 | * @throws KeyStoreException for any KeyStore error. 61 | */ 62 | void create_key_if_not_available() throws KeyStoreException; 63 | 64 | /** 65 | * Check if the Android KeyStore is unlocked. 66 | * @return {@code true} if the KeyStore is unlocked, {@code false} otherwise. 67 | * @throws KeyStoreException for any KeyStore error. 68 | */ 69 | boolean is_keystore_unlocked() throws KeyStoreException; 70 | 71 | /** 72 | * Launch the Android {@link android.content.Intent} that will help define or unlock the KeyStore. 73 | * 74 | * Application are encouraged to check if the KeyStore is unlocked by overriding {@link Activity#onResume()} 75 | * when the user finishes defining unlocking the KeyStore or cancel and go bak to the application. 76 | *

77 |      *  protected void onResume() {
78 |      *   super.onResume();
79 |      *   try {
80 |      *     // We return to the app after the KeyStore is unlocked or not.
81 |      *       if (cryptoClient.isKeystoreUnlocked()) {
82 |      *       // Encrypt/Decrypt
83 |      *       } else {
84 |      *       // Invite the user to unlock the KeyStore to continue
85 |      *       }
86 |      *   } catch (KeyStoreException e) {
87 |      *       e.printStackTrace();
88 |      *     }
89 |      *   }
90 |      * 
91 | * @throws KeyStoreException for any KeyStore error. 92 | */ 93 | void unlock_keystore() throws KeyStoreException; 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/SyncCryptoFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android.internal.android.crypto; 18 | 19 | import android.content.Context; 20 | import android.os.Build; 21 | 22 | import java.security.KeyStoreException; 23 | 24 | import io.realm.android.internal.android.crypto.api_18.SyncCryptoApi18Impl; 25 | import io.realm.android.internal.android.crypto.api_23.SyncCryptoApi23Impl; 26 | import io.realm.android.internal.android.crypto.api_legacy.SyncCryptoLegacy; 27 | 28 | /** 29 | * Return an appropriate {@link SyncCrypto} given the version of Android. 30 | */ 31 | public class SyncCryptoFactory { 32 | 33 | private static final boolean IS_API_23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; 34 | private static final boolean IS_API_18 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; 35 | private static final boolean IS_API_LEGACY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD; 36 | 37 | public static SyncCrypto get (Context context) throws KeyStoreException { 38 | if (IS_API_23) { 39 | return new SyncCryptoApi23Impl(context); 40 | } else if (IS_API_18) { 41 | return new SyncCryptoApi18Impl(context); 42 | } else if (IS_API_LEGACY) { 43 | return new SyncCryptoLegacy(context); 44 | } else { 45 | throw new KeyStoreException("Unknown android version"); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/api_18/SyncCryptoApi18Impl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android.internal.android.crypto.api_18; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.ActivityNotFoundException; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.content.pm.PackageInfo; 24 | import android.content.pm.PackageManager; 25 | import android.os.Build; 26 | import android.security.KeyPairGeneratorSpec; 27 | 28 | import java.io.ByteArrayInputStream; 29 | import java.io.ByteArrayOutputStream; 30 | import java.io.IOException; 31 | import java.io.UnsupportedEncodingException; 32 | import java.lang.reflect.InvocationTargetException; 33 | import java.lang.reflect.Method; 34 | import java.math.BigInteger; 35 | import java.security.InvalidKeyException; 36 | import java.security.KeyPairGenerator; 37 | import java.security.KeyStore; 38 | import java.security.KeyStoreException; 39 | import java.security.NoSuchAlgorithmException; 40 | import java.security.NoSuchProviderException; 41 | import java.security.SecureRandom; 42 | import java.security.UnrecoverableEntryException; 43 | import java.security.cert.CertificateException; 44 | import java.security.interfaces.RSAPublicKey; 45 | import java.util.ArrayList; 46 | import java.util.Calendar; 47 | 48 | import javax.crypto.BadPaddingException; 49 | import javax.crypto.Cipher; 50 | import javax.crypto.CipherInputStream; 51 | import javax.crypto.CipherOutputStream; 52 | import javax.crypto.IllegalBlockSizeException; 53 | import javax.crypto.KeyGenerator; 54 | import javax.crypto.NoSuchPaddingException; 55 | import javax.crypto.SecretKey; 56 | import javax.crypto.spec.SecretKeySpec; 57 | import javax.security.auth.x500.X500Principal; 58 | 59 | import io.realm.android.internal.android.crypto.CipherFactory; 60 | import io.realm.android.internal.android.crypto.SyncCrypto; 61 | import io.realm.android.internal.android.crypto.misc.Base64; 62 | import io.realm.android.internal.android.crypto.misc.PRNGFixes; 63 | 64 | import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 65 | 66 | /** 67 | * Implements {@link SyncCrypto} methods for API 18 (after the Android KeyStore public API). 68 | */ 69 | public class SyncCryptoApi18Impl implements SyncCrypto { 70 | private final static String DELIMITER = "]"; 71 | 72 | protected java.security.KeyStore keyStore; 73 | protected Context context; 74 | protected static final String X500_PRINCIPAL = "CN=Sync, O=Realm"; 75 | protected static final String ANDROID_KEYSTORE = "AndroidKeyStore"; 76 | protected String alias = "Realm"; 77 | 78 | public static final String UNLOCK_ACTION = "com.android.credentials.UNLOCK"; 79 | 80 | public SyncCryptoApi18Impl (Context context) throws KeyStoreException { 81 | PRNGFixes.apply(); 82 | this.context = context; 83 | try { 84 | PackageInfo pi = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); 85 | alias += "_" + pi.packageName; // make the alias unique per package 86 | 87 | keyStore = java.security.KeyStore.getInstance(ANDROID_KEYSTORE); 88 | keyStore.load(null); 89 | } catch (KeyStoreException e) { 90 | e.printStackTrace(); 91 | throw new KeyStoreException(e); 92 | } catch (CertificateException e) { 93 | e.printStackTrace(); 94 | throw new KeyStoreException(e); 95 | } catch (NoSuchAlgorithmException e) { 96 | e.printStackTrace(); 97 | throw new KeyStoreException(e); 98 | } catch (IOException e) { 99 | e.printStackTrace(); 100 | throw new KeyStoreException(e); 101 | } catch (PackageManager.NameNotFoundException ignored) { 102 | } 103 | } 104 | 105 | @Override 106 | public String encrypt(String plainText) throws KeyStoreException { 107 | try { 108 | SecretKey key = generateAESKey(); 109 | byte[] encrypted = encryptedUsingAESKey(key, plainText); 110 | byte[] encryptedKey = encryptAESKeyUsingRSA(key); 111 | // append with AES enc with RSA 112 | return String.format("%s%s%s", Base64.to(encryptedKey), DELIMITER, 113 | Base64.to(encrypted)); 114 | } catch (Exception e) { 115 | throw new KeyStoreException(e); 116 | } 117 | } 118 | 119 | @Override 120 | public String decrypt(String cipherText) throws KeyStoreException { 121 | try { 122 | String[] fields = cipherText.split(DELIMITER); 123 | if (fields.length != 2) { 124 | throw new IllegalArgumentException("Invalid encrypted text format"); 125 | } 126 | 127 | byte[] aesEncWithRSA = Base64.from(fields[0]); 128 | byte[] encToken = Base64.from(fields[1]); 129 | 130 | // decrypt AES using RSA 131 | SecretKey key = decryptAESKeyUsingRSA(aesEncWithRSA); 132 | 133 | // decrypt Token using decrypted AES 134 | return decryptedUsingAESKey(key, encToken); 135 | } catch (Exception e) { 136 | throw new KeyStoreException(e); 137 | } 138 | } 139 | 140 | @Override 141 | public boolean is_keystore_unlocked() throws KeyStoreException { 142 | try { 143 | Class keyStoreClass = Class.forName("android.security.KeyStore"); 144 | Method getInstanceMethod = keyStoreClass.getMethod("getInstance"); 145 | Object invoke = getInstanceMethod.invoke(null); 146 | 147 | Method isUnlockedMethod = keyStoreClass.getMethod("isUnlocked"); 148 | boolean isUnlocked = (boolean)isUnlockedMethod.invoke(invoke); 149 | return isUnlocked; 150 | } catch (ClassNotFoundException e) { 151 | throw new KeyStoreException(e); 152 | } catch (NoSuchMethodException e) { 153 | throw new KeyStoreException(e); 154 | } catch (IllegalAccessException e) { 155 | throw new KeyStoreException(e); 156 | } catch (InvocationTargetException e) { 157 | throw new KeyStoreException(e); 158 | } 159 | } 160 | 161 | @Override 162 | public void unlock_keystore() throws KeyStoreException { 163 | try { 164 | Intent intent = new Intent(UNLOCK_ACTION); 165 | intent.addFlags(FLAG_ACTIVITY_NEW_TASK); 166 | context.startActivity(intent); 167 | } catch (ActivityNotFoundException e) { 168 | throw new KeyStoreException(e); 169 | } 170 | } 171 | 172 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) 173 | public void create_key_if_not_available() throws KeyStoreException { 174 | try { 175 | if (!keyStore.containsAlias(alias)) { 176 | Calendar start = Calendar.getInstance(); 177 | Calendar end = Calendar.getInstance(); 178 | end.add(Calendar.YEAR, 10); 179 | 180 | KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context) 181 | .setAlias(alias) 182 | .setSubject(new X500Principal(X500_PRINCIPAL)) 183 | .setSerialNumber(BigInteger.ONE) 184 | .setStartDate(start.getTime()) 185 | .setEndDate(end.getTime()) 186 | .build(); 187 | KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", 188 | "AndroidKeyStore"); 189 | generator.initialize(spec); 190 | generator.generateKeyPair(); 191 | } 192 | } catch (Exception e) { 193 | throw new KeyStoreException(e); 194 | } 195 | } 196 | 197 | private SecretKey generateAESKey() throws NoSuchAlgorithmException { 198 | // Generate a 256-bit key 199 | final int outputKeyLength = 256; 200 | 201 | SecureRandom secureRandom = new SecureRandom(); 202 | // Do *not* seed secureRandom! Automatically seeded from system entropy. 203 | KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); 204 | keyGenerator.init(outputKeyLength, secureRandom); 205 | SecretKey key = keyGenerator.generateKey(); 206 | return key; 207 | } 208 | 209 | private byte[] encryptedUsingAESKey(SecretKey key, String plainText) throws KeyStoreException { 210 | try { 211 | Cipher cipher = Cipher.getInstance("AES"); 212 | cipher.init(Cipher.ENCRYPT_MODE, key); 213 | return cipher.doFinal(plainText.getBytes("UTF-8")); 214 | } catch (NoSuchAlgorithmException e) { 215 | throw new KeyStoreException(e); 216 | } catch (NoSuchPaddingException e) { 217 | throw new KeyStoreException(e); 218 | } catch (BadPaddingException e) { 219 | throw new KeyStoreException(e); 220 | } catch (UnsupportedEncodingException e) { 221 | throw new KeyStoreException(e); 222 | } catch (IllegalBlockSizeException e) { 223 | throw new KeyStoreException(e); 224 | } catch (InvalidKeyException e) { 225 | throw new KeyStoreException(e); 226 | } 227 | } 228 | 229 | private String decryptedUsingAESKey(SecretKey key, byte[] cipherText) throws KeyStoreException { 230 | try { 231 | Cipher cipher = Cipher.getInstance("AES"); 232 | cipher.init(Cipher.DECRYPT_MODE, key); 233 | byte[] encrypted = cipher.doFinal(cipherText); 234 | return new String(encrypted, "UTF-8"); 235 | } catch (NoSuchAlgorithmException e) { 236 | throw new KeyStoreException(e); 237 | } catch (NoSuchPaddingException e) { 238 | throw new KeyStoreException(e); 239 | } catch (BadPaddingException e) { 240 | throw new KeyStoreException(e); 241 | } catch (UnsupportedEncodingException e) { 242 | throw new KeyStoreException(e); 243 | } catch (IllegalBlockSizeException e) { 244 | throw new KeyStoreException(e); 245 | } catch (InvalidKeyException e) { 246 | throw new KeyStoreException(e); 247 | } 248 | } 249 | 250 | private byte[] encryptAESKeyUsingRSA(SecretKey key) throws KeyStoreException { 251 | try { 252 | KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null); 253 | RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey(); 254 | 255 | Cipher cipher = CipherFactory.get(); 256 | cipher.init(Cipher.ENCRYPT_MODE, publicKey); 257 | 258 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 259 | CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); 260 | cipherOutputStream.write(key.getEncoded()); 261 | cipherOutputStream.close(); 262 | 263 | return outputStream.toByteArray(); 264 | } catch (NoSuchPaddingException e) { 265 | throw new KeyStoreException(e); 266 | } catch (NoSuchAlgorithmException e) { 267 | throw new KeyStoreException(e); 268 | } catch (NoSuchProviderException e) { 269 | throw new KeyStoreException(e); 270 | } catch (InvalidKeyException e) { 271 | throw new KeyStoreException(e); 272 | } catch (KeyStoreException e) { 273 | throw new KeyStoreException(e); 274 | } catch (UnrecoverableEntryException e) { 275 | throw new KeyStoreException(e); 276 | } catch (IOException e) { 277 | throw new KeyStoreException(e); 278 | } 279 | } 280 | 281 | private SecretKeySpec decryptAESKeyUsingRSA(byte[] aesEncKey) throws KeyStoreException { 282 | try { 283 | KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null); 284 | Cipher cipher = CipherFactory.get(); 285 | cipher.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey()); 286 | 287 | CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(aesEncKey), cipher); 288 | ArrayList values = new ArrayList<>(); 289 | int nextByte; 290 | while ((nextByte = cipherInputStream.read()) != -1) { 291 | values.add((byte)nextByte); 292 | } 293 | 294 | final byte[] bytes = new byte[values.size()]; 295 | for (int i = 0; i < bytes.length; i++) { 296 | bytes[i] = values.get(i).byteValue(); 297 | } 298 | 299 | SecretKeySpec originalKey = new SecretKeySpec(bytes, "AES"); 300 | return originalKey; 301 | } catch (NoSuchPaddingException e) { 302 | throw new KeyStoreException(e); 303 | } catch (NoSuchAlgorithmException e) { 304 | throw new KeyStoreException(e); 305 | } catch (NoSuchProviderException e) { 306 | throw new KeyStoreException(e); 307 | } catch (UnsupportedEncodingException e) { 308 | throw new KeyStoreException(e); 309 | } catch (IOException e) { 310 | throw new KeyStoreException(e); 311 | } catch (InvalidKeyException e) { 312 | throw new KeyStoreException(e); 313 | } catch (UnrecoverableEntryException e) { 314 | throw new KeyStoreException(e); 315 | } catch (KeyStoreException e) { 316 | throw new KeyStoreException(e); 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/api_23/SyncCryptoApi23Impl.java: -------------------------------------------------------------------------------- 1 | package io.realm.android.internal.android.crypto.api_23; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.security.keystore.KeyGenParameterSpec; 7 | import android.security.keystore.KeyProperties; 8 | 9 | import java.math.BigInteger; 10 | import java.security.InvalidAlgorithmParameterException; 11 | import java.security.KeyPairGenerator; 12 | import java.security.KeyStoreException; 13 | import java.security.NoSuchAlgorithmException; 14 | import java.security.NoSuchProviderException; 15 | import java.util.Calendar; 16 | import javax.security.auth.x500.X500Principal; 17 | 18 | import io.realm.android.internal.android.crypto.SyncCrypto; 19 | import io.realm.android.internal.android.crypto.api_18.SyncCryptoApi18Impl; 20 | 21 | /** 22 | * Implements {@link SyncCrypto} methods for API 23 (after the Android KeyStore public API), using 23 | * the introduced {@link android.security.keystore.KeyGenParameterSpec}. 24 | */ 25 | public class SyncCryptoApi23Impl extends SyncCryptoApi18Impl { 26 | public SyncCryptoApi23Impl(Context context) throws KeyStoreException { 27 | super(context); 28 | } 29 | 30 | @TargetApi(Build.VERSION_CODES.M) 31 | @Override 32 | public void create_key_if_not_available() throws KeyStoreException { 33 | if (!keyStore.containsAlias(alias)) { 34 | try { 35 | Calendar start = Calendar.getInstance(); 36 | Calendar end = Calendar.getInstance(); 37 | end.add(Calendar.YEAR, 10); 38 | 39 | KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEYSTORE); 40 | KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, 41 | KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 42 | .setCertificateSubject(new X500Principal(X500_PRINCIPAL)) 43 | .setCertificateSerialNumber(BigInteger.ONE) 44 | .setCertificateNotBefore(start.getTime()) 45 | .setCertificateNotAfter(end.getTime()) 46 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) 47 | .setRandomizedEncryptionRequired(false) 48 | .build(); 49 | keyGenerator.initialize(spec); 50 | keyGenerator.generateKeyPair(); 51 | } catch (NoSuchAlgorithmException e) { 52 | e.printStackTrace(); 53 | throw new KeyStoreException(e); 54 | } catch (InvalidAlgorithmParameterException e) { 55 | e.printStackTrace(); 56 | throw new KeyStoreException(e); 57 | } catch (NoSuchProviderException e) { 58 | e.printStackTrace(); 59 | throw new KeyStoreException(e); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/api_legacy/SyncCryptoLegacy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android.internal.android.crypto.api_legacy; 18 | 19 | import android.content.ActivityNotFoundException; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.content.pm.PackageInfo; 23 | import android.net.LocalSocket; 24 | import android.net.LocalSocketAddress; 25 | 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.io.OutputStream; 29 | import java.io.UnsupportedEncodingException; 30 | import java.security.GeneralSecurityException; 31 | import java.security.KeyStoreException; 32 | import java.security.SecureRandom; 33 | import java.util.ArrayList; 34 | 35 | import javax.crypto.Cipher; 36 | import javax.crypto.KeyGenerator; 37 | import javax.crypto.SecretKey; 38 | import javax.crypto.spec.IvParameterSpec; 39 | import javax.crypto.spec.SecretKeySpec; 40 | 41 | import io.realm.android.internal.android.crypto.CipherFactory; 42 | import io.realm.android.internal.android.crypto.SyncCrypto; 43 | import io.realm.android.internal.android.crypto.misc.Base64; 44 | import io.realm.android.internal.android.crypto.misc.PRNGFixes; 45 | 46 | import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 47 | 48 | /** 49 | * Implements {@link SyncCrypto} methods for API 9 to 18 (pre Android KeyStore public API). 50 | */ 51 | public class SyncCryptoLegacy implements SyncCrypto { 52 | private Context context; 53 | private String alias = "Realm"; 54 | private int mError = NO_ERROR; 55 | private SecureRandom random = new SecureRandom(); 56 | 57 | private static final String UNLOCK_ACTION = "android.credentials.UNLOCK"; 58 | 59 | // ResponseCodes 60 | private static final int NO_ERROR = 1; 61 | private static final int LOCKED = 2; 62 | private static final int UNINITIALIZED = 3; 63 | private static final int PROTOCOL_ERROR = 5; 64 | 65 | // States 66 | private enum State { 67 | UNLOCKED, LOCKED, UNINITIALIZED 68 | } 69 | 70 | private static final LocalSocketAddress sAddress = new LocalSocketAddress( 71 | "keystore", LocalSocketAddress.Namespace.RESERVED); 72 | private final static int KEY_LENGTH = 256; 73 | private final static String DELIMITER = "]"; 74 | 75 | public SyncCryptoLegacy (Context context) throws KeyStoreException { 76 | PRNGFixes.apply(); 77 | this.context = context; 78 | 79 | try { 80 | PackageInfo pi = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); 81 | alias += "_" + pi.packageName; // make the alias unique per package 82 | } catch (Exception ignore) { 83 | } 84 | } 85 | 86 | @Override 87 | public String encrypt(String plainText) throws KeyStoreException { 88 | try { 89 | Cipher cipher = CipherFactory.get(); 90 | 91 | byte[] iv = generateIv(cipher.getBlockSize()); 92 | IvParameterSpec ivParams = new IvParameterSpec(iv); 93 | 94 | SecretKeySpec key = new SecretKeySpec(get(alias), "AES"); 95 | 96 | cipher.init(Cipher.ENCRYPT_MODE, key, ivParams); 97 | byte[] cipherText = cipher.doFinal(plainText.getBytes("UTF-8")); 98 | 99 | return String.format("%s%s%s", Base64.to(iv), DELIMITER, 100 | Base64.to(cipherText)); 101 | } catch (GeneralSecurityException e) { 102 | throw new KeyStoreException(e); 103 | } catch (UnsupportedEncodingException e) { 104 | throw new KeyStoreException(e); 105 | } 106 | } 107 | 108 | @Override 109 | public String decrypt(String cipherText) throws KeyStoreException { 110 | byte[] keyBytes = get(alias); 111 | if (keyBytes == null) { 112 | return null; 113 | } 114 | SecretKeySpec key = new SecretKeySpec(keyBytes, "AES"); 115 | 116 | try { 117 | String[] fields = cipherText.split(DELIMITER); 118 | if (fields.length != 2) { 119 | throw new IllegalArgumentException("Invalid encrypted text format"); 120 | } 121 | 122 | byte[] iv = Base64.from(fields[0]); 123 | byte[] cipherBytes = Base64.from(fields[1]); 124 | Cipher cipher = CipherFactory.get(); 125 | IvParameterSpec ivParams = new IvParameterSpec(iv); 126 | cipher.init(Cipher.DECRYPT_MODE, key, ivParams); 127 | byte[] plaintext = cipher.doFinal(cipherBytes); 128 | return new String(plaintext, "UTF-8"); 129 | } catch (GeneralSecurityException e) { 130 | throw new KeyStoreException(e); 131 | } catch (UnsupportedEncodingException e) { 132 | throw new KeyStoreException(e); 133 | } 134 | } 135 | 136 | @Override 137 | public boolean is_keystore_unlocked() throws KeyStoreException { 138 | return state() == State.UNLOCKED; 139 | } 140 | 141 | @Override 142 | public void unlock_keystore() throws KeyStoreException { 143 | try { 144 | Intent intent = new Intent(UNLOCK_ACTION); 145 | intent.addFlags(FLAG_ACTIVITY_NEW_TASK); 146 | context.startActivity(intent); 147 | } catch (ActivityNotFoundException e) { 148 | throw new KeyStoreException(e); 149 | } 150 | } 151 | 152 | @Override 153 | public void create_key_if_not_available() throws KeyStoreException { 154 | if (get(alias) == null) { 155 | try { 156 | KeyGenerator kg = KeyGenerator.getInstance("AES"); 157 | kg.init(KEY_LENGTH); 158 | SecretKey key = kg.generateKey(); 159 | 160 | boolean success = put(getBytes(alias), key.getEncoded()); 161 | if (!success) { 162 | throw new KeyStoreException("Keystore error"); 163 | } 164 | } catch (Exception e) { 165 | throw new KeyStoreException(e); 166 | } 167 | } 168 | } 169 | 170 | private State state() throws KeyStoreException { 171 | execute('t'); 172 | switch (mError) { 173 | case NO_ERROR: 174 | return State.UNLOCKED; 175 | case LOCKED: 176 | return State.LOCKED; 177 | case UNINITIALIZED: 178 | return State.UNINITIALIZED; 179 | default: 180 | throw new KeyStoreException("" + mError); 181 | } 182 | } 183 | 184 | private byte[] get(byte[] key) { 185 | ArrayList values = execute('g', key); 186 | return (values == null || values.isEmpty()) ? null : values.get(0); 187 | } 188 | 189 | private byte[] get(String key) { 190 | return get(getBytes(key)); 191 | } 192 | 193 | private boolean put(byte[] key, byte[] value) { 194 | execute('i', key, value); 195 | return mError == NO_ERROR; 196 | } 197 | 198 | private ArrayList execute(int code, byte[]... parameters) { 199 | mError = PROTOCOL_ERROR; 200 | 201 | for (byte[] parameter : parameters) { 202 | if (parameter == null || parameter.length > 65535) { 203 | return null; 204 | } 205 | } 206 | 207 | LocalSocket socket = new LocalSocket(); 208 | try { 209 | socket.connect(sAddress); 210 | 211 | OutputStream out = socket.getOutputStream(); 212 | out.write(code); 213 | for (byte[] parameter : parameters) { 214 | out.write(parameter.length >> 8); 215 | out.write(parameter.length); 216 | out.write(parameter); 217 | } 218 | out.flush(); 219 | socket.shutdownOutput(); 220 | 221 | InputStream in = socket.getInputStream(); 222 | if ((code = in.read()) != NO_ERROR) { 223 | if (code != -1) { 224 | mError = code; 225 | } 226 | return null; 227 | } 228 | 229 | ArrayList values = new ArrayList(); 230 | while (true) { 231 | int i, j; 232 | if ((i = in.read()) == -1) { 233 | break; 234 | } 235 | if ((j = in.read()) == -1) { 236 | return null; 237 | } 238 | byte[] value = new byte[i << 8 | j]; 239 | for (i = 0; i < value.length; i += j) { 240 | if ((j = in.read(value, i, value.length - i)) == -1) { 241 | return null; 242 | } 243 | } 244 | values.add(value); 245 | } 246 | mError = NO_ERROR; 247 | return values; 248 | } catch (IOException e) { 249 | e.printStackTrace(); 250 | } finally { 251 | try { 252 | socket.close(); 253 | } catch (IOException e) { 254 | } 255 | } 256 | return null; 257 | } 258 | 259 | private static byte[] getBytes(String string) { 260 | try { 261 | return string.getBytes("UTF-8"); 262 | } catch (UnsupportedEncodingException e) { 263 | throw new RuntimeException(e); 264 | } 265 | } 266 | 267 | private byte[] generateIv(int length) { 268 | byte[] b = new byte[length]; 269 | random.nextBytes(b); 270 | return b; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/ciper/CipherJB.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android.internal.android.crypto.ciper; 18 | 19 | import java.security.NoSuchAlgorithmException; 20 | import java.security.NoSuchProviderException; 21 | 22 | import javax.crypto.NoSuchPaddingException; 23 | 24 | /** 25 | * Return a {@link javax.crypto.Cipher} that works for the API ≥ 18. 26 | */ 27 | public class CipherJB { 28 | public static javax.crypto.Cipher get() throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException { 29 | return javax.crypto.Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/ciper/CipherLegacy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android.internal.android.crypto.ciper; 18 | 19 | import java.security.NoSuchAlgorithmException; 20 | import java.security.NoSuchProviderException; 21 | 22 | import javax.crypto.NoSuchPaddingException; 23 | 24 | /** 25 | * Return a {@link javax.crypto.Cipher} that works for the legacy API 9 to 18. 26 | */ 27 | public class CipherLegacy { 28 | public static javax.crypto.Cipher get() throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException { 29 | return javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/ciper/CipherMM.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android.internal.android.crypto.ciper; 18 | 19 | import java.security.NoSuchAlgorithmException; 20 | import java.security.NoSuchProviderException; 21 | 22 | import javax.crypto.NoSuchPaddingException; 23 | 24 | /** 25 | * Return a {@link javax.crypto.Cipher} that works for API ≥ 23. 26 | */ 27 | public class CipherMM { 28 | public static javax.crypto.Cipher get() throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException { 29 | return javax.crypto.Cipher.getInstance("RSA/ECB/PKCS1Padding"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/misc/Base64.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Realm Inc. 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 | * http://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 io.realm.android.internal.android.crypto.misc; 18 | 19 | /** 20 | * Base64 helper methods. 21 | */ 22 | public class Base64 { 23 | public static String to(byte[] bytes) { 24 | return android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP); 25 | } 26 | 27 | public static byte[] from(String base64) { 28 | return android.util.Base64.decode(base64, android.util.Base64.NO_WRAP); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/realm/android/internal/android/crypto/misc/PRNGFixes.java: -------------------------------------------------------------------------------- 1 | package io.realm.android.internal.android.crypto.misc; 2 | 3 | import android.os.Build; 4 | import android.os.Process; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.DataOutputStream; 8 | import java.io.IOException; 9 | import java.io.UnsupportedEncodingException; 10 | 11 | // Based on http://android-developers.blogspot.jp/2013/08/some-securerandom-thoughts.html 12 | public class PRNGFixes { 13 | 14 | private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial(); 15 | 16 | private PRNGFixes() { 17 | } 18 | 19 | public static void apply() { 20 | applyOpenSSLFix(); 21 | } 22 | 23 | public static void applyOpenSSLFix() throws SecurityException { 24 | if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) 25 | || (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) { 26 | // No need to apply the fix 27 | return; 28 | } 29 | 30 | try { 31 | // Mix in the device- and invocation-specific seed. 32 | Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") 33 | .getMethod("RAND_seed", byte[].class) 34 | .invoke(null, generateSeed()); 35 | 36 | // Mix output of Linux PRNG into OpenSSL's PRNG 37 | int bytesRead = (Integer) Class 38 | .forName( 39 | "org.apache.harmony.xnet.provider.jsse.NativeCrypto") 40 | .getMethod("RAND_load_file", String.class, long.class) 41 | .invoke(null, "/dev/urandom", 1024); 42 | if (bytesRead != 1024) { 43 | throw new IOException( 44 | "Unexpected number of bytes read from Linux PRNG: " 45 | + bytesRead); 46 | } 47 | } catch (Exception e) { 48 | throw new SecurityException("Failed to seed OpenSSL PRNG", e); 49 | } 50 | } 51 | 52 | private static byte[] generateSeed() { 53 | try { 54 | ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); 55 | DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer); 56 | seedBufferOut.writeLong(System.currentTimeMillis()); 57 | seedBufferOut.writeLong(System.nanoTime()); 58 | seedBufferOut.writeInt(Process.myPid()); 59 | seedBufferOut.writeInt(Process.myUid()); 60 | seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); 61 | seedBufferOut.close(); 62 | return seedBuffer.toByteArray(); 63 | } catch (IOException e) { 64 | throw new SecurityException("Failed to generate seed", e); 65 | } 66 | } 67 | 68 | private static String getDeviceSerialNumber() { 69 | // We're using the Reflection API because Build.SERIAL is only available 70 | // since API Level 9 (Gingerbread, Android 2.3). 71 | try { 72 | return (String) Build.class.getField("SERIAL").get(null); 73 | } catch (Exception ignored) { 74 | return null; 75 | } 76 | } 77 | 78 | private static byte[] getBuildFingerprintAndDeviceSerial() { 79 | StringBuilder result = new StringBuilder(); 80 | String fingerprint = Build.FINGERPRINT; 81 | if (fingerprint != null) { 82 | result.append(fingerprint); 83 | } 84 | String serial = getDeviceSerialNumber(); 85 | if (serial != null) { 86 | result.append(serial); 87 | } 88 | try { 89 | return result.toString().getBytes("UTF-8"); 90 | } catch (UnsupportedEncodingException e) { 91 | throw new RuntimeException("UTF-8 encoding not supported"); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | apply plugin: 'ch.netzwerg.release' 3 | project.ext.sdkVersion = 24 4 | project.ext.buildTools = '24.0.3' 5 | project.ext.realmVersion = '3.1.2' 6 | 7 | buildscript { 8 | repositories { 9 | jcenter() 10 | maven { url "${System.env.HOME}/.m2/repository" } 11 | } 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:2.2.2' 14 | classpath 'ch.netzwerg:gradle-release-plugin:1.2.0' // Manage Release tags / versioning 15 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' // Allows you to install AAR's locally 16 | classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:4.0.1' // OJO integration 17 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6' // Bintray integration 18 | classpath 'com.novoda:gradle-android-command-plugin:1.5.0' // ADB commands from Gradle (monkey) 19 | } 20 | } 21 | 22 | allprojects { 23 | group = 'io.realm' 24 | version = file("${rootDir}/version.txt").text.trim(); 25 | repositories { 26 | jcenter() 27 | } 28 | } 29 | 30 | configurations.all { 31 | // Check for updates every build 32 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds' 33 | } 34 | 35 | // Configure ch.netzwerg.release plugin 36 | // See https://github.com/netzwerg/gradle-release-plugin 37 | task build {} 38 | release { 39 | push = false 40 | versionSuffix = '-SNAPSHOT' 41 | tagPrefix = 'v' 42 | } 43 | 44 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 134 | 135 | 136 | 137 | 138 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /config/findbugs/findbugs-filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /config/pmd/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Realm PMD ruleset 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/realm-android-user-store/8943f188f4b1478e00d337405e6a9dbb3fa65fb5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 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-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realm/realm-android-user-store/8943f188f4b1478e00d337405e6a9dbb3fa65fb5/logo.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.0.2-SNAPSHOT --------------------------------------------------------------------------------