'
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 | 
103 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------