Browse Source

Merge pull request #1944 from nextcloud/e2e_rebased

E2E
Tobias Kaminsky 7 năm trước cách đây
mục cha
commit
c650f3a341
69 tập tin đã thay đổi với 5609 bổ sung328 xóa
  1. 1 1
      .drone.yml
  2. 11 6
      build.gradle
  3. 1 0
      drawable_resources/decrypt.svg
  4. 1 0
      drawable_resources/encrypt.svg
  5. 1 0
      drawable_resources/ic_list_encrypted_folder.svg
  6. 1 0
      lint.xml
  7. 1 1
      scripts/lint/lint-results.txt
  8. 2 1
      settings.gradle
  9. 42 0
      src/androidTest/assets/decrypted.json
  10. 26 0
      src/androidTest/assets/encrypted.json
  11. BIN
      src/androidTest/assets/encrypted/ia7OEEEyXMoRa1QWQk8r
  12. BIN
      src/androidTest/assets/encrypted/n9WXAIXO2wRY4R8nXwmo
  13. 103 0
      src/androidTest/assets/ia7OEEEyXMoRa1QWQk8r
  14. BIN
      src/androidTest/assets/n9WXAIXO2wRY4R8nXwmo
  15. BIN
      src/androidTest/assets/srEPevoPqPZpPEaeDnS3
  16. 3 6
      src/androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityTest.java
  17. 1 1
      src/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java
  18. 1 1
      src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java
  19. 0 0
      src/androidTest/java/com/owncloud/android/uiautomator/InitialTest.java
  20. 365 0
      src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java
  21. 16 7
      src/main/java/com/owncloud/android/MainApp.java
  22. 205 0
      src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java
  23. 94 0
      src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java
  24. 10 1
      src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  25. 66 4
      src/main/java/com/owncloud/android/datamodel/OCFile.java
  26. 3 1
      src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java
  27. 17 1
      src/main/java/com/owncloud/android/db/OCUpload.java
  28. 7 2
      src/main/java/com/owncloud/android/db/ProviderMeta.java
  29. 50 6
      src/main/java/com/owncloud/android/files/FileMenuFilter.java
  30. 2 2
      src/main/java/com/owncloud/android/files/services/FileDownloader.java
  31. 19 10
      src/main/java/com/owncloud/android/files/services/FileUploader.java
  32. 9 1
      src/main/java/com/owncloud/android/jobs/AccountRemovalJob.java
  33. 12 4
      src/main/java/com/owncloud/android/operations/CreateFolderOperation.java
  34. 40 5
      src/main/java/com/owncloud/android/operations/DownloadFileOperation.java
  35. 133 97
      src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  36. 25 7
      src/main/java/com/owncloud/android/operations/RemoveFileOperation.java
  37. 173 0
      src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java
  38. 564 118
      src/main/java/com/owncloud/android/operations/UploadFileOperation.java
  39. 59 9
      src/main/java/com/owncloud/android/providers/FileContentProvider.java
  40. 6 10
      src/main/java/com/owncloud/android/services/OperationsService.java
  41. 0 2
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  42. 1 1
      src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java
  43. 24 1
      src/main/java/com/owncloud/android/ui/adapter/FileListListAdapter.java
  44. 1 1
      src/main/java/com/owncloud/android/ui/adapter/UploaderAdapter.java
  45. 408 0
      src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java
  46. 37 0
      src/main/java/com/owncloud/android/ui/events/EncryptionEvent.java
  47. 110 8
      src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java
  48. 7 0
      src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  49. 12 2
      src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java
  50. 10 1
      src/main/java/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java
  51. 2 2
      src/main/java/com/owncloud/android/utils/ConnectivityUtils.java
  52. 75 0
      src/main/java/com/owncloud/android/utils/CsrHelper.java
  53. 643 0
      src/main/java/com/owncloud/android/utils/EncryptionUtils.java
  54. 12 1
      src/main/java/com/owncloud/android/utils/FileStorageUtils.java
  55. 8 4
      src/main/java/com/owncloud/android/utils/MimeTypeUtil.java
  56. BIN
      src/main/res/drawable-hdpi/ic_list_encrypted_folder.png
  57. BIN
      src/main/res/drawable-mdpi/ic_list_encrypted_folder.png
  58. BIN
      src/main/res/drawable-xhdpi/ic_list_encrypted_folder.png
  59. BIN
      src/main/res/drawable-xxhdpi/ic_list_encrypted_folder.png
  60. BIN
      src/main/res/drawable-xxxhdpi/ic_list_encrypted_folder.png
  61. 29 0
      src/main/res/drawable/e2e_border.xml
  62. 60 0
      src/main/res/layout/setup_encryption_dialog.xml
  63. 12 0
      src/main/res/menu/file_actions_menu.xml
  64. 2048 0
      src/main/res/raw/encryption_key_words.txt
  65. 14 2
      src/main/res/values-de/strings.xml
  66. 1 0
      src/main/res/values/colors.xml
  67. 1 0
      src/main/res/values/setup.xml
  68. 19 0
      src/main/res/values/strings.xml
  69. 5 1
      src/test/java/com/owncloud/android/utils/ErrorMessageAdapterUnitTest.java

+ 1 - 1
.drone.yml

@@ -24,7 +24,7 @@ pipeline:
       #- adb shell am instrument -w -e debug false -e class com.owncloud.android.datamodel.OCFileUnitTest com.owncloud.android.test/android.support.test.runner.AndroidJUnitRunner
 
     environment:
-      - ANDROID_TARGET=android-24
+      - ANDROID_TARGET=android-31
       - ANDROID_ABI=armeabi-v7a
       - LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/:/opt/android-sdk-linux/tools/lib64/gles_mesa/
 

+ 11 - 6
build.gradle

@@ -192,9 +192,10 @@ android {
 }
 
 dependencies {
-    /// dependencies for app building
+    // dependencies for app building
     implementation 'com.android.support:multidex:1.0.2'
-    implementation 'com.github.nextcloud:android-library:1.0.33'
+//    implementation project('nextcloud-android-library')
+    implementation 'com.github.nextcloud:android-library:1.0.34'
     versionDevImplementation 'com.github.nextcloud:android-library:master-SNAPSHOT' // use always latest master
     implementation "com.android.support:support-v4:${supportLibraryVersion}"
     implementation "com.android.support:design:${supportLibraryVersion}"
@@ -212,6 +213,10 @@ dependencies {
     implementation 'org.greenrobot:eventbus:3.0.0'
     implementation 'com.googlecode.ez-vcard:ez-vcard:0.10.2'
     implementation 'org.lukhnos:nnio:0.2'
+
+    compile 'com.madgag.spongycastle:pkix:1.54.0.0'
+    compile 'com.google.code.gson:gson:2.8.2'
+
     // uncomment for gplay, modified
     // implementation "com.google.firebase:firebase-messaging:${googleLibraryVersion}"
     // implementation "com.google.android.gms:play-services-base:${googleLibraryVersion}"
@@ -223,10 +228,10 @@ dependencies {
     implementation 'com.caverock:androidsvg:1.2.1'
     implementation "com.android.support:support-annotations:${supportLibraryVersion}"
 
-    /// dependencies for local unit tests
+    // dependencies for local unit tests
     testImplementation 'junit:junit:4.12'
     testImplementation 'org.mockito:mockito-core:1.10.19'
-    /// dependencies for instrumented tests
+    // dependencies for instrumented tests
     // JUnit4 Rules
     androidTestImplementation 'com.android.support.test:rules:1.0.1'
     // Android JUnit Runner
@@ -236,7 +241,7 @@ dependencies {
     androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
     androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.1'
     // UIAutomator - for cross-app UI tests, and to grant screen is turned on in Espresso tests
-    //androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
+    androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
     // fix conflict in dependencies; see http://g.co/androidstudio/app-test-app-conflict for details
     //androidTestImplementation "com.android.support:support-annotations:${supportLibraryVersion}"
     implementation 'org.jetbrains:annotations:15.0'
@@ -249,7 +254,7 @@ configurations.all {
 }
 
 tasks.withType(Test) {
-    /// increased logging for tests
+    // increased logging for tests
     testLogging {
         events "passed", "skipped", "failed"
     }

+ 1 - 0
drawable_resources/decrypt.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 71 100"><path stroke-width=".16" d="m8 0c-2.2091 0-4 1.7909-4 4v3h-1v7h10v-7h-1-2-2-2v-3c0-1.1046 0.8954-2 2-2s2 0.8954 2 2v1h2v-1c0-2.2091-1.791-4-4-4z" transform="matrix(6.25,0,0,6.25,-14.5,0)"/></svg>

+ 1 - 0
drawable_resources/encrypt.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 71 100"><path d="M35.5 6.25c-13.807 0-25 11.193-25 25v12.5H4.25V87.5h62.5V43.75H60.5v-12.5c0-13.807-11.194-25-25-25zm0 12.5c6.904 0 12.5 5.596 12.5 12.5v12.5H23v-12.5c0-6.904 5.596-12.5 12.5-12.5z"/></svg>

+ 1 - 0
drawable_resources/ic_list_encrypted_folder.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1"><path fill-rule="evenodd" fill="#0082c9" d="m1.4609 2c-0.25 0-0.4609 0.2109-0.4609 0.4609v11.078c0 0.258 0.2029 0.461 0.4609 0.461h13.078c0.258 0 0.461-0.203 0.461-0.461v-9.0761c0-0.25-0.211-0.4649-0.461-0.4649h-6.539l-2-1.998h-4.5391zm6.5391 3.8008c0.8836 0 1.5996 0.7159 1.5996 1.5996v0.7988h0.4004v2.8008h-4v-2.8008h0.4004v-0.7988c0-0.8837 0.716-1.5996 1.5996-1.5996zm0 0.7988c-0.4419 0-0.8008 0.3589-0.8008 0.8008v0.7988h1.6016v-0.7988c0-0.4419-0.3589-0.8008-0.8008-0.8008z"/></svg>

+ 1 - 0
lint.xml

@@ -3,6 +3,7 @@
     <issue id="InvalidPackage">
         <ignore path="**/freemarker-2.3.23.jar"/>
         <ignore path="**/nnio-0.2.jar"/>
+        <ignore path="**/pkix-1.54.0.0.jar"/>
     </issue>
     
     <issue id="UnusedResources">

+ 1 - 1
scripts/lint/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 127 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 132 warnings</span>

+ 2 - 1
settings.gradle

@@ -1 +1,2 @@
-include ':'
+include ':'
+//include 'nextcloud-android-library'

+ 42 - 0
src/androidTest/assets/decrypted.json

@@ -0,0 +1,42 @@
+{
+   "metadata":{
+      "encrypted":{
+         "metadataKeys":{
+            "0":"s4k4LPDpxoO53TKwem3Lo1",
+            "2":"…",
+            "3":"NEWESTMETADATAKEY"
+         }
+      },
+      "initializationVector":"kahzfT4u86Knc+e3",
+      "sharing":{
+         "recipient":{
+            "blah@schiessle.org":"PUBLIC KEY",
+            "bjoern@schiessle.org":"PUBLIC KEY"
+         },
+         "signature":"HMACOFRECIPIENTANDNEWESTMETADATAKEY"
+      },
+      "version":1
+   },
+   "files":{
+      "ia7OEEEyXMoRa1QWQk8r":{
+         "encrypted":{
+            "key":"jtboLmgGR1OQf2uneqCVHpklQLlIwWL5TXAQ0keK",
+            "filename":"test.txt",
+            "authenticationTag":"HMAC of file",
+            "version":1
+         },
+         "metadataKey":0,
+         "initializationVector":"+mHu52HyZq+pAAIN"
+      },
+      "n9WXAIXO2wRY4R8nXwmo":{
+         "encrypted":{
+            "key":"s4k4LPDpxoO53TKwem3Lo1yJnbNUYH2KLrSFT8Ea",
+            "filename":"test2.txt",
+            "authenticationTag":"HMAC of file",
+            "version":1
+         },
+         "metadataKey":0,
+         "initializationVector":"sOFd17hCKWIv0gyB"
+      }
+   }
+} 

+ 26 - 0
src/androidTest/assets/encrypted.json

@@ -0,0 +1,26 @@
+{
+  "metadata":{
+    "encrypted":"L01QcEZlcnBGbGJYZk0zQVRpME5venpiMlorZkVKNEo0YXFyV0Vla25Ed0kzOWtFYUg3V0Y3RVRKdDYrOWc0Y095bmhJU1hCRDlVVWkvdFJTa2swa1NTcXlPTEFiRmhVUDZFSzRzUXhiYWkrRkRPQ3VuNk1PakVxNDlBSUhWYUZucUJIZWhyeWNQZzF2d1d0VHh0cFhud3FacE55TmZOaFRRaVA2Zz09",
+    "initializationVector":"kahzfT4u86Knc+e3",
+    "sharing":{
+      "recipient":{
+        "blah@schiessle.org":"PUBLIC KEY",
+        "bjoern@schiessle.org":"PUBLIC KEY"
+      },
+      "signature":"HMACOFRECIPIENTANDNEWESTMETADATAKEY"
+    },
+    "version":1
+  },
+  "files":{
+    "ia7OEEEyXMoRa1QWQk8r":{
+      "encrypted":"a2xMcFI0cERHa2lCM3U1ajR5UXdnLzNmN0dCK2xnSmk5ck93bHhYTTI2ZmdQQlNaLzkxOTRJK3pHTlJzSjhoTTNjdlBhb2VVaEhHdGtBd0MvVUJlbWd1VFlvZDFKM2hLSkNmZWhoNlhIclBJaGU3ZllQY3lnMHprV1M1QUpIOCs2aUE5Tno2ZkZtRHpYMExabXRZcUpyZnk5Y2hyUTEyL2M4RDE1VmliR1ltbUxqKzBTUlJyc2ZCdTRwenZiR1hCVjk5OTA5UDVjb0llUCtPcjhVM1VBL1ZUNkpPaDYvSlpSaHlHTkVDbEpDRT0\\u003d",
+      "metadataKey":0,
+      "initializationVector":"+mHu52HyZq+pAAIN"
+    },
+    "n9WXAIXO2wRY4R8nXwmo":{
+      "encrypted":"VncyZU4yZStaRmFqeXJEQkpZNlNZa09yL3FIbVNNVW1wVDFWTENJN0pnSVBkdzIySUlrRnFDMGdzcTMwdHZneFlweEJjeGt5Z0crSVlUUkdGVk5iUzlBczJaejFlNTZzeEQrTUVHVldjRGQ4VDVIN0p6ZFFlRWsvRkN4M2FoQXlFOHpXOHQ5TnhXQUYycmpvNE5xNVowUStPTGZPc0hqaVdpUUR3dm9TV0hPS3JSaVd5c1YwSEhOYmVzZkZQaEF4Mk0rLzdDU05jK2dmNmdqb2ZndzIwOC91YXNlQUlPb2FnV3k0dWd0SFAvYz0\\u003d",
+      "metadataKey":0,
+      "initializationVector":"sOFd17hCKWIv0gyB"
+    }
+  }
+}

BIN
src/androidTest/assets/encrypted/ia7OEEEyXMoRa1QWQk8r


BIN
src/androidTest/assets/encrypted/n9WXAIXO2wRY4R8nXwmo


+ 103 - 0
src/androidTest/assets/ia7OEEEyXMoRa1QWQk8r

@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="Layer_1"
+   x="0px"
+   y="0px"
+   viewBox="0 0 133.89203 94.627347"
+   enable-background="new 0 0 196.6 72"
+   xml:space="preserve"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="nextcloud-logo-white-transparent.svg"
+   width="133.89201"
+   height="94.62735"
+   inkscape:export-filename="nextcloud-logo-white-transparent.png"
+   inkscape:export-xdpi="300.09631"
+   inkscape:export-ydpi="300.09631"><metadata
+     id="metadata20"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+     id="defs18" /><sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2560"
+     inkscape:window-height="1359"
+     id="namedview16"
+     showgrid="false"
+     inkscape:zoom="4"
+     inkscape:cx="43.021274"
+     inkscape:cy="53.386932"
+     inkscape:current-layer="Layer_1"
+     fit-margin-top="10"
+     fit-margin-left="10"
+     fit-margin-right="10"
+     fit-margin-bottom="10"
+     inkscape:window-x="0"
+     inkscape:window-y="240"
+     inkscape:window-maximized="1"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:snap-page="true" /><path
+     style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#0082c9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5.56589985;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+     d="m 67.032801,9.9999701 c -11.80525,0 -21.81118,8.0031799 -24.91235,18.8465899 -2.69524,-5.75151 -8.53592,-9.78093 -15.26337,-9.78093 -9.25183,0 -16.85708,7.60525 -16.85708,16.85708 0,9.25182 7.60525,16.86054 16.85708,16.86054 6.72745,0 12.56813,-4.03188 15.26337,-9.78439 3.10117,10.84422 13.1071,18.85006 24.91235,18.85006 11.71795,0 21.67286,-7.8851 24.85334,-18.60701 2.74505,5.62192 8.513439,9.54134 15.145329,9.54134 9.25183,0 16.86055,-7.60872 16.86055,-16.86054 0,-9.25183 -7.60872,-16.85708 -16.86055,-16.85708 -6.63189,0 -12.400279,3.91696 -15.145329,9.53788 C 88.705661,17.88243 78.750751,9.9999701 67.032801,9.9999701 Z m 0,9.8954999 c 8.91163,0 16.03073,7.11564 16.03073,16.02724 0,8.9116 -7.1191,16.03071 -16.03073,16.03071 -8.91158,0 -16.02722,-7.11911 -16.02722,-16.03071 0,-8.9116 7.11564,-16.02724 16.02722,-16.02724 z m -40.17572,9.06567 c 3.90437,0 6.96504,3.05718 6.96504,6.96157 0,3.90438 -3.06067,6.96504 -6.96504,6.96504 -3.90439,0 -6.96158,-3.06066 -6.96158,-6.96504 0,-3.90439 3.05719,-6.96157 6.96158,-6.96157 z m 80.174389,0 c 3.9044,0 6.96504,3.05718 6.96504,6.96157 0,3.90438 -3.06066,6.96504 -6.96504,6.96504 -3.90437,0 -6.96156,-3.06066 -6.96156,-6.96504 0,-3.90439 3.05721,-6.96157 6.96156,-6.96157 z"
+     id="XMLID_107_"
+     inkscape:connector-curvature="0" /><g
+     id="g4571"
+     transform="matrix(0.47038519,0,0,0.47038519,21.389201,50.75959)"
+     style="opacity:1;fill:#0082c9;fill-opacity:1"><path
+       id="XMLID_121_"
+       d="m 37.669669,48.9 c 5.9,0 9.2,4.2 9.2,10.5 0,0.6 -0.5,1.1 -1.1,1.1 l -15.9,0 c 0.1,5.6 4,8.8 8.5,8.8 2.8,0 4.8,-1.2 5.8,-2 0.6,-0.4 1.1,-0.3 1.4,0.3 l 0.3,0.5 c 0.3,0.5 0.2,1 -0.3,1.4 -1.2,0.9 -3.8,2.4 -7.3,2.4 -6.5,0 -11.5,-4.7 -11.5,-11.5 0.1,-7.2 4.9,-11.5 10.9,-11.5 z m 6.1,9.4 c -0.2,-4.6 -3,-6.9 -6.2,-6.9 -3.7,0 -6.9,2.4 -7.6,6.9 l 13.8,0 z"
+       inkscape:connector-curvature="0"
+       style="fill:#0082c9;fill-opacity:1" /><path
+       id="XMLID_119_"
+       d="m 76.9,52.1 0,-2.5 0,-5.2 c 0,-0.7 0.4,-1.1 1.1,-1.1 l 0.8,0 c 0.7,0 1,0.4 1,1.1 l 0,5.2 4.5,0 c 0.7,0 1.1,0.4 1.1,1.1 l 0,0.3 c 0,0.7 -0.4,1 -1.1,1 l -4.5,0 0,11 c 0,5.1 3.1,5.7 4.8,5.8 0.9,0.1 1.2,0.3 1.2,1.1 l 0,0.6 c 0,0.7 -0.3,1 -1.2,1 -4.8,0 -7.7,-2.9 -7.7,-8.1 l 0,-11.3 z"
+       inkscape:connector-curvature="0"
+       style="fill:#0082c9;fill-opacity:1" /><path
+       id="XMLID_117_"
+       d="m 99.8,48.9 c 3.8,0 6.2,1.6 7.3,2.5 0.5,0.4 0.6,0.9 0.1,1.5 l -0.3,0.5 c -0.4,0.6 -0.9,0.6 -1.5,0.2 -1,-0.7 -2.9,-2 -5.5,-2 -4.8,0 -8.6,3.6 -8.6,8.9 0,5.2 3.8,8.8 8.6,8.8 3.1,0 5.2,-1.4 6.2,-2.3 0.6,-0.4 1,-0.3 1.4,0.3 l 0.3,0.4 c 0.3,0.6 0.2,1 -0.3,1.5 -1.1,0.9 -3.8,2.8 -7.8,2.8 -6.5,0 -11.5,-4.7 -11.5,-11.5 0.1,-6.8 5.1,-11.6 11.6,-11.6 z"
+       inkscape:connector-curvature="0"
+       style="fill:#0082c9;fill-opacity:1" /><path
+       id="XMLID_115_"
+       d="m 113.1,41.8 c 0,-0.7 -0.4,-1.1 0.3,-1.1 l 0.8,0 c 0.7,0 1.8,0.4 1.8,1.1 l 0,23.9 c 0,2.8 1.3,3.1 2.3,3.2 0.5,0 0.9,0.3 0.9,1 l 0,0.7 c 0,0.7 -0.3,1.1 -1.1,1.1 -1.8,0 -5,-0.6 -5,-5.4 l 0,-24.5 z"
+       inkscape:connector-curvature="0"
+       style="fill:#0082c9;fill-opacity:1" /><path
+       id="XMLID_112_"
+       d="m 133.6,48.9 c 6.4,0 11.6,4.9 11.6,11.4 0,6.6 -5.2,11.6 -11.6,11.6 -6.4,0 -11.6,-5 -11.6,-11.6 0,-6.5 5.2,-11.4 11.6,-11.4 z m 0,20.4 c 4.7,0 8.5,-3.8 8.5,-9 0,-5 -3.8,-8.7 -8.5,-8.7 -4.7,0 -8.6,3.8 -8.6,8.7 0.1,5.1 3.9,9 8.6,9 z"
+       inkscape:connector-curvature="0"
+       style="fill:#0082c9;fill-opacity:1" /><path
+       id="XMLID_109_"
+       d="m 183.5,48.9 c 5.3,0 7.2,4.4 7.2,4.4 l 0.1,0 c 0,0 -0.1,-0.7 -0.1,-1.7 l 0,-9.9 c 0,-0.7 -0.3,-1.1 0.4,-1.1 l 0.8,0 c 0.7,0 1.8,0.4 1.8,1.1 l 0,28.5 c 0,0.7 -0.3,1.1 -1,1.1 l -0.7,0 c -0.7,0 -1.1,-0.3 -1.1,-1 l 0,-1.7 c 0,-0.8 0.2,-1.4 0.2,-1.4 l -0.1,0 c 0,0 -1.9,4.6 -7.6,4.6 -5.9,0 -9.6,-4.7 -9.6,-11.5 -0.2,-6.8 3.9,-11.4 9.7,-11.4 z m 0.1,20.4 c 3.7,0 7.1,-2.6 7.1,-8.9 0,-4.5 -2.3,-8.8 -7,-8.8 -3.9,0 -7.1,3.2 -7.1,8.8 0.1,5.4 2.9,8.9 7,8.9 z"
+       inkscape:connector-curvature="0"
+       style="fill:#0082c9;fill-opacity:1" /><path
+       sodipodi:nodetypes="ssssssssssscccccsss"
+       style="fill:#0082c9;fill-opacity:1"
+       inkscape:connector-curvature="0"
+       d="m 1,71.4 0.8,0 c 0.7,0 1.1,-0.4 1.1,-1.1 l 0,-21.472335 C 2.9,45.427665 6.6,43 10.8,43 c 4.2,0 7.9,2.427665 7.9,5.827665 L 18.7,70.3 c 0,0.7 0.4,1.1 1.1,1.1 l 0.8,0 c 0.7,0 1,-0.4 1,-1.1 l 0,-21.6 c 0,-5.7 -5.7,-8.5 -10.9,-8.5 l 0,0 0,0 0,0 0,0 C 5.7,40.2 0,43 0,48.7 l 0,21.6 c 0,0.7 0.3,1.1 1,1.1 z"
+       id="XMLID_103_" /><path
+       style="fill:#0082c9;fill-opacity:1"
+       inkscape:connector-curvature="0"
+       d="m 167.9,49.4 -0.8,0 c -0.7,0 -1.1,0.4 -1.1,1.1 l 0,12.1 c 0,3.4 -2.2,6.5 -6.5,6.5 -4.2,0 -6.5,-3.1 -6.5,-6.5 l 0,-12.1 c 0,-0.7 -0.4,-1.1 -1.1,-1.1 l -0.8,0 c -0.7,0 -1,0.4 -1,1.1 l 0,12.9 c 0,5.7 4.2,8.5 9.4,8.5 l 0,0 c 0,0 0,0 0,0 0,0 0,0 0,0 l 0,0 c 5.2,0 9.4,-2.8 9.4,-8.5 l 0,-12.9 c 0.1,-0.7 -0.3,-1.1 -1,-1.1 z"
+       id="XMLID_102_" /><path
+       inkscape:connector-curvature="0"
+       id="path4165-9"
+       d="m 68.908203,49.235938 c -0.244942,0.0391 -0.480102,0.202589 -0.705078,0.470703 l -4.046875,4.824218 -3.029297,3.609375 -4.585937,-5.466796 -2.488282,-2.966797 c -0.224975,-0.268116 -0.479748,-0.414718 -0.74414,-0.4375 -0.264393,-0.02278 -0.538524,0.07775 -0.806641,0.302734 l -0.613281,0.513672 c -0.536232,0.449952 -0.508545,0.948144 -0.05859,1.484375 l 4.048828,4.824219 3.357422,4 -4.916016,5.857421 c -0.0037,0.0044 -0.0061,0.0093 -0.0098,0.01367 l -2.480469,2.955078 c -0.449952,0.536232 -0.399531,1.100832 0.136719,1.550782 l 0.613281,0.511718 c 0.536231,0.449951 1.022704,0.33701 1.472656,-0.199218 l 4.046875,-4.824219 3.029297,-3.609375 4.585938,5.466797 c 0.003,0.0036 0.0067,0.0062 0.0098,0.0098 l 2.480469,2.957032 c 0.44995,0.536231 1.012595,0.584735 1.548828,0.134765 l 0.613282,-0.513671 c 0.536231,-0.449952 0.508544,-0.948144 0.05859,-1.484376 l -4.048828,-4.824218 -3.357422,-4 4.916016,-5.857422 c 0.0037,-0.0044 0.0061,-0.0093 0.0098,-0.01367 l 2.480469,-2.955078 c 0.449952,-0.53623 0.399532,-1.10083 -0.136719,-1.550781 l -0.613281,-0.513672 c -0.268115,-0.224976 -0.522636,-0.308636 -0.767578,-0.269531 z"
+       style="fill:#0082c9;fill-opacity:1" /></g></svg>

BIN
src/androidTest/assets/n9WXAIXO2wRY4R8nXwmo


BIN
src/androidTest/assets/srEPevoPqPZpPEaeDnS3


+ 3 - 6
androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityTest.java → src/androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityTest.java

@@ -19,6 +19,7 @@
 
 package com.owncloud.android.authentication;
 
+import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
@@ -29,7 +30,6 @@ import android.support.test.runner.AndroidJUnit4;
 import android.support.test.uiautomator.UiDevice;
 import android.test.suitebuilder.annotation.LargeTest;
 
-import static org.junit.Assert.assertTrue;
 import com.owncloud.android.R;
 
 import org.junit.Before;
@@ -39,18 +39,15 @@ import org.junit.runner.RunWith;
 
 import java.lang.reflect.Field;
 
-import android.app.Activity;
-
 import static android.support.test.espresso.Espresso.onView;
 import static android.support.test.espresso.action.ViewActions.click;
 import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
 import static android.support.test.espresso.action.ViewActions.typeText;
-
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
 import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
-
-import static android.support.test.espresso.assertion.ViewAssertions.matches;
 import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertTrue;
 
 @RunWith(AndroidJUnit4.class)
 @LargeTest

+ 1 - 1
androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java → src/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java

@@ -120,7 +120,7 @@ public class OCFileUnitTest {
         );
         assertThat(fileReadFromParcel.getLastSyncDateForProperties(), is(LAST_SYNC_DATE_FOR_PROPERTIES));
         assertThat(fileReadFromParcel.getLastSyncDateForData(), is(LAST_SYNC_DATE_FOR_DATA));
-        assertThat(fileReadFromParcel.setAvailableOffline(), is(true));
+        assertThat(fileReadFromParcel.isAvailableOffline(), is(true));
         assertThat(fileReadFromParcel.getEtag(), is(ETAG));
         assertThat(fileReadFromParcel.isSharedViaLink(), is(true));
         assertThat(fileReadFromParcel.isSharedWithSharee(), is(true));

+ 1 - 1
androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java → src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java

@@ -38,7 +38,7 @@ public class UploadStorageManagerTest {
 
     @Test
     public void testDeleteAllUploads() {
-        //Clean
+        // Clean
         for (Account account : Accounts) {
             uploadsStorageManager.removeAccountUploads(account);
         }

+ 0 - 0
androidTest/java/com/owncloud/android/uiautomator/InitialTest.java → src/androidTest/java/com/owncloud/android/uiautomator/InitialTest.java


+ 365 - 0
src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java

@@ -0,0 +1,365 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2017 Tobias Kaminsky
+ * Copyright (C) 2017 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.util;
+
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import com.google.gson.reflect.TypeToken;
+import com.owncloud.android.datamodel.DecryptedFolderMetadata;
+import com.owncloud.android.datamodel.EncryptedFolderMetadata;
+import com.owncloud.android.utils.CsrHelper;
+import com.owncloud.android.utils.EncryptionUtils;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+@RequiresApi(api = Build.VERSION_CODES.KITKAT)
+@RunWith(AndroidJUnit4.class)
+public class EncryptionTestIT {
+    private String privateKey = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAo" +
+            "IBAQDsn0JKS/THu328z1IgN0VzYU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzV" +
+            "GzKFvGfZ03fwFrN7Q8P8R2e8SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7" +
+            "Y0BJX9i/nW/L0L/VaE8CZTAqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCi" +
+            "CC3qV99b0igRJGmmLQaGiAflhFzuDQPMifUMq75wI8RSRPdxUAtjTfkl68QHu7Umye" +
+            "yy33OQgdUKaTl5zcS3VSQbNjveVCNM4RDH1RlEc+7Wf1BY8APqT6jbiBcROJD2CeoL" +
+            "H2eiIJCi+61ZkSGfAgMBAAECggEBALFStCHrhBf+GL9a+qer4/8QZ/X6i91PmaBX/7" +
+            "SYk2jjjWVSXRNmex+V6+Y/jBRT2mvAgm8J+7LPwFdatE+lz0aZrMRD2gCWYF6Itpda" +
+            "90OlLkmQPVWWtGTgX2ta2tF5r2iSGzk0IdoL8zw98Q2UzpOcw30KnWtFMxuxWk0mHq" +
+            "pgp00g80cDWg3+RPbWOhdLp5bflQ36fKDfmjq05cGlIk6unnVyC5HXpvh4d4k2EWlX" +
+            "rjGsndVBPCjGkZePlLRgDHxT06r+5XdJ+1CBDZgCsmjGz3M8uOHyCfVW0WhB7ynzDT" +
+            "agVgz0iqpuhAi9sPt6iWWwpAnRw8cQgqEKw9bvKKECgYEA/WPi2PJtL6u/xlysh/H7" +
+            "A717CId6fPHCMDace39ZNtzUzc0nT5BemlcF0wZ74NeJSur3Q395YzB+eBMLs5p8mA" +
+            "95wgGvJhM65/J+HX+k9kt6Z556zLMvtG+j1yo4D0VEwm3xahB4SUUP+1kD7dNvo4+8" +
+            "xeSCyjzNllvYZZC0DrECgYEA7w8pEqhHHn0a+twkPCZJS+gQTB9Rm+FBNGJqB3XpWs" +
+            "TeLUxYRbVGk0iDve+eeeZ41drxcdyWP+WcL34hnrjgI1Fo4mK88saajpwUIYMy6+qM" +
+            "LY+jC2NRSBox56eH7nsVYvQQK9eKqv9wbB+PF9SwOIvuETN7fd8mAY02UnoaaU8CgY" +
+            "BoHRKocXPLkpZJuuppMVQiRUi4SHJbxDo19Tp2w+y0TihiJ1lvp7I3WGpcOt3LlMQk" +
+            "tEbExSvrRZGxZKH6Og/XqwQsYuTEkEIz679F/5yYVosE6GkskrOXQAfh8Mb3/04xVV" +
+            "tMaVgDQw0+CWVD4wyL+BNofGwBDNqsXTCdCsfxAQKBgQCDv2EtbRw0y1HRKv21QIxo" +
+            "ju5cZW4+cDfVPN+eWPdQFOs1H7wOPsc0aGRiiupV2BSEF3O1ApKziEE5U1QH+29bR4" +
+            "R8L1pemeGX8qCNj5bCubKjcWOz5PpouDcEqimZ3q98p3E6GEHN15UHoaTkx0yO/V8o" +
+            "j6zhQ9fYRxDHB5ACtQKBgQCOO7TJUO1IaLTjcrwS4oCfJyRnAdz49L1AbVJkIBK0fh" +
+            "JLecOFu3ZlQl/RStQb69QKb5MNOIMmQhg8WOxZxHcpmIDbkDAm/J/ovJXFSoBdOr5o" +
+            "uQsYsDZhsWW97zvLMzg5pH9/3/1BNz5q3Vu4HgfBSwWGt4E2NENj+XA+QAVmGA==";
+
+    private String cert = "-----BEGIN CERTIFICATE-----\n" +
+            "MIIDpzCCAo+gAwIBAgIBADANBgkqhkiG9w0BAQUFADBuMRowGAYDVQQDDBF3d3cu\n" +
+            "bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
+            "dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
+            "HhcNMTcwOTI2MTAwNDMwWhcNMzcwOTIxMTAwNDMwWjBuMRowGAYDVQQDDBF3d3cu\n" +
+            "bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
+            "dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
+            "ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDsn0JKS/THu328z1IgN0Vz\n" +
+            "YU53HjSX03WJIgWkmyTaxbiKpoJaKbksXmfSpgzVGzKFvGfZ03fwFrN7Q8P8R2e8\n" +
+            "SNiell7mh1TDw9/0P7Bt/ER8PJrXORo+GviKHxaLr7Y0BJX9i/nW/L0L/VaE8CZT\n" +
+            "AqYBdcSJGgHJjY4UMf892ZPTa9T2Dl3ggdMZ7BQ2kiCiCC3qV99b0igRJGmmLQaG\n" +
+            "iAflhFzuDQPMifUMq75wI8RSRPdxUAtjTfkl68QHu7Umyeyy33OQgdUKaTl5zcS3\n" +
+            "VSQbNjveVCNM4RDH1RlEc+7Wf1BY8APqT6jbiBcROJD2CeoLH2eiIJCi+61ZkSGf\n" +
+            "AgMBAAGjUDBOMB0GA1UdDgQWBBTFrXz2tk1HivD9rQ75qeoyHrAgIjAfBgNVHSME\n" +
+            "GDAWgBTFrXz2tk1HivD9rQ75qeoyHrAgIjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\n" +
+            "DQEBBQUAA4IBAQARQTX21QKO77gAzBszFJ6xVnjfa23YZF26Z4X1KaM8uV8TGzuN\n" +
+            "JA95XmReeP2iO3r8EWXS9djVCD64m2xx6FOsrUI8HZaw1JErU8mmOaLAe8q9RsOm\n" +
+            "9Eq37e4vFp2YUEInYUqs87ByUcA4/8g3lEYeIUnRsRsWsA45S3wD7wy07t+KAn7j\n" +
+            "yMmfxdma6hFfG9iN/egN6QXUAyIPXvUvlUuZ7/BhWBj/3sHMrF9quy9Q2DOI8F3t\n" +
+            "1wdQrkq4BtStKhciY5AIXz9SqsctFHTv4Lwgtkapoel4izJnO0ZqYTXVe7THwri9\n" +
+            "H/gua6uJDWH9jk2/CiZDWfsyFuNUuXvDSp05\n" +
+            "-----END CERTIFICATE-----";
+
+    @Test
+    public void encryptStringAsymmetric() throws Exception {
+        byte[] key1 = EncryptionUtils.generateKey();
+        String base64encodedKey = EncryptionUtils.encodeBytesToBase64String(key1);
+
+        String encryptedString = EncryptionUtils.encryptStringAsymmetric(base64encodedKey, cert);
+        String decryptedString = EncryptionUtils.decryptStringAsymmetric(encryptedString, privateKey);
+
+        byte[] key2 = EncryptionUtils.decodeStringToBase64Bytes(decryptedString);
+
+        assertTrue(Arrays.equals(key1, key2));
+    }
+
+    @Test
+    public void encryptStringSymmetric() throws Exception {
+        byte[] key = EncryptionUtils.generateKey();
+
+        String encryptedString = EncryptionUtils.encryptStringSymmetric(privateKey, key);
+        String decryptedString = EncryptionUtils.decryptStringSymmetric(encryptedString, key);
+
+        assertEquals(privateKey, decryptedString);
+    }
+
+    @Test
+    public void encryptPrivateKey() throws Exception {
+        String keyPhrase = "moreovertelevisionfactorytendencyindependenceinternationalintellectualimpress" +
+                "interestvolunteer";
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(4096, new SecureRandom());
+        KeyPair keyPair = keyGen.generateKeyPair();
+        PrivateKey privateKey = keyPair.getPrivate();
+        byte[] privateKeyBytes = privateKey.getEncoded();
+        String privateKeyString = EncryptionUtils.encodeBytesToBase64String(privateKeyBytes);
+
+        String encryptedString = EncryptionUtils.encryptPrivateKey(privateKeyString, keyPhrase);
+        String decryptedString = EncryptionUtils.decryptPrivateKey(encryptedString, keyPhrase);
+
+        assertEquals(privateKeyString, decryptedString);
+    }
+
+    @Test
+    public void generateCSR() throws Exception {
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048, new SecureRandom());
+        KeyPair keyPair = keyGen.generateKeyPair();
+
+        assertFalse(CsrHelper.generateCsrPemEncodedString(keyPair, "").isEmpty());
+        assertFalse(EncryptionUtils.encodeBytesToBase64String(keyPair.getPublic().getEncoded()).isEmpty());
+    }
+
+    /**
+     * DecryptedFolderMetadata -> EncryptedFolderMetadata -> JSON -> encrypt
+     * -> decrypt -> JSON -> EncryptedFolderMetadata -> DecryptedFolderMetadata
+     */
+    @Test
+    public void encryptionMetadata() throws Exception {
+        DecryptedFolderMetadata decryptedFolderMetadata1 = generateFolderMetadata();
+
+        // encrypt
+        EncryptedFolderMetadata encryptedFolderMetadata1 = EncryptionUtils.encryptFolderMetadata(
+                decryptedFolderMetadata1, privateKey);
+
+        // serialize
+        String encryptedJson = EncryptionUtils.serializeJSON(encryptedFolderMetadata1);
+
+        // de-serialize
+        EncryptedFolderMetadata encryptedFolderMetadata2 = EncryptionUtils.deserializeJSON(encryptedJson,
+                new TypeToken<EncryptedFolderMetadata>() {
+                });
+
+        // decrypt
+        DecryptedFolderMetadata decryptedFolderMetadata2 = EncryptionUtils.decryptFolderMetaData(
+                encryptedFolderMetadata2, privateKey);
+
+        // compare
+        assertTrue(compareJsonStrings(EncryptionUtils.serializeJSON(decryptedFolderMetadata1),
+                EncryptionUtils.serializeJSON(decryptedFolderMetadata2)));
+    }
+
+    @Test
+    public void testCryptFileWithoutMetadata() throws Exception {
+        byte[] key = EncryptionUtils.decodeStringToBase64Bytes("WANM0gRv+DhaexIsI0T3Lg==");
+        byte[] iv = EncryptionUtils.decodeStringToBase64Bytes("gKm3n+mJzeY26q4OfuZEqg==");
+        byte[] authTag = EncryptionUtils.decodeStringToBase64Bytes("PboI9tqHHX3QeAA22PIu4w==");
+
+        assertTrue(cryptFile("ia7OEEEyXMoRa1QWQk8r", "78f42172166f9dc8fd1a7156b1753353", key, iv, authTag));
+    }
+
+    @Test
+    public void cryptFileWithMetadata() throws Exception {
+        DecryptedFolderMetadata metadata = generateFolderMetadata();
+
+        // n9WXAIXO2wRY4R8nXwmo
+        assertTrue(cryptFile("ia7OEEEyXMoRa1QWQk8r",
+                "78f42172166f9dc8fd1a7156b1753353",
+                EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles().get("ia7OEEEyXMoRa1QWQk8r")
+                        .getEncrypted().getKey()),
+                EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles().get("ia7OEEEyXMoRa1QWQk8r")
+                        .getInitializationVector()),
+                EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles().get("ia7OEEEyXMoRa1QWQk8r")
+                        .getAuthenticationTag())));
+
+        // n9WXAIXO2wRY4R8nXwmo
+        assertTrue(cryptFile("n9WXAIXO2wRY4R8nXwmo",
+                "825143ed1f21ebb0c3b3c3f005b2f5db",
+                EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles().get("n9WXAIXO2wRY4R8nXwmo")
+                        .getEncrypted().getKey()),
+                EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles().get("n9WXAIXO2wRY4R8nXwmo")
+                        .getInitializationVector()),
+                EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles().get("n9WXAIXO2wRY4R8nXwmo")
+                        .getAuthenticationTag())));
+    }
+
+    /**
+     * generates new keys and tests if they are unique
+     */
+    @Test
+    public void testKey() {
+        Set<String> keys = new HashSet<>();
+
+        for (int i = 0; i < 50; i++) {
+            assertTrue(keys.add(EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey())));
+        }
+    }
+
+    /**
+     * generates new ivs and tests if they are unique
+     */
+    @Test
+    public void testIV() {
+        Set<String> ivs = new HashSet<>();
+
+        for (int i = 0; i < 50; i++) {
+            assertTrue(ivs.add(EncryptionUtils.encodeBytesToBase64String(
+                    EncryptionUtils.randomBytes(EncryptionUtils.ivLength))));
+        }
+    }
+
+    /**
+     * generates new salt and tests if they are unique
+     */
+    @Test
+    public void testSalt() {
+        Set<String> ivs = new HashSet<>();
+
+        for (int i = 0; i < 50; i++) {
+            assertTrue(ivs.add(EncryptionUtils.encodeBytesToBase64String(
+                    EncryptionUtils.randomBytes(EncryptionUtils.saltLength))));
+        }
+    }
+
+
+    // Helper
+    private boolean compareJsonStrings(String expected, String actual) {
+        JsonParser parser = new JsonParser();
+        JsonElement o1 = parser.parse(expected);
+        JsonElement o2 = parser.parse(actual);
+
+        if (o1.equals(o2)) {
+            return true;
+        } else {
+            System.out.println("expected: " + o1);
+            System.out.println("actual: " + o2);
+            return false;
+        }
+    }
+
+    private DecryptedFolderMetadata generateFolderMetadata() throws Exception {
+        String metadataKey0 = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
+        String metadataKey1 = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
+        String metadataKey2 = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
+        HashMap<Integer, String> metadataKeys = new HashMap<>();
+        metadataKeys.put(0, EncryptionUtils.encryptStringAsymmetric(metadataKey0, cert));
+        metadataKeys.put(1, EncryptionUtils.encryptStringAsymmetric(metadataKey1, cert));
+        metadataKeys.put(2, EncryptionUtils.encryptStringAsymmetric(metadataKey2, cert));
+        DecryptedFolderMetadata.Encrypted encrypted = new DecryptedFolderMetadata.Encrypted();
+        encrypted.setMetadataKeys(metadataKeys);
+
+        DecryptedFolderMetadata.Metadata metadata1 = new DecryptedFolderMetadata.Metadata();
+        metadata1.setMetadataKeys(metadataKeys);
+        metadata1.setVersion(1);
+
+        DecryptedFolderMetadata.Sharing sharing = new DecryptedFolderMetadata.Sharing();
+        sharing.setSignature("HMACOFRECIPIENTANDNEWESTMETADATAKEY");
+        HashMap<String, String> recipient = new HashMap<>();
+        recipient.put("blah@schiessle.org", "PUBLIC KEY");
+        recipient.put("bjoern@schiessle.org", "PUBLIC KEY");
+        sharing.setRecipient(recipient);
+        metadata1.setSharing(sharing);
+
+        HashMap<String, DecryptedFolderMetadata.DecryptedFile> files = new HashMap<>();
+
+        DecryptedFolderMetadata.Data data1 = new DecryptedFolderMetadata.Data();
+        data1.setKey("WANM0gRv+DhaexIsI0T3Lg==");
+        data1.setFilename("test.txt");
+        data1.setVersion(1);
+
+        DecryptedFolderMetadata.DecryptedFile file1 = new DecryptedFolderMetadata.DecryptedFile();
+        file1.setInitializationVector("gKm3n+mJzeY26q4OfuZEqg==");
+        file1.setEncrypted(data1);
+        file1.setMetadataKey(0);
+        file1.setAuthenticationTag("PboI9tqHHX3QeAA22PIu4w==");
+
+        files.put("ia7OEEEyXMoRa1QWQk8r", file1);
+
+        DecryptedFolderMetadata.Data data2 = new DecryptedFolderMetadata.Data();
+        data2.setKey("9dfzbIYDt28zTyZfbcll+g==");
+        data2.setFilename("test2.txt");
+        data2.setVersion(1);
+
+        DecryptedFolderMetadata.DecryptedFile file2 = new DecryptedFolderMetadata.DecryptedFile();
+        file2.setInitializationVector("hnJLF8uhDvDoFK4ajuvwrg==");
+        file2.setEncrypted(data2);
+        file2.setMetadataKey(0);
+        file2.setAuthenticationTag("qOQZdu5soFO77Y7y4rAOVA==");
+
+        files.put("n9WXAIXO2wRY4R8nXwmo", file2);
+
+        return new DecryptedFolderMetadata(metadata1, files);
+    }
+
+    private boolean cryptFile(String fileName, String md5, byte[] key, byte[] iv, byte[] expectedAuthTag)
+            throws Exception {
+        File file = getFile(fileName);
+        assertEquals(md5, EncryptionUtils.getMD5Sum(file));
+
+        EncryptionUtils.EncryptedFile encryptedFile = EncryptionUtils.encryptFile(file, key, iv);
+
+        File encryptedTempFile = File.createTempFile("file", "tmp");
+        FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
+        fileOutputStream.write(encryptedFile.encryptedBytes);
+        fileOutputStream.close();
+
+        byte[] authenticationTag = EncryptionUtils.decodeStringToBase64Bytes(encryptedFile.authenticationTag);
+
+        // verify authentication tag
+        assertTrue(Arrays.equals(expectedAuthTag, authenticationTag));
+
+        byte[] decryptedBytes = EncryptionUtils.decryptFile(encryptedTempFile, key, iv, authenticationTag);
+
+        File decryptedFile = File.createTempFile("file", "dec");
+        FileOutputStream fileOutputStream1 = new FileOutputStream(decryptedFile);
+        fileOutputStream1.write(decryptedBytes);
+        fileOutputStream1.close();
+
+        return md5.compareTo(EncryptionUtils.getMD5Sum(decryptedFile)) == 0;
+    }
+
+    private File getFile(String filename) throws IOException {
+        InputStream inputStream = getInstrumentation().getContext().getAssets().open(filename);
+        File temp = File.createTempFile("file", "file");
+        FileUtils.copyInputStreamToFile(inputStream, temp);
+
+        return temp;
+    }
+}

+ 16 - 7
src/main/java/com/owncloud/android/MainApp.java

@@ -35,6 +35,7 @@ import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.StrictMode;
+import android.support.annotation.StringRes;
 import android.support.multidex.MultiDexApplication;
 import android.support.v4.util.Pair;
 import android.support.v7.app.AlertDialog;
@@ -120,6 +121,7 @@ public class MainApp extends MultiDexApplication {
         boolean isSamlAuth = AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso));
 
         OwnCloudClientManagerFactory.setUserAgent(getUserAgent());
+        OwnCloudClientManagerFactory.setNextcloudUserAgent(getNextcloudUserAgent());
         if (isSamlAuth) {
             OwnCloudClientManagerFactory.setDefaultPolicy(Policy.SINGLE_SESSION_PER_ACCOUNT);
         } else {
@@ -328,7 +330,7 @@ public class MainApp extends MultiDexApplication {
         }
     }
 
-    //  From AccountAuthenticator 
+    //  From AccountAuthenticator
     //  public static final String AUTHORITY = "org.owncloud";
     public static String getAuthority() {
         return getAppContext().getResources().getString(R.string.authority);
@@ -372,9 +374,19 @@ public class MainApp extends MultiDexApplication {
         return mOnlyOnDevice;
     }
 
-    // user agent
     public static String getUserAgent() {
-        String appString = getAppContext().getResources().getString(R.string.user_agent);
+        // Mozilla/5.0 (Android) ownCloud-android/1.7.0
+        return getUserAgent(R.string.user_agent);
+    }
+
+    public static String getNextcloudUserAgent() {
+        // Mozilla/5.0 (Android) Nextcloud-android/2.1.0
+        return getUserAgent(R.string.nextcloud_user_agent);
+    }
+
+    // user agent
+    private static String getUserAgent(@StringRes int agent) {
+        String appString = getAppContext().getResources().getString(agent);
         String packageName = getAppContext().getPackageName();
         String version = "";
 
@@ -388,10 +400,7 @@ public class MainApp extends MultiDexApplication {
             Log_OC.e(TAG, "Trying to get packageName", e.getCause());
         }
 
-        // Mozilla/5.0 (Android) ownCloud-android/1.7.0
-        String userAgent = String.format(appString, version);
-
-        return userAgent;
+        return String.format(appString, version);
     }
 
     private static void updateToAutoUpload() {

+ 205 - 0
src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadata.java

@@ -0,0 +1,205 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2017 Tobias Kaminsky
+ * Copyright (C) 2017 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datamodel;
+
+import java.util.HashMap;
+
+/**
+ * Decrypted class representation of metadata json of folder metadata
+ */
+
+public class DecryptedFolderMetadata {
+    private Metadata metadata;
+    private HashMap<String, DecryptedFile> files;
+
+    public DecryptedFolderMetadata() {
+        this.metadata = new Metadata();
+        this.files = new HashMap<>();
+    }
+
+    public DecryptedFolderMetadata(Metadata metadata, HashMap<String, DecryptedFile> files) {
+        this.metadata = metadata;
+        this.files = files;
+    }
+
+    public Metadata getMetadata() {
+        return metadata;
+    }
+
+    public void setMetadata(Metadata metadata) {
+        this.metadata = metadata;
+    }
+
+    public HashMap<String, DecryptedFile> getFiles() {
+        return files;
+    }
+
+    public void setFiles(HashMap<String, DecryptedFile> files) {
+        this.files = files;
+    }
+
+    public static class Metadata {
+        private HashMap<Integer, String> metadataKeys; // each keys is encrypted on its own, decrypt on use
+        private Sharing sharing;
+        private int version;
+
+        public HashMap<Integer, String> getMetadataKeys() {
+            return metadataKeys;
+        }
+
+        public void setMetadataKeys(HashMap<Integer, String> metadataKeys) {
+            this.metadataKeys = metadataKeys;
+        }
+
+        public Sharing getSharing() {
+            return sharing;
+        }
+
+        public void setSharing(Sharing sharing) {
+            this.sharing = sharing;
+        }
+
+        public int getVersion() {
+            return version;
+        }
+
+        public void setVersion(int version) {
+            this.version = version;
+        }
+
+        @Override
+        public String toString() {
+            return String.valueOf(version);
+        }
+    }
+
+    public static class Encrypted {
+        private HashMap<Integer, String> metadataKeys;
+
+        public HashMap<Integer, String> getMetadataKeys() {
+            return metadataKeys;
+        }
+
+        public void setMetadataKeys(HashMap<Integer, String> metadataKeys) {
+            this.metadataKeys = metadataKeys;
+        }
+    }
+
+    public static class Sharing {
+        private HashMap<String, String> recipient;
+        private String signature;
+
+        public HashMap<String, String> getRecipient() {
+            return recipient;
+        }
+
+        public void setRecipient(HashMap<String, String> recipient) {
+            this.recipient = recipient;
+        }
+
+        public String getSignature() {
+            return signature;
+        }
+
+        public void setSignature(String signature) {
+            this.signature = signature;
+        }
+    }
+
+    public static class DecryptedFile {
+        private Data encrypted;
+        private String initializationVector;
+        private String authenticationTag;
+        private int metadataKey;
+
+        public Data getEncrypted() {
+            return encrypted;
+        }
+
+        public void setEncrypted(Data encrypted) {
+            this.encrypted = encrypted;
+        }
+
+        public String getInitializationVector() {
+            return initializationVector;
+        }
+
+        public void setInitializationVector(String initializationVector) {
+            this.initializationVector = initializationVector;
+        }
+
+        public String getAuthenticationTag() {
+            return authenticationTag;
+        }
+
+        public void setAuthenticationTag(String authenticationTag) {
+            this.authenticationTag = authenticationTag;
+        }
+
+        public int getMetadataKey() {
+            return metadataKey;
+        }
+
+        public void setMetadataKey(int metadataKey) {
+            this.metadataKey = metadataKey;
+        }
+    }
+
+    public static class Data {
+        private String key;
+        private String filename;
+        private String mimetype;
+        private int version;
+
+        public String getKey() {
+            return key;
+        }
+
+        public void setKey(String key) {
+            this.key = key;
+        }
+
+        public String getFilename() {
+            return filename;
+        }
+
+        public void setFilename(String filename) {
+            this.filename = filename;
+        }
+
+        public String getMimetype() {
+            return mimetype;
+        }
+
+        public void setMimetype(String mimetype) {
+            this.mimetype = mimetype;
+        }
+
+        public int getVersion() {
+            return version;
+        }
+
+        public void setVersion(int version) {
+            this.version = version;
+        }
+    }
+}

+ 94 - 0
src/main/java/com/owncloud/android/datamodel/EncryptedFolderMetadata.java

@@ -0,0 +1,94 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2017 Tobias Kaminsky
+ * Copyright (C) 2017 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datamodel;
+
+import java.util.HashMap;
+
+/**
+ * Encrypted class representation of metadata json of folder metadata
+ */
+
+public class EncryptedFolderMetadata {
+    private DecryptedFolderMetadata.Metadata metadata;
+    private HashMap<String, EncryptedFile> files;
+    
+    public EncryptedFolderMetadata(DecryptedFolderMetadata.Metadata metadata, HashMap<String, EncryptedFile> files) {
+        this.metadata = metadata;
+        this.files = files;
+    }
+
+    public DecryptedFolderMetadata.Metadata getMetadata() {
+        return metadata;
+    }
+
+    public void setMetadata(DecryptedFolderMetadata.Metadata metadata) {
+        this.metadata = metadata;
+    }
+
+    public HashMap<String, EncryptedFile> getFiles() {
+        return files;
+    }
+
+    public void setFiles(HashMap<String, EncryptedFile> files) {
+        this.files = files;
+    }
+
+    public static class EncryptedFile {
+        private String encrypted;
+        private String initializationVector;
+        private String authenticationTag;
+        private int metadataKey;
+
+        public String getEncrypted() {
+            return encrypted;
+        }
+
+        public void setEncrypted(String encrypted) {
+            this.encrypted = encrypted;
+        }
+
+        public String getInitializationVector() {
+            return initializationVector;
+        }
+
+        public void setInitializationVector(String initializationVector) {
+            this.initializationVector = initializationVector;
+        }
+
+        public String getAuthenticationTag() {
+            return authenticationTag;
+        }
+
+        public void setAuthenticationTag(String authenticationTag) {
+            this.authenticationTag = authenticationTag;
+        }
+
+        public int getMetadataKey() {
+            return metadataKey;
+        }
+
+        public void setMetadataKey(int metadataKey) {
+            this.metadataKey = metadataKey;
+        }
+    }
+}
+

+ 10 - 1
src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -187,6 +187,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, file.getFileLength());
         cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimetype());
         cv.put(ProviderTableMeta.FILE_NAME, file.getFileName());
+        cv.put(ProviderTableMeta.FILE_ENCRYPTED_NAME, file.getEncryptedFileName());
         cv.put(ProviderTableMeta.FILE_PARENT, file.getParentId());
         cv.put(ProviderTableMeta.FILE_PATH, file.getRemotePath());
         if (!file.isFolder()) {
@@ -451,6 +452,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_PERMISSIONS, folder.getPermissions());
         cv.put(ProviderTableMeta.FILE_REMOTE_ID, folder.getRemoteId());
         cv.put(ProviderTableMeta.FILE_FAVORITE, folder.getIsFavorite());
+        cv.put(ProviderTableMeta.FILE_IS_ENCRYPTED, folder.isEncrypted());
         return cv;
     }
 
@@ -465,6 +467,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, file.getFileLength());
         cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, file.getMimetype());
         cv.put(ProviderTableMeta.FILE_NAME, file.getFileName());
+        cv.put(ProviderTableMeta.FILE_ENCRYPTED_NAME, file.getEncryptedFileName());
         //cv.put(ProviderTableMeta.FILE_PARENT, file.getParentId());
         cv.put(ProviderTableMeta.FILE_PARENT, folder.getFileId());
         cv.put(ProviderTableMeta.FILE_PATH, file.getRemotePath());
@@ -485,6 +488,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.FILE_IS_DOWNLOADING, file.isDownloading());
         cv.put(ProviderTableMeta.FILE_ETAG_IN_CONFLICT, file.getEtagInConflict());
         cv.put(ProviderTableMeta.FILE_FAVORITE, file.getIsFavorite());
+        cv.put(ProviderTableMeta.FILE_IS_ENCRYPTED, file.isEncrypted());
         return cv;
     }
 
@@ -937,8 +941,10 @@ public class FileDataStorageManager {
         OCFile file = null;
         if (c != null) {
             file = new OCFile(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PATH)));
+            file.setFileName(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_NAME)));
             file.setFileId(c.getLong(c.getColumnIndex(ProviderTableMeta._ID)));
             file.setParentId(c.getLong(c.getColumnIndex(ProviderTableMeta.FILE_PARENT)));
+            file.setEncryptedFileName(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_ENCRYPTED_NAME)));
             file.setMimetype(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_CONTENT_TYPE)));
             file.setStoragePath(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH)));
             if (file.getStoragePath() == null) {
@@ -974,7 +980,7 @@ public class FileDataStorageManager {
                     c.getColumnIndex(ProviderTableMeta.FILE_IS_DOWNLOADING)) == 1);
             file.setEtagInConflict(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_ETAG_IN_CONFLICT)));
             file.setFavorite(c.getInt(c.getColumnIndex(ProviderTableMeta.FILE_FAVORITE)) == 1);
-
+            file.setEncrypted(c.getInt(c.getColumnIndex(ProviderTableMeta.FILE_IS_ENCRYPTED)) == 1);
         }
         return file;
     }
@@ -1930,6 +1936,7 @@ public class FileDataStorageManager {
         cv.put(ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR, capability.getServerElementColor());
         cv.put(ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_URL, capability.getServerBackground());
         cv.put(ProviderTableMeta.CAPABILITIES_SERVER_SLOGAN, capability.getServerSlogan());
+        cv.put(ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION, capability.getEndToEndEncryption().getValue());
 
         if (capabilityExists(mAccount.name)) {
             if (getContentResolver() != null) {
@@ -2078,6 +2085,8 @@ public class FileDataStorageManager {
             capability.setServerBackground(c.getString(c.getColumnIndex(
                     ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_URL)));
             capability.setServerSlogan(c.getString(c.getColumnIndex(ProviderTableMeta.CAPABILITIES_SERVER_SLOGAN)));
+            capability.setEndToEndEncryption(CapabilityBooleanType.fromValue(c.getInt(c
+                    .getColumnIndex(ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION))));
         }
         return capability;
     }

+ 66 - 4
src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -93,6 +93,8 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
 
     private boolean mIsFavorite;
 
+    private boolean mIsEncrypted;
+
     /**
      * URI to the local path of the file contents, if stored in the device; cached after first call
      * to {@link #getStorageUri()}
@@ -106,6 +108,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
      * Cached after first call, until changed.
      */
     private Uri mExposedFileUri;
+    private String mEncryptedFileName;
 
 
     /**
@@ -153,6 +156,8 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         mEtagInConflict = source.readString();
         mShareWithSharee = source.readInt() == 1;
         mIsFavorite = source.readInt() == 1;
+        mIsEncrypted = source.readInt() == 1;
+        mEncryptedFileName = source.readString();
     }
 
     @Override
@@ -180,6 +185,8 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         dest.writeString(mEtagInConflict);
         dest.writeInt(mShareWithSharee ? 1 : 0);
         dest.writeInt(mIsFavorite ? 1 : 0);
+        dest.writeInt(mIsEncrypted ? 1 : 0);
+        dest.writeString(mEncryptedFileName);
     }
 
     public boolean getIsFavorite() {
@@ -190,22 +197,55 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         this.mIsFavorite = mIsFavorite;
     }
 
+    public boolean isEncrypted() {
+        return mIsEncrypted;
+    }
+
+    public void setEncrypted(boolean mIsEncrypted) {
+        this.mIsEncrypted = mIsEncrypted;
+    }
     /**
-     * Gets the ID of the file
+     * Gets the android internal ID of the file
      *
-     * @return the file ID
+     * @return the android internal file ID
      */
     public long getFileId() {
         return mId;
     }
 
+    public String getDecryptedRemotePath() {
+        return mRemotePath;
+    }
+
     /**
      * Returns the remote path of the file on ownCloud
      *
      * @return The remote path to the file
      */
     public String getRemotePath() {
-        return mRemotePath;
+        if (isEncrypted() && !isFolder()) {
+            String parentPath = new File(mRemotePath).getParent();
+
+            if (parentPath.endsWith("/")) {
+                return parentPath + getEncryptedFileName();
+            } else {
+                return parentPath + "/" + getEncryptedFileName();
+            }
+        } else {
+            if (isFolder()) {
+                if (mRemotePath.endsWith("/")) {
+                    return mRemotePath;
+                } else {
+                    return mRemotePath + "/";
+                }
+            } else {
+                return mRemotePath;
+            }
+        }
+    }
+
+    public void setRemotePath(String path) {
+        mRemotePath = path;
     }
 
     /**
@@ -389,7 +429,7 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
      * @return The name of the file
      */
     public String getFileName() {
-        File f = new File(getRemotePath());
+        File f = new File(mRemotePath);
         return f.getName().length() == 0 ? ROOT_PATH : f.getName();
     }
 
@@ -413,6 +453,14 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         }
     }
 
+    public void setEncryptedFileName(String name) {
+        mEncryptedFileName = name;
+    }
+
+    public String getEncryptedFileName() {
+        return mEncryptedFileName;
+    }
+
     /**
      * Can be used to get the Mimetype
      *
@@ -652,10 +700,24 @@ public class OCFile implements Parcelable, Comparable<OCFile> {
         this.mPermissions = permissions;
     }
 
+    /**
+     * The fileid namespaced by the instance id, globally unique
+     *
+     * @return globally unique file id: file id + instance id
+     */
     public String getRemoteId() {
         return mRemoteId;
     }
 
+    /**
+     * The unique id for the file within the instance
+     *
+     * @return file id, unique within the instance
+     */
+    public String getLocalId() {
+        return getRemoteId().substring(0, 8).replaceAll("^0*", "");
+    }
+
     public void setRemoteId(String remoteId) {
         this.mRemoteId = remoteId;
     }

+ 3 - 1
src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java

@@ -123,7 +123,7 @@ public class UploadsStorageManager extends Observable {
         cv.put(ProviderTableMeta.UPLOADS_CREATED_BY, ocUpload.getCreadtedBy());
         cv.put(ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY, ocUpload.isWhileChargingOnly() ? 1 : 0);
         cv.put(ProviderTableMeta.UPLOADS_IS_WIFI_ONLY, ocUpload.isUseWifiOnly() ? 1 : 0);
-
+        cv.put(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN, ocUpload.getFolderUnlockToken());
         Uri result = getDB().insert(ProviderTableMeta.CONTENT_URI_UPLOADS, cv);
 
         Log_OC.d(TAG, "storeUpload returns with: " + result + " for file: " + ocUpload.getLocalPath());
@@ -155,6 +155,7 @@ public class UploadsStorageManager extends Observable {
         cv.put(ProviderTableMeta.UPLOADS_LAST_RESULT, ocUpload.getLastResult().getValue());
         cv.put(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP, ocUpload.getUploadEndTimestamp());
         cv.put(ProviderTableMeta.UPLOADS_FILE_SIZE, ocUpload.getFileSize());
+        cv.put(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN, ocUpload.getFolderUnlockToken());
 
         int result = getDB().update(ProviderTableMeta.CONTENT_URI_UPLOADS,
                 cv,
@@ -379,6 +380,7 @@ public class UploadsStorageManager extends Observable {
             upload.setUseWifiOnly(c.getInt(c.getColumnIndex(ProviderTableMeta.UPLOADS_IS_WIFI_ONLY)) == 1);
             upload.setWhileChargingOnly(c.getInt(c.getColumnIndex(ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY))
                     == 1);
+            upload.setFolderUnlockToken(c.getString(c.getColumnIndex(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN)));
         }
         return upload;
     }

+ 17 - 1
src/main/java/com/owncloud/android/db/OCUpload.java

@@ -112,7 +112,12 @@ public class OCUpload implements Parcelable {
      */
     private boolean mIsWhileChargingOnly;
 
-     /**
+    /**
+     * Token to unlock E2E folder
+     */
+    private String mFolderUnlockToken;
+
+    /**
      * Main constructor.
      *
      * @param localPath         Absolute path in the local file system to the file to be uploaded.
@@ -162,6 +167,7 @@ public class OCUpload implements Parcelable {
         mCreatedBy = UploadFileOperation.CREATED_BY_USER;
         mIsUseWifiOnly = true;
         mIsWhileChargingOnly = false;
+        mFolderUnlockToken = "";
     }
 
     // Getters & Setters
@@ -372,6 +378,14 @@ public class OCUpload implements Parcelable {
         return mIsWhileChargingOnly;
     }
 
+    public void setFolderUnlockToken(String token) {
+        mFolderUnlockToken = token;
+    }
+
+    public String getFolderUnlockToken() {
+        return mFolderUnlockToken;
+    }
+
     /**
      * Reconstruct from parcel
      *
@@ -403,6 +417,7 @@ public class OCUpload implements Parcelable {
         mCreatedBy = source.readInt();
         mIsUseWifiOnly = (source.readInt() == 1);
         mIsWhileChargingOnly = (source.readInt() == 1);
+        mFolderUnlockToken = source.readString();
     }
 
     @Override
@@ -425,6 +440,7 @@ public class OCUpload implements Parcelable {
         dest.writeInt(mCreatedBy);
         dest.writeInt(mIsUseWifiOnly ? 1 : 0);
         dest.writeInt(mIsWhileChargingOnly ? 1 : 0);
+        dest.writeString(mFolderUnlockToken);
     }
 
     enum CanUploadFileNowStatus {NOW, LATER, FILE_GONE, ERROR}

+ 7 - 2
src/main/java/com/owncloud/android/db/ProviderMeta.java

@@ -32,7 +32,7 @@ import com.owncloud.android.MainApp;
 public class ProviderMeta {
 
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 26;
+    public static final int DB_VERSION = 27;
 
     private ProviderMeta() {
     }
@@ -79,6 +79,7 @@ public class ProviderMeta {
         // Columns of filelist table
         public static final String FILE_PARENT = "parent";
         public static final String FILE_NAME = "filename";
+        public static final String FILE_ENCRYPTED_NAME = "encrypted_filename";
         public static final String FILE_CREATION = "created";
         public static final String FILE_MODIFIED = "modified";
         public static final String FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA = "modified_at_last_sync_for_data";
@@ -100,8 +101,10 @@ public class ProviderMeta {
         public static final String FILE_IS_DOWNLOADING = "is_downloading";
         public static final String FILE_ETAG_IN_CONFLICT = "etag_in_conflict";
         public static final String FILE_FAVORITE = "favorite";
+        public static final String FILE_IS_ENCRYPTED = "is_encrypted";
 
-        public static final String[] FILE_ALL_COLUMNS = {_ID, FILE_PARENT, FILE_NAME, FILE_CREATION, FILE_MODIFIED,
+        public static final String [] FILE_ALL_COLUMNS = {_ID, FILE_PARENT, FILE_NAME
+               , FILE_CREATION, FILE_MODIFIED,
                 FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, FILE_CONTENT_LENGTH, FILE_CONTENT_TYPE, FILE_STORAGE_PATH,
                 FILE_PATH, FILE_ACCOUNT_OWNER, FILE_LAST_SYNC_DATE, FILE_LAST_SYNC_DATE_FOR_DATA, FILE_KEEP_IN_SYNC,
                 FILE_ETAG, FILE_SHARED_VIA_LINK, FILE_SHARED_WITH_SHAREE, FILE_PUBLIC_LINK, FILE_PERMISSIONS,
@@ -162,6 +165,7 @@ public class ProviderMeta {
         public static final String CAPABILITIES_SERVER_ELEMENT_COLOR = "server_element_color";
         public static final String CAPABILITIES_SERVER_BACKGROUND_URL = "background_url";
         public static final String CAPABILITIES_SERVER_SLOGAN = "server_slogan";
+        public static final String CAPABILITIES_END_TO_END_ENCRYPTION = "end_to_end_encryption";
 
         public static final String CAPABILITIES_DEFAULT_SORT_ORDER = CAPABILITIES_ACCOUNT_NAME
                 + " collate nocase asc";
@@ -182,6 +186,7 @@ public class ProviderMeta {
         public static final String UPLOADS_DEFAULT_SORT_ORDER = ProviderTableMeta._ID + " collate nocase desc";
         public static final String UPLOADS_IS_WHILE_CHARGING_ONLY = "is_while_charging_only";
         public static final String UPLOADS_IS_WIFI_ONLY = "is_wifi_only";
+        public static final String UPLOADS_FOLDER_UNLOCK_TOKEN = "folder_unlock_token";
 
         // Columns of synced folder table
         public static final String SYNCED_FOLDER_LOCAL_PATH = "local_path";

+ 50 - 6
src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -156,7 +156,7 @@ public class FileMenuFilter {
         }
 
         // RENAME
-        if (!isSingleSelection() || synchronizing) {
+        if (!isSingleSelection() || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) {
             toHide.add(R.id.action_rename_file);
 
         } else {
@@ -164,7 +164,7 @@ public class FileMenuFilter {
         }
 
         // MOVE & COPY
-        if (mFiles.isEmpty() || synchronizing) {
+        if (mFiles.isEmpty() || synchronizing || containsEncryptedFile() || containsEncryptedFolder()) {
             toHide.add(R.id.action_move);
             toHide.add(R.id.action_copy);
         } else {
@@ -173,9 +173,8 @@ public class FileMenuFilter {
         }
 
         // REMOVE
-        if (mFiles.isEmpty() || synchronizing) {
+        if (mFiles.isEmpty() || synchronizing || containsEncryptedFolder()) {
             toHide.add(R.id.action_remove_file);
-
         } else {
             toShow.add(R.id.action_remove_file);
         }
@@ -240,8 +239,9 @@ public class FileMenuFilter {
                 (capability.getFilesSharingApiEnabled().isTrue() ||
                         capability.getFilesSharingApiEnabled().isUnknown()
                 );
-        if ((!shareViaLinkAllowed && !shareWithUsersAllowed) ||
-                !isSingleSelection() || !shareApiEnabled || mOverflowMenu) {
+        if (containsEncryptedFile() || (!shareViaLinkAllowed && !shareWithUsersAllowed) ||
+                !isSingleSelection() ||
+                !shareApiEnabled || mOverflowMenu) {
             toHide.add(R.id.action_send_share_file);
         } else {
             toShow.add(R.id.action_send_share_file);
@@ -282,6 +282,22 @@ public class FileMenuFilter {
             toShow.add(R.id.action_unset_favorite);
         }
 
+        // Encryption
+        boolean endToEndEncryptionEnabled = capability != null && capability.getEndToEndEncryption().isTrue();
+        if (mFiles.isEmpty() || !isSingleSelection() || isSingleFile() || isEncryptedFolder()
+                || !endToEndEncryptionEnabled) {
+            toHide.add(R.id.action_encrypted);
+        } else {
+            toShow.add(R.id.action_encrypted);
+        }
+
+        // Un-encrypt
+        if (mFiles.isEmpty() || !isSingleSelection() || isSingleFile() || !isEncryptedFolder()
+                || !endToEndEncryptionEnabled) {
+            toHide.add(R.id.action_unset_encrypted);
+        } else {
+            toShow.add(R.id.action_unset_encrypted);
+        }
 
         // SET PICTURE AS
         if (isSingleImage() && !MimeTypeUtil.isSVG(mFiles.iterator().next())) {
@@ -344,6 +360,16 @@ public class FileMenuFilter {
         return isSingleSelection() && !mFiles.iterator().next().isFolder();
     }
 
+    private boolean isEncryptedFolder() {
+        if (isSingleSelection()) {
+            OCFile file = mFiles.iterator().next();
+
+            return file.isFolder() && file.isEncrypted();
+        } else {
+            return false;
+        }
+    }
+
     private boolean isSingleImage() {
         return isSingleSelection() && MimeTypeUtil.isImage(mFiles.iterator().next());
     }
@@ -352,6 +378,24 @@ public class FileMenuFilter {
         return mFiles != null && !containsFolder();
     }
 
+    private boolean containsEncryptedFile() {
+        for (OCFile file : mFiles) {
+            if (!file.isFolder() && file.isEncrypted()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean containsEncryptedFolder() {
+        for (OCFile file : mFiles) {
+            if (file.isFolder() && file.isEncrypted()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private boolean containsFolder() {
         for (OCFile file : mFiles) {
             if (file.isFolder()) {

+ 2 - 2
src/main/java/com/owncloud/android/files/services/FileDownloader.java

@@ -188,7 +188,7 @@ public class FileDownloader extends Service
             AbstractList<String> requestedDownloads = new Vector<String>();
             try {
                 DownloadFileOperation newDownload = new DownloadFileOperation(account, file, behaviour, activityName,
-                        packageName);
+                        packageName, getBaseContext());
                 newDownload.addDatatransferProgressListener(this);
                 newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder);
                 Pair<String, String> putResult = mPendingDownloads.putIfAbsent(
@@ -450,7 +450,7 @@ public class FileDownloader extends Service
 
 
                     /// perform the download
-                    downloadResult = mCurrentDownload.execute(mDownloadClient);
+                    downloadResult = mCurrentDownload.execute(mDownloadClient, mCurrentDownload.getFile().isEncrypted());
                     if (downloadResult.isSuccess()) {
                         saveDownloadedFile();
                     }

+ 19 - 10
src/main/java/com/owncloud/android/files/services/FileUploader.java

@@ -615,8 +615,8 @@ public class FileUploader extends Service
                     if (isCreateRemoteFolder) {
                         newUpload.setRemoteFolderToBeCreated();
                     }
-                    newUpload.addDatatransferProgressListener(this);
-                    newUpload.addDatatransferProgressListener((FileUploaderBinder) mBinder);
+                    newUpload.addDataTransferProgressListener(this);
+                    newUpload.addDataTransferProgressListener((FileUploaderBinder) mBinder);
 
                     newUpload.addRenameUploadListener(this);
 
@@ -671,8 +671,8 @@ public class FileUploader extends Service
                     whileChargingOnly
             );
 
-            newUpload.addDatatransferProgressListener(this);
-            newUpload.addDatatransferProgressListener((FileUploaderBinder) mBinder);
+            newUpload.addDataTransferProgressListener(this);
+            newUpload.addDataTransferProgressListener((FileUploaderBinder) mBinder);
 
             newUpload.addRenameUploadListener(this);
 
@@ -1055,8 +1055,19 @@ public class FileUploader extends Service
                 mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
                         getClientFor(ocAccount, this);
 
-                /// perform the upload
-                uploadResult = mCurrentUpload.execute(mUploadClient, mStorageManager);
+
+//                // If parent folder is encrypted, upload file encrypted
+//                OCFile parent = mStorageManager.getFileByPath(mCurrentUpload.getFile().getParentRemotePath());
+
+//                if (parent.isEncrypted()) {
+//                    UploadEncryptedFileOperation uploadEncryptedFileOperation =
+//                            new UploadEncryptedFileOperation(parent, mCurrentUpload);
+//
+//                    uploadResult = uploadEncryptedFileOperation.execute(mUploadClient, mStorageManager);
+//                } else {
+                    /// perform the regular upload
+                    uploadResult = mCurrentUpload.execute(mUploadClient, mStorageManager);
+//                }
 
 
             } catch (Exception e) {
@@ -1073,10 +1084,8 @@ public class FileUploader extends Service
                     // TODO: grant that name is also updated for mCurrentUpload.getOCUploadId
 
                 } else {
-                    removeResult = mPendingUploads.removePayload(
-                            mCurrentAccount.name,
-                            mCurrentUpload.getRemotePath()
-                    );
+                    removeResult = mPendingUploads.removePayload(mCurrentAccount.name,
+                            mCurrentUpload.getDecryptedRemotePath());
                 }
 
                 mUploadsStorageManager.updateDatabaseUploadResult(uploadResult, mCurrentUpload);

+ 9 - 1
src/main/java/com/owncloud/android/jobs/AccountRemovalJob.java

@@ -40,6 +40,7 @@ import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.ui.activity.ContactsPreferenceActivity;
 import com.owncloud.android.ui.events.AccountRemovedEvent;
+import com.owncloud.android.utils.EncryptionUtils;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.FilesSyncHelper;
 
@@ -72,7 +73,10 @@ public class AccountRemovalJob extends Job implements AccountManagerCallback<Boo
             // disable contact backup job
             ContactsPreferenceActivity.cancelContactBackupJobForAccount(context, account);
 
-            am.removeAccount(account, this, null);
+
+            if (am != null) {
+                am.removeAccount(account, this, null);
+            }
 
             FileDataStorageManager storageManager = new FileDataStorageManager(account, context.getContentResolver());
 
@@ -115,6 +119,10 @@ public class AccountRemovalJob extends Job implements AccountManagerCallback<Boo
                 filesystemDataProvider.deleteAllEntriesForSyncedFolder(Long.toString(syncedFolderId));
             }
 
+            // delete stored E2E keys 
+            arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PRIVATE_KEY);
+            arbitraryDataProvider.deleteKeyForAccount(account.name, EncryptionUtils.PUBLIC_KEY);
+
             return Result.SUCCESS;
         } else {
             return Result.FAILURE;

+ 12 - 4
src/main/java/com/owncloud/android/operations/CreateFolderOperation.java

@@ -1,4 +1,4 @@
-/**
+/*
  *   ownCloud Android client application
  *
  *   @author David A. Velasco
@@ -28,6 +28,8 @@ import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.CreateRemoteFolderOperation;
+import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
+import com.owncloud.android.lib.resources.files.RemoteFile;
 import com.owncloud.android.operations.common.SyncOperation;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeType;
@@ -43,7 +45,8 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
     
     protected String mRemotePath;
     private boolean mCreateFullPath;
-    
+    private RemoteFile createdRemoteFolder;
+
     /**
      * Constructor
      * 
@@ -62,6 +65,10 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
         RemoteOperationResult result =  operation.execute(client);
         
         if (result.isSuccess()) {
+            ReadRemoteFolderOperation remoteFolderOperation = new ReadRemoteFolderOperation(mRemotePath);
+            RemoteOperationResult remoteFolderOperationResult = remoteFolderOperation.execute(client);
+
+            createdRemoteFolder = (RemoteFile) remoteFolderOperationResult.getData().get(0);
             saveFolderInDB();
         } else {
             Log_OC.e(TAG, mRemotePath + " hasn't been created");
@@ -88,7 +95,7 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
     /**
      * Save new directory in local database.
      */
-    public void saveFolderInDB() {
+    private void saveFolderInDB() {
         if (mCreateFullPath && getStorageManager().
                 getFileByPath(FileStorageUtils.getParentPath(mRemotePath)) == null){// When parent
                                                                                     // of remote path
@@ -96,7 +103,7 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
             String[] subFolders = mRemotePath.split("/");
             String composedRemotePath = "/";
 
-            // For each antecesor folders create them recursively
+            // For each ancestor folders create them recursively
             for (String subFolder : subFolders) {
                 if (!subFolder.isEmpty()) {
                     composedRemotePath = composedRemotePath + subFolder + "/";
@@ -109,6 +116,7 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper
             newDir.setMimetype(MimeType.DIRECTORY);
             long parentId = getStorageManager().getFileByPath(FileStorageUtils.getParentPath(mRemotePath)).getFileId();
             newDir.setParentId(parentId);
+            newDir.setRemoteId(createdRemoteFolder.getRemoteId());
             newDir.setModificationTimestamp(System.currentTimeMillis());
             getStorageManager().saveFile(newDir);
 

+ 40 - 5
src/main/java/com/owncloud/android/operations/DownloadFileOperation.java

@@ -22,8 +22,11 @@
 package com.owncloud.android.operations;
 
 import android.accounts.Account;
+import android.content.Context;
 import android.webkit.MimeTypeMap;
 
+import com.owncloud.android.datamodel.DecryptedFolderMetadata;
+import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
@@ -32,9 +35,11 @@ import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.DownloadRemoteFileOperation;
+import com.owncloud.android.utils.EncryptionUtils;
 import com.owncloud.android.utils.FileStorageUtils;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
@@ -46,10 +51,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
 public class DownloadFileOperation extends RemoteOperation {
     
     private static final String TAG = DownloadFileOperation.class.getSimpleName();
-
     private Account mAccount;
+
     private OCFile mFile;
     private String mBehaviour;
+    private Context mContext;
     private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
     private long mModificationTimestamp = 0;
     private String mEtag = "";
@@ -60,8 +66,8 @@ public class DownloadFileOperation extends RemoteOperation {
     private String mPackageName;
 
 
-    public DownloadFileOperation(Account account, OCFile file, String behaviour, String activityName,
-                                 String packageName) {
+    public DownloadFileOperation(Account account, OCFile file, String behaviour, String activityName, 
+                                 String packageName, Context context) {
         if (account == null) {
             throw new IllegalArgumentException("Illegal null account in DownloadFileOperation " +
                     "creation");
@@ -76,6 +82,7 @@ public class DownloadFileOperation extends RemoteOperation {
         mBehaviour = behaviour;
         mActivityName = activityName;
         mPackageName = packageName;
+        mContext = context;
     }
 
 
@@ -175,10 +182,38 @@ public class DownloadFileOperation extends RemoteOperation {
             mEtag = mDownloadOperation.getEtag();
             newFile = new File(getSavePath());
             newFile.getParentFile().mkdirs();
+
+            // decrypt file
+            if (mFile.isEncrypted() && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
+                FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(mAccount, mContext.getContentResolver());
+
+                OCFile parent = fileDataStorageManager.getFileByPath(mFile.getParentRemotePath());
+
+                DecryptedFolderMetadata metadata = EncryptionUtils.downloadFolderMetadata(parent, client, mContext, mAccount);
+
+                if (metadata == null) {
+                    return new RemoteOperationResult(RemoteOperationResult.ResultCode.METADATA_NOT_FOUND);
+                }
+                byte[] key = EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles()
+                        .get(mFile.getEncryptedFileName()).getEncrypted().getKey());
+                byte[] iv = EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles()
+                        .get(mFile.getEncryptedFileName()).getInitializationVector());
+                byte[] authenticationTag = EncryptionUtils.decodeStringToBase64Bytes(metadata.getFiles()
+                        .get(mFile.getEncryptedFileName()).getAuthenticationTag());
+
+                try {
+                    byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile, key, iv, authenticationTag);
+
+                    FileOutputStream fileOutputStream = new FileOutputStream(tmpFile);
+                    fileOutputStream.write(decryptedBytes);
+                    fileOutputStream.close();
+                } catch (Exception e) {
+                    return new RemoteOperationResult(e);
+                }
+            }
             moved = tmpFile.renameTo(newFile);
             if (!moved) {
-                result = new RemoteOperationResult(
-                        RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED);
+                result = new RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED);
             }
         }
         Log_OC.i(TAG, "Download of " + mFile.getRemotePath() + " to " + getSavePath() + ": " +

+ 133 - 97
src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java

@@ -1,21 +1,20 @@
 /**
- *   ownCloud Android client application
- *
- *   @author David A. Velasco
- *   Copyright (C) 2015 ownCloud Inc.
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License version 2,
- *   as published by the Free Software Foundation.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * ownCloud Android client application
  *
+ * @author David A. Velasco
+ * Copyright (C) 2015 ownCloud Inc.
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
 package com.owncloud.android.operations;
@@ -25,6 +24,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.util.Log;
 
+import com.owncloud.android.datamodel.DecryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.OwnCloudClient;
@@ -39,6 +39,7 @@ import com.owncloud.android.lib.resources.shares.GetRemoteSharesForFileOperation
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
 import com.owncloud.android.utils.DataHolderUtil;
+import com.owncloud.android.utils.EncryptionUtils;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
 
@@ -49,14 +50,13 @@ import java.util.Map;
 import java.util.Vector;
 
 
-
 /**
  *  Remote operation performing the synchronization of the list of files contained 
  *  in a folder identified with its remote path.
- *  
+ *
  *  Fetches the list and properties of the files contained in the given folder, including their 
  *  properties, and updates the local database with them.
- *  
+ *
  *  Does NOT enter in the child folders to synchronize their contents also.
  */
 @SuppressWarnings("PMD.AvoidDuplicateLiterals")
@@ -64,26 +64,26 @@ public class RefreshFolderOperation extends RemoteOperation {
 
     private static final String TAG = RefreshFolderOperation.class.getSimpleName();
 
-    public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED  = 
+    public static final String EVENT_SINGLE_FOLDER_CONTENTS_SYNCED =
             RefreshFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_CONTENTS_SYNCED";
-    public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED    = 
+    public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED =
             RefreshFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED";
-    
+
     /** Time stamp for the synchronization process in progress */
     private long mCurrentSyncTime;
-    
+
     /** Remote folder to synchronize */
     private OCFile mLocalFolder;
-    
+
     /** Access to the local database */
     private FileDataStorageManager mStorageManager;
-    
+
     /** Account where the file to synchronize belongs */
     private Account mAccount;
-    
+
     /** Android context; necessary to send requests to the download service */
     private Context mContext;
-    
+
     /** Files and folders contained in the synchronized folder after a successful operation */
     private List<OCFile> mChildren;
 
@@ -99,12 +99,14 @@ public class RefreshFolderOperation extends RemoteOperation {
      **/
     private Map<String, String> mForgottenLocalFiles;
 
-    /** 'True' means that this operation is part of a full account synchronization */ 
+    /**
+     * 'True' means that this operation is part of a full account synchronization
+     */
     private boolean mSyncFullAccount;
 
     /** 'True' means that Share resources bound to the files into should be refreshed also */
     private boolean mIsShareSupported;
-    
+
     /** 'True' means that the remote folder changed and should be fetched */
     private boolean mRemoteFolderChanged;
 
@@ -117,7 +119,7 @@ public class RefreshFolderOperation extends RemoteOperation {
 
     /**
      * Creates a new instance of {@link RefreshFolderOperation}.
-     * 
+     *
      * @param   folder                  Folder to synchronize.
      * @param   currentSyncTime         Time stamp for the synchronization process in progress.
      * @param   syncFullAccount         'True' means that this operation is part of a full account 
@@ -150,33 +152,33 @@ public class RefreshFolderOperation extends RemoteOperation {
         mIgnoreETag = ignoreETag;
         mFilesToSyncContents = new Vector<SynchronizeFileOperation>();
     }
-    
-    
+
+
     public int getConflictsFound() {
         return mConflictsFound;
     }
-    
+
     public int getFailsInKeptInSyncFound() {
         return mFailsInKeptInSyncFound;
     }
-    
+
     public Map<String, String> getForgottenLocalFiles() {
         return mForgottenLocalFiles;
     }
-    
+
     /**
      * Returns the list of files and folders contained in the synchronized folder, 
      * if called after synchronization is complete.
-     * 
-     * @return  List of files and folders contained in the synchronized folder.
+     *
+     * @return List of files and folders contained in the synchronized folder.
      */
     public List<OCFile> getChildren() {
         return mChildren;
     }
-    
+
     /**
      * Performs the synchronization.
-     * 
+     *
      * {@inheritDoc}
      */
     @Override
@@ -185,14 +187,14 @@ public class RefreshFolderOperation extends RemoteOperation {
         mFailsInKeptInSyncFound = 0;
         mConflictsFound = 0;
         mForgottenLocalFiles.clear();
-        
+
         if (OCFile.ROOT_PATH.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount) {
             updateOCVersion(client);
             updateUserProfile();
         }
-        
+
         result = checkForChanges(client);
-        
+
         if (result.isSuccess()) {
             if (mRemoteFolderChanged) {
                 result = fetchAndSyncRemoteFolder(client);
@@ -206,25 +208,25 @@ public class RefreshFolderOperation extends RemoteOperation {
                 startContentSynchronizations(mFilesToSyncContents);
             }
         }
-        
-        if (!mSyncFullAccount) {            
+
+        if (!mSyncFullAccount) {
             sendLocalBroadcast(
                     EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result
             );
         }
-        
+
         if (result.isSuccess() && mIsShareSupported && !mSyncFullAccount) {
             refreshSharesForFolder(client); // share result is ignored 
         }
-        
-        if (!mSyncFullAccount) {            
+
+        if (!mSyncFullAccount) {
             sendLocalBroadcast(
                     EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result
             );
         }
-        
+
         return result;
-        
+
     }
 
     private void updateOCVersion(OwnCloudClient client) {
@@ -252,10 +254,10 @@ public class RefreshFolderOperation extends RemoteOperation {
         }
     }
 
-    private void updateCapabilities(){
+    private void updateCapabilities() {
         GetCapabilitiesOperarion getCapabilities = new GetCapabilitiesOperarion();
-        RemoteOperationResult  result = getCapabilities.execute(mStorageManager,mContext);
-        if (!result.isSuccess()){
+        RemoteOperationResult result = getCapabilities.execute(mStorageManager, mContext);
+        if (!result.isSuccess()) {
             Log_OC.w(TAG, "Update Capabilities unsuccessfully");
         }
     }
@@ -266,11 +268,11 @@ public class RefreshFolderOperation extends RemoteOperation {
         String remotePath = mLocalFolder.getRemotePath();
 
         Log_OC.d(TAG, "Checking changes in " + mAccount.name + remotePath);
-        
+
         // remote request 
         ReadRemoteFileOperation operation = new ReadRemoteFileOperation(remotePath);
-        result = operation.execute(client);
-        if (result.isSuccess()){
+        result = operation.execute(client, true);
+        if (result.isSuccess()) {
             OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0));
 
             if (!mIgnoreETag) {
@@ -286,24 +288,24 @@ public class RefreshFolderOperation extends RemoteOperation {
             }
 
             result = new RemoteOperationResult(ResultCode.OK);
-        
+
             Log_OC.i(TAG, "Checked " + mAccount.name + remotePath + " : " +
                     (mRemoteFolderChanged ? "changed" : "not changed"));
-            
+
         } else {
             // check failed
             if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
                 removeLocalFolder();
             }
             if (result.isException()) {
-                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath  + " : " + 
+                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " +
                         result.getLogMessage(), result.getException());
             } else {
-                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " + 
+                Log_OC.e(TAG, "Checked " + mAccount.name + remotePath + " : " +
                         result.getLogMessage());
             }
         }
-        
+
         return result;
     }
 
@@ -311,32 +313,32 @@ public class RefreshFolderOperation extends RemoteOperation {
     private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) {
         String remotePath = mLocalFolder.getRemotePath();
         ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(remotePath);
-        RemoteOperationResult result = operation.execute(client);
+        RemoteOperationResult result = operation.execute(client, true);
         Log_OC.d(TAG, "Synchronizing " + mAccount.name + remotePath);
-        
+
         if (result.isSuccess()) {
             synchronizeData(result.getData());
-            if (mConflictsFound > 0  || mFailsInKeptInSyncFound > 0) {
-                result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);   
-                    // should be a different result code, but will do the job
+            if (mConflictsFound > 0 || mFailsInKeptInSyncFound > 0) {
+                result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
+                // should be a different result code, but will do the job
             }
         } else {
             if (result.getCode() == ResultCode.FILE_NOT_FOUND) {
                 removeLocalFolder();
             }
         }
-        
+
         return result;
     }
 
-    
+
     private void removeLocalFolder() {
         if (mStorageManager.fileExists(mLocalFolder.getFileId())) {
             String currentSavePath = FileStorageUtils.getSavePath(mAccount.name);
             mStorageManager.removeFolder(
-                    mLocalFolder, 
-                    true, 
-                    (   mLocalFolder.isDown() && 
+                    mLocalFolder,
+                    true,
+                    (mLocalFolder.isDown() &&
                             mLocalFolder.getStoragePath().startsWith(currentSavePath)
                     )
             );
@@ -360,26 +362,40 @@ public class RefreshFolderOperation extends RemoteOperation {
         OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) folderAndFiles.get(0));
         remoteFolder.setParentId(mLocalFolder.getParentId());
         remoteFolder.setFileId(mLocalFolder.getFileId());
-        
+
         Log_OC.d(TAG, "Remote folder " + mLocalFolder.getRemotePath()
                 + " changed - starting update of local data ");
-        
+
         List<OCFile> updatedFiles = new Vector<OCFile>(folderAndFiles.size() - 1);
         mFilesToSyncContents.clear();
 
+        // if local folder is encrypted, download fresh metadata
+        DecryptedFolderMetadata metadata;
+        boolean encryptedAncestor = FileStorageUtils.checkIfInEncryptedFolder(mLocalFolder, mStorageManager);
+        if (encryptedAncestor && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
+            metadata = EncryptionUtils.downloadFolderMetadata(mLocalFolder, getClient(), mContext, mAccount);
+        } else {
+            metadata = null;
+        }
+
         // get current data about local contents of the folder to synchronize
         List<OCFile> localFiles = mStorageManager.getFolderContent(mLocalFolder, false);
         Map<String, OCFile> localFilesMap = new HashMap<String, OCFile>(localFiles.size());
         for (OCFile file : localFiles) {
-            localFilesMap.put(file.getRemotePath(), file);
+            String remotePath = file.getRemotePath();
+
+            if (metadata != null && !file.isFolder()) {
+                remotePath = file.getParentRemotePath() + file.getEncryptedFileName();
+            }
+            localFilesMap.put(remotePath, file);
         }
-        
+
         // loop to update every child
         OCFile remoteFile = null;
         OCFile localFile = null;
         OCFile updatedFile = null;
         RemoteFile r;
-        for (int i=1; i<folderAndFiles.size(); i++) {
+        for (int i = 1; i < folderAndFiles.size(); i++) {
             /// new OCFile instance with the data from the server
             r = (RemoteFile) folderAndFiles.get(i);
             remoteFile = FileStorageUtils.fillOCFile(r);
@@ -391,7 +407,7 @@ public class RefreshFolderOperation extends RemoteOperation {
             /// retrieve local data for the read file 
             //  localFile = mStorageManager.getFileByPath(remoteFile.getRemotePath());
             localFile = localFilesMap.remove(remoteFile.getRemotePath());
-            
+
             /// add to updatedFile data about LOCAL STATE (not existing in server)
             updatedFile.setLastSyncDateForProperties(mCurrentSyncTime);
             if (localFile != null) {
@@ -426,16 +442,36 @@ public class RefreshFolderOperation extends RemoteOperation {
 
             /// prepare content synchronization for kept-in-sync files
             if (updatedFile.isAvailableOffline()) {
-                SynchronizeFileOperation operation = new SynchronizeFileOperation(  localFile,        
-                                                                                    remoteFile, 
-                                                                                    mAccount, 
-                                                                                    true, 
-                                                                                    mContext
-                                                                                    );
-                
+                SynchronizeFileOperation operation = new SynchronizeFileOperation(localFile,
+                        remoteFile,
+                        mAccount,
+                        true,
+                        mContext
+                );
+
                 mFilesToSyncContents.add(operation);
             }
 
+            // update file name for encrypted files
+            if (metadata != null) {
+                updatedFile.setEncryptedFileName(updatedFile.getFileName());
+                try {
+                    String decryptedFileName = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted()
+                            .getFilename();
+                    String mimetype = metadata.getFiles().get(updatedFile.getFileName()).getEncrypted().getMimetype();
+                    updatedFile.setFileName(decryptedFileName);
+                    if (mimetype == null || mimetype.isEmpty()) {
+                        updatedFile.setMimetype("application/octet-stream");
+                    } else {
+                        updatedFile.setMimetype(mimetype);
+                    }
+                } catch (NullPointerException e) {
+                    Log_OC.e(TAG, "Metadata for file " + updatedFile.getFileId() + " not found!");
+                }
+            }
+
+            updatedFile.setEncrypted(encryptedAncestor);
+            
             updatedFiles.add(updatedFile);
         }
 
@@ -448,15 +484,15 @@ public class RefreshFolderOperation extends RemoteOperation {
     /**
      * Performs a list of synchronization operations, determining if a download or upload is needed
      * or if exists conflict due to changes both in local and remote contents of the each file.
-     * 
+     *
      * If download or upload is needed, request the operation to the corresponding service and goes 
      * on.
-     * 
+     *
      * @param filesToSyncContents       Synchronization operations to execute.
      */
     private void startContentSynchronizations(List<SynchronizeFileOperation> filesToSyncContents) {
         RemoteOperationResult contentsResult;
-        for (SynchronizeFileOperation op: filesToSyncContents) {
+        for (SynchronizeFileOperation op : filesToSyncContents) {
             contentsResult = op.execute(mStorageManager, mContext);   // async
             if (!contentsResult.isSuccess()) {
                 if (contentsResult.getCode() == ResultCode.SYNC_CONFLICT) {
@@ -464,10 +500,10 @@ public class RefreshFolderOperation extends RemoteOperation {
                 } else {
                     mFailsInKeptInSyncFound++;
                     if (contentsResult.getException() != null) {
-                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
-                                +  contentsResult.getLogMessage(), contentsResult.getException());
+                        Log_OC.e(TAG, "Error while synchronizing favourites : "
+                                + contentsResult.getLogMessage(), contentsResult.getException());
                     } else {
-                        Log_OC.e(TAG, "Error while synchronizing favourites : " 
+                        Log_OC.e(TAG, "Error while synchronizing favourites : "
                                 + contentsResult.getLogMessage());
                     }
                 }
@@ -480,21 +516,21 @@ public class RefreshFolderOperation extends RemoteOperation {
      * Syncs the Share resources for the files contained in the folder refreshed (children, not deeper descendants).
      *
      * @param client    Handler of a session with an OC server.
-     * @return          The result of the remote operation retrieving the Share resources in the folder refreshed by
+     * @return The result of the remote operation retrieving the Share resources in the folder refreshed by
      *                  the operation.
      */
     private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) {
         RemoteOperationResult result;
-        
+
         // remote request 
-        GetRemoteSharesForFileOperation operation = 
+        GetRemoteSharesForFileOperation operation =
                 new GetRemoteSharesForFileOperation(mLocalFolder.getRemotePath(), true, true);
         result = operation.execute(client);
-        
+
         if (result.isSuccess()) {
             // update local database
             ArrayList<OCShare> shares = new ArrayList<OCShare>();
-            for(Object obj: result.getData()) {
+            for (Object obj : result.getData()) {
                 shares.add((OCShare) obj);
             }
             mStorageManager.saveSharesInFolder(shares, mLocalFolder);
@@ -502,12 +538,12 @@ public class RefreshFolderOperation extends RemoteOperation {
 
         return result;
     }
-    
+
 
     /**
      * Sends a message to any application component interested in the progress 
      * of the synchronization.
-     * 
+     *
      * @param event
      * @param dirRemotePath     Remote path of a folder that was just synchronized 
      *                          (with or without success)
@@ -515,7 +551,7 @@ public class RefreshFolderOperation extends RemoteOperation {
      */
     private void sendLocalBroadcast(
             String event, String dirRemotePath, RemoteOperationResult result
-        ) {
+    ) {
         Log_OC.d(TAG, "Send broadcast " + event);
         Intent intent = new Intent(event);
         intent.putExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME, mAccount.name);

+ 25 - 7
src/main/java/com/owncloud/android/operations/RemoveFileOperation.java

@@ -1,8 +1,9 @@
-/**
+/*
  *   ownCloud Android client application
  *
  *   @author David A. Velasco
  *   @author masensio
+ *   @author Tobias Kaminsky
  *   Copyright (C) 2015 ownCloud Inc.
  *
  *   This program is free software: you can redistribute it and/or modify
@@ -21,9 +22,13 @@
 
 package com.owncloud.android.operations;
 
+import android.accounts.Account;
+import android.content.Context;
+
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
 import com.owncloud.android.lib.resources.files.RemoveRemoteFileOperation;
@@ -36,10 +41,12 @@ import com.owncloud.android.operations.common.SyncOperation;
 public class RemoveFileOperation extends SyncOperation {
     
     // private static final String TAG = RemoveFileOperation.class.getSimpleName();
-    
-    OCFile mFileToRemove;
-    String mRemotePath;
-    boolean mOnlyLocalCopy;
+
+    private OCFile mFileToRemove;
+    private String mRemotePath;
+    private boolean mOnlyLocalCopy;
+    private Account mAccount;
+    private Context mContext;
     
     
     /**
@@ -50,9 +57,11 @@ public class RemoveFileOperation extends SyncOperation {
      * @param onlyLocalCopy         When 'true', and a local copy of the file exists, only this is 
      *                              removed.
      */
-    public RemoveFileOperation(String remotePath, boolean onlyLocalCopy) {
+    public RemoveFileOperation(String remotePath, boolean onlyLocalCopy, Account account, Context context) {
         mRemotePath = remotePath;
         mOnlyLocalCopy = onlyLocalCopy;
+        mAccount = account;
+        mContext = context;
     }
     
     
@@ -73,6 +82,7 @@ public class RemoveFileOperation extends SyncOperation {
     @Override
     protected RemoteOperationResult run(OwnCloudClient client) {
         RemoteOperationResult result = null;
+        RemoteOperation operation;
         
         mFileToRemove = getStorageManager().getFileByPath(mRemotePath);
 
@@ -81,7 +91,15 @@ public class RemoveFileOperation extends SyncOperation {
 
         boolean localRemovalFailed = false;
         if (!mOnlyLocalCopy) {
-            RemoveRemoteFileOperation operation = new RemoveRemoteFileOperation(mRemotePath);
+
+            if (mFileToRemove.isEncrypted() &&
+                    android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
+                OCFile parent = getStorageManager().getFileByPath(mFileToRemove.getParentRemotePath());
+                operation = new RemoveRemoteEncryptedFileOperation(mRemotePath, parent.getLocalId(), mAccount, mContext,
+                        mFileToRemove.getEncryptedFileName());
+            } else {
+                operation = new RemoveRemoteFileOperation(mRemotePath);
+            }
             result = operation.execute(client);
             if (result.isSuccess() || result.getCode() == ResultCode.FILE_NOT_FOUND) {
                 localRemovalFailed = !(getStorageManager().removeFile(mFileToRemove, true, true));

+ 173 - 0
src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.java

@@ -0,0 +1,173 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2017 Tobias Kaminsky
+ * Copyright (C) 2017 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.operations;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+
+import com.google.gson.reflect.TypeToken;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.datamodel.DecryptedFolderMetadata;
+import com.owncloud.android.datamodel.EncryptedFolderMetadata;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.network.WebdavUtils;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.files.GetMetadataOperation;
+import com.owncloud.android.lib.resources.files.LockFileOperation;
+import com.owncloud.android.lib.resources.files.UnlockFileOperation;
+import com.owncloud.android.lib.resources.files.UpdateMetadataOperation;
+import com.owncloud.android.utils.EncryptionUtils;
+
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.jackrabbit.webdav.client.methods.DeleteMethod;
+
+/**
+ * Remote operation performing the removal of a remote encrypted file or folder
+ */
+@RequiresApi(api = Build.VERSION_CODES.KITKAT)
+public class RemoveRemoteEncryptedFileOperation extends RemoteOperation {
+    private static final String TAG = RemoveRemoteEncryptedFileOperation.class.getSimpleName();
+
+    private static final int REMOVE_READ_TIMEOUT = 30000;
+    private static final int REMOVE_CONNECTION_TIMEOUT = 5000;
+
+    private String remotePath;
+    private String parentId;
+    private Account account;
+
+    private ArbitraryDataProvider arbitraryDataProvider;
+    private String fileName;
+
+    /**
+     * Constructor
+     *
+     * @param remotePath RemotePath of the remote file or folder to remove from the server
+     * @param parentId   local id of parent folder
+     */
+    public RemoveRemoteEncryptedFileOperation(String remotePath, String parentId, Account account, Context context,
+                                              String fileName) {
+        this.remotePath = remotePath;
+        this.parentId = parentId;
+        this.account = account;
+        this.fileName = fileName;
+
+        arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
+    }
+
+    /**
+     * Performs the remove operation.
+     */
+    @Override
+    protected RemoteOperationResult run(OwnCloudClient client) {
+        RemoteOperationResult result;
+        DeleteMethod delete = null;
+        String token = null;
+        DecryptedFolderMetadata metadata;
+
+        String privateKey = arbitraryDataProvider.getValue(account.name, EncryptionUtils.PRIVATE_KEY);
+
+        // unlock
+
+        try {
+            // Lock folder
+            LockFileOperation lockFileOperation = new LockFileOperation(parentId);
+            RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client, true);
+
+            if (lockFileOperationResult.isSuccess()) {
+                token = (String) lockFileOperationResult.getData().get(0);
+            } else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
+                throw new RemoteOperationFailedException("Forbidden! Please try again later.)");
+            } else {
+                throw new RemoteOperationFailedException("Unknown error!");
+            }
+
+            // refresh metadata
+            GetMetadataOperation getMetadataOperation = new GetMetadataOperation(parentId);
+            RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client, true);
+
+            if (getMetadataOperationResult.isSuccess()) {
+                // decrypt metadata
+                String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0);
+
+                EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
+                        serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
+                        });
+
+                metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, privateKey);
+            } else {
+                throw new RemoteOperationFailedException("No Metadata found!");
+            }
+
+            // delete file remote
+            delete = new DeleteMethod(client.getWebdavUri() + WebdavUtils.encodePath(remotePath));
+            int status = client.executeMethod(delete, REMOVE_READ_TIMEOUT, REMOVE_CONNECTION_TIMEOUT);
+
+            delete.getResponseBodyAsString();   // exhaust the response, although not interesting
+            result = new RemoteOperationResult((delete.succeeded() || status == HttpStatus.SC_NOT_FOUND), delete);
+            Log_OC.i(TAG, "Remove " + remotePath + ": " + result.getLogMessage());
+
+            // remove file from metadata
+            metadata.getFiles().remove(fileName);
+
+            EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
+                    privateKey);
+            String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
+
+            // upload metadata
+            UpdateMetadataOperation storeMetadataOperation = new UpdateMetadataOperation(parentId,
+                    serializedFolderMetadata, token);
+            RemoteOperationResult uploadMetadataOperationResult = storeMetadataOperation.execute(client, true);
+
+            if (!uploadMetadataOperationResult.isSuccess()) {
+                throw new RemoteOperationFailedException("Metadata not uploaded!");
+            }
+
+            // return success
+            return result;
+        } catch (Exception e) {
+            result = new RemoteOperationResult(e);
+            Log_OC.e(TAG, "Remove " + remotePath + ": " + result.getLogMessage(), e);
+
+        } finally {
+            if (delete != null) {
+                delete.releaseConnection();
+            }
+
+            // unlock file
+            if (token != null) {
+                UnlockFileOperation unlockFileOperation = new UnlockFileOperation(parentId, token);
+                RemoteOperationResult unlockFileOperationResult = unlockFileOperation.execute(client, true);
+
+                if (!unlockFileOperationResult.isSuccess()) {
+                    Log_OC.e(TAG, "Failed to unlock " + parentId);
+                }
+            }
+        }
+
+        return result;
+    }
+
+}

+ 564 - 118
src/main/java/com/owncloud/android/operations/UploadFileOperation.java

@@ -1,4 +1,4 @@
-/**
+/*
  * ownCloud Android client application
  *
  * @author David A. Velasco
@@ -20,11 +20,18 @@
 package com.owncloud.android.operations;
 
 import android.accounts.Account;
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
 
 import com.evernote.android.job.JobRequest;
 import com.evernote.android.job.util.Device;
+import com.google.gson.reflect.TypeToken;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.datamodel.DecryptedFolderMetadata;
+import com.owncloud.android.datamodel.EncryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
@@ -41,11 +48,17 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.ChunkedUploadRemoteFileOperation;
 import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation;
+import com.owncloud.android.lib.resources.files.GetMetadataOperation;
+import com.owncloud.android.lib.resources.files.LockFileOperation;
 import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
 import com.owncloud.android.lib.resources.files.RemoteFile;
+import com.owncloud.android.lib.resources.files.StoreMetadataOperation;
+import com.owncloud.android.lib.resources.files.UnlockFileOperation;
+import com.owncloud.android.lib.resources.files.UpdateMetadataOperation;
 import com.owncloud.android.lib.resources.files.UploadRemoteFileOperation;
 import com.owncloud.android.operations.common.SyncOperation;
 import com.owncloud.android.utils.ConnectivityUtils;
+import com.owncloud.android.utils.EncryptionUtils;
 import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.MimeType;
 import com.owncloud.android.utils.MimeTypeUtil;
@@ -68,9 +81,11 @@ import java.io.RandomAccessFile;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.nio.channels.OverlappingFileLockException;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 
@@ -97,6 +112,7 @@ public class UploadFileOperation extends SyncOperation {
      */
     private OCFile mOldFile;
     private String mRemotePath = null;
+    private String mFolderUnlockToken;
     private boolean mChunked = false;
     private boolean mRemoteFolderToBeCreated = false;
     private boolean mForceOverwrite = false;
@@ -112,7 +128,7 @@ public class UploadFileOperation extends SyncOperation {
      * Local path to file which is to be uploaded (before any possible renaming or moving).
      */
     private String mOriginalStoragePath = null;
-    private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
+    private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<>();
     private OnRenameListener mRenameUploadListener;
 
     private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false);
@@ -125,6 +141,10 @@ public class UploadFileOperation extends SyncOperation {
     protected RequestEntity mEntity = null;
 
     private Account mAccount;
+    private OCUpload mUpload;
+    private UploadsStorageManager uploadsStorageManager;
+
+    private boolean encryptedAncestor;
 
     public static OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType) {
 
@@ -175,6 +195,7 @@ public class UploadFileOperation extends SyncOperation {
         }
 
         mAccount = account;
+        mUpload = upload;
         if (file == null) {
             mFile = obtainNewOCFileToUpload(
                     upload.getRemotePath(),
@@ -197,6 +218,7 @@ public class UploadFileOperation extends SyncOperation {
         mRemoteFolderToBeCreated = upload.isCreateRemoteFolder();
         // Ignore power save mode only if user explicitly created this upload
         mIgnoringPowerSaveMode = (mCreatedBy == CREATED_BY_USER);
+        mFolderUnlockToken = upload.getFolderUnlockToken();
     }
 
     public boolean getIsWifiRequired() {
@@ -241,6 +263,10 @@ public class UploadFileOperation extends SyncOperation {
         return mFile.getRemotePath();
     }
 
+    public String getDecryptedRemotePath() {
+        return mFile.getDecryptedRemotePath();
+    }
+
     public String getMimeType() {
         return mFile.getMimetype();
     }
@@ -288,7 +314,7 @@ public class UploadFileOperation extends SyncOperation {
         return mDataTransferListeners;
     }
 
-    public void addDatatransferProgressListener(OnDatatransferProgressListener listener) {
+    public void addDataTransferProgressListener(OnDatatransferProgressListener listener) {
         synchronized (mDataTransferListeners) {
             mDataTransferListeners.add(listener);
         }
@@ -300,7 +326,7 @@ public class UploadFileOperation extends SyncOperation {
         }
     }
 
-    public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) {
+    public void removeDataTransferProgressListener(OnDatatransferProgressListener listener) {
         synchronized (mDataTransferListeners) {
             mDataTransferListeners.remove(listener);
         }
@@ -316,115 +342,463 @@ public class UploadFileOperation extends SyncOperation {
         mRenameUploadListener = listener;
     }
 
+    public boolean isChunkedUploadSupported() {
+        return mChunked;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
     @Override
     @SuppressWarnings("PMD.AvoidDuplicateLiterals")
     protected RemoteOperationResult run(OwnCloudClient client) {
         mCancellationRequested.set(false);
         mUploadStarted.set(true);
-        RemoteOperationResult result = null;
-        File temporalFile = null;
-        File originalFile = new File(mOriginalStoragePath);
-        File expectedFile = null;
-        FileLock fileLock = null;
 
-        UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(mContext.getContentResolver(),
-                mContext);
-
-        long size = 0;
+        uploadsStorageManager = new UploadsStorageManager(mContext.getContentResolver(), mContext);
 
         for (OCUpload ocUpload : uploadsStorageManager.getAllStoredUploads()) {
             if (ocUpload.getUploadId() == getOCUploadId()) {
-                ocUpload.setFileSize(size);
+                ocUpload.setFileSize(0);
                 uploadsStorageManager.updateUpload(ocUpload);
                 break;
             }
         }
 
-        try {
+        // check the existence of the parent folder for the file to upload
+        String remoteParentPath = new File(getRemotePath()).getParent();
+        remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ?
+                remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR;
 
-            if (Device.getNetworkType(mContext).equals(JobRequest.NetworkType.ANY) ||
-                    ConnectivityUtils.isInternetWalled(mContext)) {
-                return new RemoteOperationResult(ResultCode.NO_NETWORK_CONNECTION);
-            }
+        RemoteOperationResult result = grantFolderExistence(remoteParentPath, client);
 
-            /// Check that connectivity conditions are met and delays the upload otherwise
-            if (mOnWifiOnly && !Device.getNetworkType(mContext).equals(JobRequest.NetworkType.UNMETERED)) {
-                Log_OC.d(TAG, "Upload delayed until WiFi is available: " + getRemotePath());
-                return new RemoteOperationResult(ResultCode.DELAYED_FOR_WIFI);
+        if (!result.isSuccess()) {
+            return result;
+        }
+
+        OCFile parent = getStorageManager().getFileByPath(remoteParentPath);
+        mFile.setParentId(parent.getFileId());
+
+        // check if any parent is encrypted
+        encryptedAncestor = FileStorageUtils.checkIfInEncryptedFolder(parent, getStorageManager());
+        mFile.setEncrypted(encryptedAncestor);
+
+        // try to unlock folder with stored token, e.g. when upload needs to be resumed or app crashed
+        if (encryptedAncestor && !mFolderUnlockToken.isEmpty()) {
+            UnlockFileOperation unlockFileOperation = new UnlockFileOperation(parent.getLocalId(), mFolderUnlockToken);
+            RemoteOperationResult unlockFileOperationResult = unlockFileOperation.execute(client, true);
+
+            if (!unlockFileOperationResult.isSuccess()) {
+                return unlockFileOperationResult;
             }
+        }
 
-            // Check if charging conditions are met and delays the upload otherwise
-            if (mWhileChargingOnly && (!Device.getBatteryStatus(mContext).isCharging() && Device.getBatteryStatus
-                    (mContext).getBatteryPercent() < 1)) {
-                Log_OC.d(TAG, "Upload delayed until the device is charging: " + getRemotePath());
-                return new RemoteOperationResult(ResultCode.DELAYED_FOR_CHARGING);
+        if (encryptedAncestor) {
+            Log_OC.d(TAG, "encrypted upload");
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                return encryptedUpload(client, parent);
+            } else {
+                Log_OC.e(TAG, "Encrypted upload on old Android API");
+                return new RemoteOperationResult(ResultCode.OLD_ANDROID_API);
             }
+        } else {
+            Log_OC.d(TAG, "normal upload");
+            return normalUpload(client);
+        }
+    }
+
+    @SuppressLint("AndroidLintUseSparseArrays") // gson cannot handle sparse arrays easily, therefore use hashmap
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile parentFile) {
+        RemoteOperationResult result = null;
+        File temporalFile = null;
+        File originalFile = new File(mOriginalStoragePath);
+        File expectedFile = null;
+        FileLock fileLock = null;
+        long size;
 
-            // Check that device is not in power save mode
-            if (!mIgnoringPowerSaveMode && PowerUtils.isPowerSaveMode(mContext)) {
-                Log_OC.d(TAG, "Upload delayed because device is in power save mode: " + getRemotePath());
-                return new RemoteOperationResult(ResultCode.DELAYED_IN_POWER_SAVE_MODE);
+        boolean metadataExists = false;
+        String token = null;
+
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
+
+        String privateKey = arbitraryDataProvider.getValue(getAccount().name, EncryptionUtils.PRIVATE_KEY);
+        String publicKey = arbitraryDataProvider.getValue(getAccount().name, EncryptionUtils.PUBLIC_KEY);
+
+        try {
+            // check conditions 
+            result = checkConditions(originalFile);
+
+            /***** E2E *****/
+
+            // Lock folder
+            LockFileOperation lockFileOperation = new LockFileOperation(parentFile.getLocalId());
+            RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client, true);
+
+            if (lockFileOperationResult.isSuccess()) {
+                token = (String) lockFileOperationResult.getData().get(0);
+                // immediately store it 
+                mUpload.setFolderUnlockToken(token);
+                uploadsStorageManager.updateUpload(mUpload);
+            } else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
+                throw new Exception("Forbidden! Please try again later.)");
+            } else {
+                throw new Exception("Unknown error!");
             }
 
-            /// check if the file continues existing before schedule the operation
-            if (!originalFile.exists()) {
-                Log_OC.d(TAG, mOriginalStoragePath + " not exists anymore");
-                return new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
+            // Update metadata
+            GetMetadataOperation getMetadataOperation = new GetMetadataOperation(parentFile.getLocalId());
+            RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client, true);
+
+            DecryptedFolderMetadata metadata;
+
+            if (getMetadataOperationResult.isSuccess()) {
+                metadataExists = true;
+
+                // decrypt metadata
+                String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0);
+
+
+                EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
+                        serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
+                        });
+
+                metadata = EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, privateKey);
+
+            } else if (getMetadataOperationResult.getHttpCode() == HttpStatus.SC_NOT_FOUND) {
+                // new metadata
+                metadata = new DecryptedFolderMetadata();
+                metadata.setMetadata(new DecryptedFolderMetadata.Metadata());
+                metadata.getMetadata().setMetadataKeys(new HashMap<>());
+                String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
+                String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey);
+                metadata.getMetadata().getMetadataKeys().put(0, encryptedMetadataKey);
+            } else {
+                // TODO error
+                throw new Exception("something wrong");
             }
 
-            /// check the existence of the parent folder for the file to upload
-            String remoteParentPath = new File(getRemotePath()).getParent();
-            remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ?
-                    remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR;
-            result = grantFolderExistence(remoteParentPath, client);
+            /***** E2E *****/
+
+            // check name collision
+            checkNameCollision(client, metadata, parentFile.isEncrypted());
 
-            if (!result.isSuccess()) {
+            String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile);
+            expectedFile = new File(expectedPath);
 
+            result = copyFile(originalFile, expectedPath);
+            if (result != null) {
                 return result;
             }
 
-            /// set parent local id in uploading file
-            OCFile parent = getStorageManager().getFileByPath(remoteParentPath);
-            mFile.setParentId(parent.getFileId());
+            // Get the last modification date of the file from the file system
+            Long timeStampLong = originalFile.lastModified() / 1000;
+            String timeStamp = timeStampLong.toString();
 
-            /// automatic rename of file to upload in case of name collision in server
-            Log_OC.d(TAG, "Checking name collision in server");
-            if (!mForceOverwrite) {
-                String remotePath = getAvailableRemotePath(client, mRemotePath);
-                mWasRenamed = !remotePath.equals(mRemotePath);
-                if (mWasRenamed) {
-                    createNewOCFile(remotePath);
-                    Log_OC.d(TAG, "File renamed as " + remotePath);
-                }
-                mRemotePath = remotePath;
-                mRenameUploadListener.onRenameUpload();
-            }
+            /***** E2E *****/
 
-            if (mCancellationRequested.get()) {
-                throw new OperationCancelledException();
+            // Key, always generate new one
+            byte[] key = EncryptionUtils.generateKey();
+
+            // IV, always generate new one
+            byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength);
+
+            EncryptionUtils.EncryptedFile encryptedFile = EncryptionUtils.encryptFile(mFile, key, iv);
+
+            // new random file name, check if it exists in metadata
+            String encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
+
+            while (metadata.getFiles().get(encryptedFileName) != null) {
+                encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
             }
 
-            String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile);
-            expectedFile = new File(expectedPath);
+            mFile.setEncryptedFileName(encryptedFileName);
+
+            File encryptedTempFile = File.createTempFile("encFile", encryptedFileName);
+            FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
+            fileOutputStream.write(encryptedFile.encryptedBytes);
+            fileOutputStream.close();
 
-            /// copy the file locally before uploading
-            if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_COPY &&
-                    !mOriginalStoragePath.equals(expectedPath)) {
+            /***** E2E *****/
 
+            FileChannel channel = null;
+            try {
+                channel = new RandomAccessFile(mFile.getStoragePath(), "rw").getChannel();
+                fileLock = channel.tryLock();
+            } catch (FileNotFoundException e) {
+                // this basically means that the file is on SD card
+                // try to copy file to temporary dir if it doesn't exist
                 String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath();
                 mFile.setStoragePath(temporalPath);
                 temporalFile = new File(temporalPath);
 
+                Files.deleteIfExists(Paths.get(temporalPath));
                 result = copy(originalFile, temporalFile);
-                if (result != null) {
-                    return result;
+
+                if (result == null) {
+                    if (temporalFile.length() == originalFile.length()) {
+                        channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel();
+                        fileLock = channel.tryLock();
+                    } else {
+                        result = new RemoteOperationResult(ResultCode.LOCK_FAILED);
+                    }
+                }
+            }
+
+            try {
+                size = channel.size();
+            } catch (IOException e1) {
+                size = new File(mFile.getStoragePath()).length();
+            }
+
+            for (OCUpload ocUpload : uploadsStorageManager.getAllStoredUploads()) {
+                if (ocUpload.getUploadId() == getOCUploadId()) {
+                    ocUpload.setFileSize(size);
+                    uploadsStorageManager.updateUpload(ocUpload);
+                    break;
                 }
             }
 
+            /// perform the upload
+            if (mChunked && (size > ChunkedUploadRemoteFileOperation.CHUNK_SIZE)) {
+                mUploadOperation = new ChunkedUploadRemoteFileOperation(mContext, encryptedTempFile.getAbsolutePath(),
+                        mFile.getParentRemotePath() + encryptedFileName, mFile.getMimetype(),
+                        mFile.getEtagInConflict(), timeStamp);
+            } else {
+                mUploadOperation = new UploadRemoteFileOperation(encryptedTempFile.getAbsolutePath(),
+                        mFile.getParentRemotePath() + encryptedFileName, mFile.getMimetype(),
+                        mFile.getEtagInConflict(), timeStamp);
+            }
+
+            Iterator<OnDatatransferProgressListener> listener = mDataTransferListeners.iterator();
+            while (listener.hasNext()) {
+                mUploadOperation.addDatatransferProgressListener(listener.next());
+            }
+
             if (mCancellationRequested.get()) {
                 throw new OperationCancelledException();
             }
 
+//            FileChannel channel = null;
+//            try {
+//                channel = new RandomAccessFile(ocFile.getStoragePath(), "rw").getChannel();
+//                fileLock = channel.tryLock();
+//            } catch (FileNotFoundException e) {
+//                if (temporalFile == null) {
+//                    String temporalPath = FileStorageUtils.getTemporalPath(account.name) + ocFile.getRemotePath();
+//                    ocFile.setStoragePath(temporalPath);
+//                    temporalFile = new File(temporalPath);
+//
+//                    result = copy(originalFile, temporalFile);
+//
+//                    if (result != null) {
+//                        return result;
+//                    } else {
+//                        if (temporalFile.length() == originalFile.length()) {
+//                            channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel();
+//                            fileLock = channel.tryLock();
+//                        } else {
+//                            while (temporalFile.length() != originalFile.length()) {
+//                                Files.deleteIfExists(Paths.get(temporalPath));
+//                                result = copy(originalFile, temporalFile);
+//
+//                                if (result != null) {
+//                                    return result;
+//                                } else {
+//                                    channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").
+//                                            getChannel();
+//                                    fileLock = channel.tryLock();
+//                                }
+//                            }
+//                        }
+//                    }
+//                } else {
+//                    channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel();
+//                    fileLock = channel.tryLock();
+//                }
+//            }
+
+//            boolean test = true;
+//            if (test) {
+//                throw new Exception("test");
+//            }
+
+            result = mUploadOperation.execute(client);
+//            if (result == null || result.isSuccess() && mUploadOperation != null) {
+//                result = mUploadOperation.execute(client);
+
+            /// move local temporal file or original file to its corresponding
+            // location in the Nextcloud local folder
+            if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) {
+                result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
+            }
+//            }
+
+            if (result.isSuccess()) {
+                // upload metadata
+                DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
+                DecryptedFolderMetadata.Data data = new DecryptedFolderMetadata.Data();
+                data.setFilename(mFile.getFileName());
+                data.setMimetype(mFile.getMimetype());
+                data.setKey(EncryptionUtils.encodeBytesToBase64String(key));
+
+                decryptedFile.setEncrypted(data);
+                decryptedFile.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(iv));
+                decryptedFile.setAuthenticationTag(encryptedFile.authenticationTag);
+
+                metadata.getFiles().put(encryptedFileName, decryptedFile);
+
+                EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.encryptFolderMetadata(metadata,
+                        privateKey);
+                String serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata);
+
+                // upload metadata
+                RemoteOperationResult uploadMetadataOperationResult;
+                if (metadataExists) {
+                    // update metadata
+                    UpdateMetadataOperation storeMetadataOperation = new UpdateMetadataOperation(parentFile.getLocalId(),
+                            serializedFolderMetadata, token);
+                    uploadMetadataOperationResult = storeMetadataOperation.execute(client, true);
+                } else {
+                    // store metadata
+                    StoreMetadataOperation storeMetadataOperation = new StoreMetadataOperation(parentFile.getLocalId(),
+                            serializedFolderMetadata);
+                    uploadMetadataOperationResult = storeMetadataOperation.execute(client, true);
+                }
+
+                if (!uploadMetadataOperationResult.isSuccess()) {
+                    throw new Exception();
+                }
+            }
+        } catch (FileNotFoundException e) {
+            Log_OC.d(TAG, mFile.getStoragePath() + " not exists anymore");
+            result = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
+        } catch (OverlappingFileLockException e) {
+            Log_OC.d(TAG, "Overlapping file lock exception");
+            result = new RemoteOperationResult(ResultCode.LOCK_FAILED);
+        } catch (Exception e) {
+            result = new RemoteOperationResult(e);
+        } finally {
+            mUploadStarted.set(false);
+
+            if (fileLock != null) {
+                try {
+                    fileLock.release();
+                } catch (IOException e) {
+                    Log_OC.e(TAG, "Failed to unlock file with path " + mFile.getStoragePath());
+                }
+            }
+
+            if (temporalFile != null && !originalFile.equals(temporalFile)) {
+                temporalFile.delete();
+            }
+            if (result == null) {
+                result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR);
+            }
+
+            if (result.isSuccess()) {
+                Log_OC.i(TAG, "Upload of " + mFile.getStoragePath() + " to " + mFile.getRemotePath() + ": " +
+                        result.getLogMessage());
+            } else {
+                if (result.getException() != null) {
+                    if (result.isCancelled()) {
+                        Log_OC.w(TAG, "Upload of " + mFile.getStoragePath() + " to " + mFile.getRemotePath() +
+                                ": " + result.getLogMessage());
+                    } else {
+                        Log_OC.e(TAG, "Upload of " + mFile.getStoragePath() + " to " + mFile.getRemotePath() +
+                                ": " + result.getLogMessage(), result.getException());
+                    }
+
+                } else {
+                    Log_OC.e(TAG, "Upload of " + mFile.getStoragePath() + " to " + mFile.getRemotePath() +
+                            ": " + result.getLogMessage());
+                }
+            }
+        }
+
+        if (result.isSuccess()) {
+            handleSuccessfulUpload(temporalFile, expectedFile, originalFile, client);
+            RemoteOperationResult unlockFolderResult = unlockFolder(parentFile, client, token);
+
+            if (!unlockFolderResult.isSuccess()) {
+                return unlockFolderResult;
+            }
+
+        } else if (result.getCode() == ResultCode.SYNC_CONFLICT) {
+            getStorageManager().saveConflict(mFile, mFile.getEtagInConflict());
+        }
+
+        return result;
+    }
+
+    private RemoteOperationResult unlockFolder(OCFile parentFolder, OwnCloudClient client, String token) {
+        if (token != null) {
+            UnlockFileOperation unlockFileOperation = new UnlockFileOperation(parentFolder.getLocalId(), token);
+            RemoteOperationResult unlockFileOperationResult = unlockFileOperation.execute(client, true);
+
+            return unlockFileOperationResult;
+        } else
+            return new RemoteOperationResult(new Exception("No token available"));
+    }
+
+    private RemoteOperationResult checkConditions(File originalFile) {
+        // check that internet is not behind walled garden
+        if (Device.getNetworkType(mContext).equals(JobRequest.NetworkType.ANY) ||
+                ConnectivityUtils.isInternetWalled(mContext)) {
+            return new RemoteOperationResult(ResultCode.NO_NETWORK_CONNECTION);
+        }
+
+        // check that connectivity conditions are met and delays the upload otherwise
+        if (mOnWifiOnly && !Device.getNetworkType(mContext).equals(JobRequest.NetworkType.UNMETERED)) {
+            Log_OC.d(TAG, "Upload delayed until WiFi is available: " + getRemotePath());
+            return new RemoteOperationResult(ResultCode.DELAYED_FOR_WIFI);
+        }
+
+        // check if charging conditions are met and delays the upload otherwise
+        if (mWhileChargingOnly && !Device.getBatteryStatus(mContext).isCharging()) {
+            Log_OC.d(TAG, "Upload delayed until the device is charging: " + getRemotePath());
+            return new RemoteOperationResult(ResultCode.DELAYED_FOR_CHARGING);
+        }
+
+        // check that device is not in power save mode
+        if (!mIgnoringPowerSaveMode && PowerUtils.isPowerSaveMode(mContext)) {
+            Log_OC.d(TAG, "Upload delayed because device is in power save mode: " + getRemotePath());
+            return new RemoteOperationResult(ResultCode.DELAYED_IN_POWER_SAVE_MODE);
+        }
+
+        // check if the file continues existing before schedule the operation
+        if (!originalFile.exists()) {
+            Log_OC.d(TAG, mOriginalStoragePath + " not exists anymore");
+            return new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
+        }
+
+        return null;
+    }
+
+    private RemoteOperationResult normalUpload(OwnCloudClient client) {
+        RemoteOperationResult result = null;
+        File temporalFile = null;
+        File originalFile = new File(mOriginalStoragePath);
+        File expectedFile = null;
+        FileLock fileLock = null;
+        long size = 0;
+
+        try {
+            // check conditions
+            result = checkConditions(originalFile);
+
+            // check name collision
+            checkNameCollision(client, null, false);
+
+            String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile);
+            expectedFile = new File(expectedPath);
+
+            result = copyFile(originalFile, expectedPath);
+            if (result != null) {
+                return result;
+            }
+
             // Get the last modification date of the file from the file system
             Long timeStampLong = originalFile.lastModified() / 1000;
             String timeStamp = timeStampLong.toString();
@@ -467,13 +841,11 @@ public class UploadFileOperation extends SyncOperation {
                 }
             }
 
-            /// perform the upload
-            if (mChunked &&
-                    (size > ChunkedUploadRemoteFileOperation.CHUNK_SIZE)) {
+            // perform the upload
+            if (mChunked && (size > ChunkedUploadRemoteFileOperation.CHUNK_SIZE)) {
                 mUploadOperation = new ChunkedUploadRemoteFileOperation(mContext, mFile.getStoragePath(),
                         mFile.getRemotePath(), mFile.getMimetype(), mFile.getEtagInConflict(), timeStamp);
             } else {
-
                 mUploadOperation = new UploadRemoteFileOperation(mFile.getStoragePath(),
                         mFile.getRemotePath(), mFile.getMimetype(), mFile.getEtagInConflict(), timeStamp);
             }
@@ -488,17 +860,14 @@ public class UploadFileOperation extends SyncOperation {
             }
 
             if (result == null || result.isSuccess() && mUploadOperation != null) {
-                result = mUploadOperation.execute(client);
+                result = mUploadOperation.execute(client, mFile.isEncrypted());
 
                 /// move local temporal file or original file to its corresponding
-                // location in the ownCloud local folder
+                // location in the Nextcloud local folder
                 if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) {
                     result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
                 }
-
             }
-
-
         } catch (FileNotFoundException e) {
             Log_OC.d(TAG, mOriginalStoragePath + " not exists anymore");
             result = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
@@ -507,7 +876,6 @@ public class UploadFileOperation extends SyncOperation {
             result = new RemoteOperationResult(ResultCode.LOCK_FAILED);
         } catch (Exception e) {
             result = new RemoteOperationResult(e);
-
         } finally {
             mUploadStarted.set(false);
 
@@ -526,6 +894,7 @@ public class UploadFileOperation extends SyncOperation {
             if (result == null) {
                 result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR);
             }
+
             if (result.isSuccess()) {
                 Log_OC.i(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath + ": " +
                         result.getLogMessage());
@@ -538,7 +907,6 @@ public class UploadFileOperation extends SyncOperation {
                         Log_OC.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath +
                                 ": " + result.getLogMessage(), result.getException());
                     }
-
                 } else {
                     Log_OC.e(TAG, "Upload of " + mOriginalStoragePath + " to " + mRemotePath +
                             ": " + result.getLogMessage());
@@ -547,9 +915,58 @@ public class UploadFileOperation extends SyncOperation {
         }
 
         if (result.isSuccess()) {
+            handleSuccessfulUpload(temporalFile, expectedFile, originalFile, client);
+        } else if (result.getCode() == ResultCode.SYNC_CONFLICT) {
+            getStorageManager().saveConflict(mFile, mFile.getEtagInConflict());
+        }
+
+        return result;
+    }
 
-            if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_FORGET) {
+    private RemoteOperationResult copyFile(File originalFile, String expectedPath) throws OperationCancelledException,
+            IOException {
+        RemoteOperationResult result = null;
+
+        if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_COPY && !mOriginalStoragePath.equals(expectedPath)) {
+            String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath();
+            mFile.setStoragePath(temporalPath);
+            File temporalFile = new File(temporalPath);
+
+            result = copy(originalFile, temporalFile);
+        }
+
+        if (mCancellationRequested.get()) {
+            throw new OperationCancelledException();
+        }
+
+        return result;
+    }
+
+    private void checkNameCollision(OwnCloudClient client, DecryptedFolderMetadata metadata, boolean encrypted)
+            throws OperationCancelledException {
+        /// automatic rename of file to upload in case of name collision in server
+        Log_OC.d(TAG, "Checking name collision in server");
+        if (!mForceOverwrite) {
+            String remotePath = getAvailableRemotePath(client, mRemotePath, metadata, encrypted);
+            mWasRenamed = !remotePath.equals(mRemotePath);
+            if (mWasRenamed) {
+                createNewOCFile(remotePath);
+                Log_OC.d(TAG, "File renamed as " + remotePath);
+            }
+            mRemotePath = remotePath;
+            mRenameUploadListener.onRenameUpload();
+        }
 
+        if (mCancellationRequested.get()) {
+            throw new OperationCancelledException();
+        }
+    }
+
+    private void handleSuccessfulUpload(File temporalFile, File expectedFile, File originalFile,
+                                        OwnCloudClient client) {
+        switch (mLocalBehaviour) {
+            case FileUploader.LOCAL_BEHAVIOUR_FORGET:
+            default:
                 String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath();
                 if (mOriginalStoragePath.equals(temporalPath)) {
                     // delete local file is was pre-copied in temporary folder (see .ui.helpers.UriUploader)
@@ -558,37 +975,44 @@ public class UploadFileOperation extends SyncOperation {
                 }
                 mFile.setStoragePath("");
                 saveUploadedFile(client);
+                break;
 
+            case FileUploader.LOCAL_BEHAVIOUR_DELETE:
+                Log_OC.d(TAG, "Delete source file");
 
-            } else if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_DELETE) {
                 originalFile.delete();
                 getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
                 saveUploadedFile(client);
-            } else {
+                break;
 
-                if (temporalFile != null) {         // FileUploader.LOCAL_BEHAVIOUR_COPY
+            case FileUploader.LOCAL_BEHAVIOUR_COPY:
+                if (temporalFile != null) {
                     try {
                         move(temporalFile, expectedFile);
                     } catch (IOException e) {
-                        e.printStackTrace();
-                    }
-                } else {                            // FileUploader.LOCAL_BEHAVIOUR_MOVE
-                    try {
-                        move(originalFile, expectedFile);
-                    } catch (IOException e) {
-                        e.printStackTrace();
+                        Log_OC.e(TAG, e.getMessage());
                     }
-                    getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
                 }
                 mFile.setStoragePath(expectedFile.getAbsolutePath());
                 saveUploadedFile(client);
                 FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath());
-            }
-        } else if (result.getCode() == ResultCode.SYNC_CONFLICT) {
-            getStorageManager().saveConflict(mFile, mFile.getEtagInConflict());
-        }
+                break;
 
-        return result;
+            case FileUploader.LOCAL_BEHAVIOUR_MOVE:
+                String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile);
+                File newFile = new File(expectedPath);
+
+                try {
+                    move(originalFile, newFile);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+                getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
+                mFile.setStoragePath(newFile.getAbsolutePath());
+                saveUploadedFile(client);
+                FileDataStorageManager.triggerMediaScan(newFile.getAbsolutePath());
+                break;
+        }
     }
 
     /**
@@ -604,7 +1028,7 @@ public class UploadFileOperation extends SyncOperation {
      */
     private RemoteOperationResult grantFolderExistence(String pathToGrant, OwnCloudClient client) {
         RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, mContext, false);
-        RemoteOperationResult result = operation.execute(client);
+        RemoteOperationResult result = operation.execute(client, mFile.isEncrypted());
         if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND && mRemoteFolderToBeCreated) {
             SyncOperation syncOp = new CreateFolderOperation(pathToGrant, true);
             result = syncOp.execute(client, getStorageManager());
@@ -672,46 +1096,62 @@ public class UploadFileOperation extends SyncOperation {
      * Checks if remotePath does not exist in the server and returns it, or adds
      * a suffix to it in order to avoid the server file is overwritten.
      *
-     * @param wc
-     * @param remotePath
-     * @return
+     * @param client     OwnCloud client
+     * @param remotePath remote path of the file
+     * @param metadata   metadata of encrypted folder
+     * @return new remote path
      */
-    private String getAvailableRemotePath(OwnCloudClient wc, String remotePath) {
-        boolean check = existsFile(wc, remotePath);
+    private String getAvailableRemotePath(OwnCloudClient client, String remotePath, DecryptedFolderMetadata metadata,
+                                          boolean encrypted) {
+        boolean check = existsFile(client, remotePath, metadata, encrypted);
         if (!check) {
             return remotePath;
         }
 
         int pos = remotePath.lastIndexOf('.');
-        String suffix = "";
+        String suffix;
         String extension = "";
+        String remotePathWithoutExtension = "";
         if (pos >= 0) {
             extension = remotePath.substring(pos + 1);
-            remotePath = remotePath.substring(0, pos);
+            remotePathWithoutExtension = remotePath.substring(0, pos);
         }
         int count = 2;
         do {
             suffix = " (" + count + ")";
             if (pos >= 0) {
-                check = existsFile(wc, remotePath + suffix + "." + extension);
+                check = existsFile(client, remotePathWithoutExtension + suffix + "." + extension, metadata, encrypted);
             } else {
-                check = existsFile(wc, remotePath + suffix);
+                check = existsFile(client, remotePath + suffix, metadata, encrypted);
             }
             count++;
         } while (check);
 
         if (pos >= 0) {
-            return remotePath + suffix + "." + extension;
+            return remotePathWithoutExtension + suffix + "." + extension;
         } else {
             return remotePath + suffix;
         }
     }
 
-    private boolean existsFile(OwnCloudClient client, String remotePath) {
-        ExistenceCheckRemoteOperation existsOperation =
-                new ExistenceCheckRemoteOperation(remotePath, mContext, false);
-        RemoteOperationResult result = existsOperation.execute(client);
-        return result.isSuccess();
+    private boolean existsFile(OwnCloudClient client, String remotePath, DecryptedFolderMetadata metadata,
+                               boolean encrypted) {
+        if (encrypted) {
+            String fileName = new File(remotePath).getName();
+
+            for (DecryptedFolderMetadata.DecryptedFile file : metadata.getFiles().values()) {
+                if (file.getEncrypted().getFilename().equalsIgnoreCase(fileName)) {
+                    return true;
+                }
+            }
+
+            return false;
+        } else {
+            ExistenceCheckRemoteOperation existsOperation = new ExistenceCheckRemoteOperation(remotePath, mContext,
+                    false);
+            RemoteOperationResult result = existsOperation.execute(client);
+            return result.isSuccess();
+        }
     }
 
     /**
@@ -748,7 +1188,7 @@ public class UploadFileOperation extends SyncOperation {
      * @param sourceFile Source file to copy.
      * @param targetFile Target location to copy the file.
      * @return {@link RemoteOperationResult}
-     * @throws IOException
+     * @throws IOException exception if file cannot be accessed
      */
     private RemoteOperationResult copy(File sourceFile, File targetFile) throws IOException {
         Log_OC.d(TAG, "Copying local file");
@@ -833,13 +1273,13 @@ public class UploadFileOperation extends SyncOperation {
     /**
      * TODO rewrite with homogeneous fail handling, remove dependency on {@link RemoteOperationResult},
      * TODO     use Exceptions instead
-     *
+     * <p>
      * TODO refactor both this and 'copy' in a single method
      *
      * @param sourceFile Source file to move.
      * @param targetFile Target location to move the file.
-     * @return {@link RemoteOperationResult}
-     * @throws IOException
+     * @return {@link RemoteOperationResult} result from remote operation
+     * @throws IOException exception if file cannot be read/wrote
      */
     private void move(File sourceFile, File targetFile) throws IOException {
 
@@ -878,11 +1318,10 @@ public class UploadFileOperation extends SyncOperation {
 
     /**
      * Saves a OC File after a successful upload.
-     * <p/>
+     * <p>
      * A PROPFIND is necessary to keep the props in the local database
      * synchronized with the server, specially the modification time and Etag
      * (where available)
-     * <p/>
      */
     private void saveUploadedFile(OwnCloudClient client) {
         OCFile file = mFile;
@@ -896,8 +1335,15 @@ public class UploadFileOperation extends SyncOperation {
         // in theory, should return the same we already have
         // TODO from the appropriate OC server version, get data from last PUT response headers, instead
         // TODO     of a new PROPFIND; the latter may fail, specially for chunked uploads
-        ReadRemoteFileOperation operation = new ReadRemoteFileOperation(getRemotePath());
-        RemoteOperationResult result = operation.execute(client);
+        String path;
+        if (encryptedAncestor) {
+            path = file.getParentRemotePath() + mFile.getEncryptedFileName();
+        } else {
+            path = getRemotePath();
+        }
+
+        ReadRemoteFileOperation operation = new ReadRemoteFileOperation(path);
+        RemoteOperationResult result = operation.execute(client, mFile.isEncrypted());
         if (result.isSuccess()) {
             updateOCFile(file, (RemoteFile) result.getData().get(0));
             file.setLastSyncDateForProperties(syncDate);

+ 59 - 9
src/main/java/com/owncloud/android/providers/FileContentProvider.java

@@ -83,6 +83,7 @@ public class FileContentProvider extends ContentProvider {
     private static final String TEXT = " TEXT, ";
     private static final String ALTER_TABLE = "ALTER TABLE ";
     private static final String ADD_COLUMN = " ADD COLUMN ";
+    private static final String REMOVE_COLUMN = " REMOVE COLUMN ";
     private static final String UPGRADE_VERSION_MSG = "OUT of the ADD in onUpgrade; oldVersion == %d, newVersion == %d";
     private DataBaseHelper mDbHelper;
     private Context mContext;
@@ -716,6 +717,7 @@ public class FileContentProvider extends ContentProvider {
         db.execSQL("CREATE TABLE " + ProviderTableMeta.FILE_TABLE_NAME + "("
                 + ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "
                 + ProviderTableMeta.FILE_NAME + TEXT
+                + ProviderTableMeta.FILE_ENCRYPTED_NAME + TEXT
                 + ProviderTableMeta.FILE_PATH + TEXT
                 + ProviderTableMeta.FILE_PARENT + INTEGER
                 + ProviderTableMeta.FILE_CREATION + INTEGER
@@ -736,6 +738,7 @@ public class FileContentProvider extends ContentProvider {
                 + ProviderTableMeta.FILE_UPDATE_THUMBNAIL + INTEGER //boolean
                 + ProviderTableMeta.FILE_IS_DOWNLOADING + INTEGER //boolean
                 + ProviderTableMeta.FILE_FAVORITE + INTEGER // boolean
+                + ProviderTableMeta.FILE_IS_ENCRYPTED + INTEGER // boolean
                 + ProviderTableMeta.FILE_ETAG_IN_CONFLICT + TEXT
                 + ProviderTableMeta.FILE_SHARED_WITH_SHAREE + " INTEGER);"
         );
@@ -794,7 +797,8 @@ public class FileContentProvider extends ContentProvider {
                 + ProviderTableMeta.CAPABILITIES_SERVER_TEXT_COLOR + TEXT
                 + ProviderTableMeta.CAPABILITIES_SERVER_ELEMENT_COLOR + TEXT
                 + ProviderTableMeta.CAPABILITIES_SERVER_SLOGAN + TEXT
-                + ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_URL + " TEXT );");
+                + ProviderTableMeta.CAPABILITIES_SERVER_BACKGROUND_URL + TEXT
+                + ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION + " INTEGER );");
     }
 
     private void createUploadsTable(SQLiteDatabase db) {
@@ -814,8 +818,8 @@ public class FileContentProvider extends ContentProvider {
                 + ProviderTableMeta.UPLOADS_LAST_RESULT + INTEGER     // Upload LastResult
                 + ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY + INTEGER  // boolean
                 + ProviderTableMeta.UPLOADS_IS_WIFI_ONLY + INTEGER // boolean
-                + ProviderTableMeta.UPLOADS_CREATED_BY + " INTEGER );"    // Upload createdBy
-        );
+                + ProviderTableMeta.UPLOADS_CREATED_BY + INTEGER    // Upload createdBy
+                + ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN + " TEXT );");
 
 
         /* before:
@@ -1504,7 +1508,28 @@ public class FileContentProvider extends ContentProvider {
             }
 
             if (oldVersion < 25 && newVersion >= 25) {
-                Log_OC.i(SQL, "Entering in the #25 Adding text and element color to capabilities");
+                    Log_OC.i(SQL, "Entering in the #25 Adding encryption flag to file");
+                    db.beginTransaction();
+                    try {
+                        db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
+                                ADD_COLUMN + ProviderTableMeta.FILE_IS_ENCRYPTED + " INTEGER ");
+                        db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
+                                ADD_COLUMN + ProviderTableMeta.FILE_ENCRYPTED_NAME + " TEXT ");
+                        db.execSQL(ALTER_TABLE + ProviderTableMeta.CAPABILITIES_TABLE_NAME +
+                                ADD_COLUMN + ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION + " INTEGER ");
+                        upgraded = true;
+                        db.setTransactionSuccessful();
+                    } finally {
+                        db.endTransaction();
+                    }
+            }
+
+            if (!upgraded) {
+                Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
+            }
+
+            if (oldVersion < 26 && newVersion >= 26) {
+                Log_OC.i(SQL, "Entering in the #26 Adding text and element color to capabilities");
                 db.beginTransaction();
                 try {
                     db.execSQL(ALTER_TABLE + ProviderTableMeta.CAPABILITIES_TABLE_NAME +
@@ -1520,8 +1545,29 @@ public class FileContentProvider extends ContentProvider {
                 }
             }
 
-            if (oldVersion < 26 && newVersion >= 26) {
-                Log_OC.i(SQL, "Entering in the #26 Adding CRC32 column to filesystem table");
+            if (!upgraded) {
+                Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
+            }
+
+            if (oldVersion < 27 && newVersion >= 27) {
+                Log_OC.i(SQL, "Entering in the #27 Adding token to ocUpload");
+                db.beginTransaction();
+                try {
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.UPLOADS_TABLE_NAME +
+                            ADD_COLUMN + ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN + " TEXT ");
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+
+            if (!upgraded) {
+                Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
+            }
+
+            if (oldVersion < 28 && newVersion >= 28) {
+                Log_OC.i(SQL, "Entering in the #28 Adding CRC32 column to filesystem table");
                 db.beginTransaction();
                 try {
                     db.execSQL(ALTER_TABLE + ProviderTableMeta.FILESYSTEM_TABLE_NAME +
@@ -1534,17 +1580,21 @@ public class FileContentProvider extends ContentProvider {
                 }
             }
 
-
             if (!upgraded) {
                 Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
             }
+
         }
 
         @Override
         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             if (oldVersion == 25 && newVersion == 24) {
-                // nothing needs to be done as the upgrade was adding columns only if they did not exist
-                Log_OC.i(TAG, "Downgrading v" + oldVersion + " to " + newVersion);
+                db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
+                        REMOVE_COLUMN + ProviderTableMeta.FILE_IS_ENCRYPTED);
+                db.execSQL(ALTER_TABLE + ProviderTableMeta.FILE_TABLE_NAME +
+                        REMOVE_COLUMN + ProviderTableMeta.FILE_ENCRYPTED_NAME);
+                db.execSQL(ALTER_TABLE + ProviderTableMeta.CAPABILITIES_TABLE_NAME +
+                        REMOVE_COLUMN + ProviderTableMeta.CAPABILITIES_END_TO_END_ENCRYPTION);
             }
         }
     }

+ 6 - 10
src/main/java/com/owncloud/android/services/OperationsService.java

@@ -644,25 +644,21 @@ public class OperationsService extends Service {
                 } else if (action.equals(ACTION_REMOVE)) {
                     // Remove file or folder
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                    boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL,
-                            false);
-                    operation = new RemoveFileOperation(remotePath, onlyLocalCopy);
+                    boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false);
+                    operation = new RemoveFileOperation(remotePath, onlyLocalCopy, account, getApplicationContext());
                     
                 } else if (action.equals(ACTION_CREATE_FOLDER)) {
                     // Create Folder
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                    boolean createFullPath = operationIntent.getBooleanExtra(EXTRA_CREATE_FULL_PATH,
-                            true);
+                    boolean createFullPath = operationIntent.getBooleanExtra(EXTRA_CREATE_FULL_PATH, true);
                     operation = new CreateFolderOperation(remotePath, createFullPath);
 
                 } else if (action.equals(ACTION_SYNC_FILE)) {
                     // Sync file
                     String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
-                    boolean syncFileContents =
-                            operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true);
-                    operation = new SynchronizeFileOperation(
-                            remotePath, account, syncFileContents, getApplicationContext()
-                    );
+                    boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true);
+                    operation = new SynchronizeFileOperation(remotePath, account, syncFileContents,
+                            getApplicationContext());
                     
                 } else if (action.equals(ACTION_SYNC_FOLDER)) {
                     // Sync folder (all its descendant files are sync'ed)

+ 0 - 2
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -237,8 +237,6 @@ public class FileDisplayActivity extends HookActivity
             fm.beginTransaction()
                     .add(taskRetainerFragment, TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT).commit();
         }   // else, Fragment already created and retained across configuration change
-
-        Log_OC.v(TAG, "onCreate() end");
     }
 
     @Override

+ 1 - 1
src/main/java/com/owncloud/android/ui/adapter/ActivityListAdapter.java

@@ -260,7 +260,7 @@ public class ActivityListAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
             // Folder
             fileIcon.setImageDrawable(
                     MimeTypeUtil.getFolderTypeIcon(file.isSharedWithMe() || file.isSharedWithSharee(),
-                            file.isSharedViaLink()));
+                            file.isSharedViaLink(), file.isEncrypted()));
         }
     }
 

+ 24 - 1
src/main/java/com/owncloud/android/ui/adapter/FileListListAdapter.java

@@ -160,6 +160,29 @@ public class FileListListAdapter extends BaseAdapter {
         });
     }
 
+    public void setEncryptionAttributeForItemID(String fileId, boolean encrypted) {
+        for (int i = 0; i < mFiles.size(); i++) {
+            if (mFiles.get(i).getRemoteId().equals(fileId)) {
+                mFiles.get(i).setEncrypted(encrypted);
+                break;
+            }
+        }
+
+        for (int i = 0; i < mFilesAll.size(); i++) {
+            if (mFilesAll.get(i).getRemoteId().equals(fileId)) {
+                mFilesAll.get(i).setEncrypted(encrypted);
+                break;
+            }
+        }
+
+        new Handler(Looper.getMainLooper()).post(new Runnable() {
+            @Override
+            public void run() {
+                notifyDataSetChanged();
+            }
+        });
+    }
+
     @Override
     public long getItemId(int position) {
         if (mFiles == null || mFiles.size() <= position) {
@@ -394,7 +417,7 @@ public class FileListListAdapter extends BaseAdapter {
             } else {
                 // Folder
                 fileIcon.setImageDrawable(MimeTypeUtil.getFolderTypeIcon(file.isSharedWithMe() ||
-                        file.isSharedWithSharee(), file.isSharedViaLink()));
+                        file.isSharedWithSharee(), file.isSharedViaLink(), file.isEncrypted()));
             }
         }
         return view;

+ 1 - 1
src/main/java/com/owncloud/android/ui/adapter/UploaderAdapter.java

@@ -93,7 +93,7 @@ public class UploaderAdapter extends SimpleAdapter {
 
         if (file.isFolder()) {
             fileIcon.setImageDrawable(MimeTypeUtil.getFolderTypeIcon(file.isSharedWithMe() ||
-                    file.isSharedWithSharee(), file.isSharedViaLink(), mAccount));
+                    file.isSharedWithSharee(), file.isSharedViaLink(), file.isEncrypted(), mAccount));
         } else {
             // get Thumbnail if file is image
             if (MimeTypeUtil.isImage(file) && file.getRemoteId() != null) {

+ 408 - 0
src/main/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragment.java

@@ -0,0 +1,408 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2017 Tobias Kaminsky
+ * Copyright (C) 2017 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.dialog;
+
+import android.accounts.Account;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.design.widget.TextInputEditText;
+import android.support.design.widget.TextInputLayout;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v7.app.AlertDialog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.users.GetPrivateKeyOperation;
+import com.owncloud.android.lib.resources.users.GetPublicKeyOperation;
+import com.owncloud.android.lib.resources.users.SendCSROperation;
+import com.owncloud.android.lib.resources.users.StorePrivateKeyOperation;
+import com.owncloud.android.utils.CsrHelper;
+import com.owncloud.android.utils.EncryptionUtils;
+import com.owncloud.android.utils.ThemeUtils;
+
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/*
+ *  Dialog to setup encryption
+ */
+
+public class SetupEncryptionDialogFragment extends DialogFragment {
+
+    public static final String SUCCESS = "SUCCESS";
+    public static final int SETUP_ENCRYPTION_RESULT_CODE = 101;
+    public static final int SETUP_ENCRYPTION_REQUEST_CODE = 100;
+    public static String SETUP_ENCRYPTION_DIALOG_TAG = "SETUP_ENCRYPTION_DIALOG_TAG";
+    public static final String ARG_POSITION = "ARG_POSITION";
+
+    private static String ARG_ACCOUNT = "ARG_ACCOUNT";
+    private static String TAG = SetupEncryptionDialogFragment.class.getSimpleName();
+
+    private static final String KEY_CREATED = "KEY_CREATED";
+    private static final String KEY_EXISTING_USED = "KEY_EXISTING_USED";
+    private static final String KEY_FAILED = "KEY_FAILED";
+    private static final String KEY_GENERATE = "KEY_GENERATE";
+
+    private Account account;
+    private TextView textView;
+    private TextView passphraseTextView;
+    private ArbitraryDataProvider arbitraryDataProvider;
+    private Button positiveButton;
+    private Button negativeButton;
+    private TextInputLayout passwordLayout;
+    private DownloadKeysAsyncTask task;
+    private TextInputEditText passwordField;
+    private String keyResult;
+    private ArrayList<String> keyWords;
+
+    /**
+     * Public factory method to create new SetupEncryptionDialogFragment instance
+     *
+     * @return Dialog ready to show.
+     */
+    public static SetupEncryptionDialogFragment newInstance(Account account, int position) {
+        SetupEncryptionDialogFragment fragment = new SetupEncryptionDialogFragment();
+        Bundle args = new Bundle();
+        args.putParcelable(ARG_ACCOUNT, account);
+        args.putInt(ARG_POSITION, position);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        int color = ThemeUtils.primaryAccentColor();
+
+        AlertDialog alertDialog = (AlertDialog) getDialog();
+
+        positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
+        positiveButton.setTextColor(color);
+
+        negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
+        negativeButton.setTextColor(color);
+
+        task = new DownloadKeysAsyncTask();
+        task.execute();
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        int accentColor = ThemeUtils.primaryAccentColor();
+        account = getArguments().getParcelable(ARG_ACCOUNT);
+
+        arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
+
+        // Inflate the layout for the dialog
+        LayoutInflater inflater = getActivity().getLayoutInflater();
+
+        // Setup layout
+        View v = inflater.inflate(R.layout.setup_encryption_dialog, null);
+        textView = v.findViewById(R.id.encryption_status);
+        passphraseTextView = v.findViewById(R.id.encryption_passphrase);
+        passwordLayout = v.findViewById(R.id.encryption_passwordLayout);
+        passwordField = v.findViewById(R.id.encryption_passwordInput);
+        passwordField.getBackground().setColorFilter(accentColor, PorterDuff.Mode.SRC_ATOP);
+
+        Drawable wrappedDrawable = DrawableCompat.wrap(passwordField.getBackground());
+        DrawableCompat.setTint(wrappedDrawable, accentColor);
+        passwordField.setBackgroundDrawable(wrappedDrawable);
+
+        // Build the dialog  
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        builder.setView(v).setPositiveButton(R.string.common_ok, null)
+                .setNegativeButton(R.string.common_cancel, null)
+                .setTitle(ThemeUtils.getColoredTitle(getString(R.string.end_to_end_encryption_title), accentColor));
+
+        Dialog dialog = builder.create();
+        dialog.setCanceledOnTouchOutside(false);
+
+        dialog.setOnShowListener(new DialogInterface.OnShowListener() {
+
+            @Override
+            public void onShow(final DialogInterface dialog) {
+
+                Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
+                button.setOnClickListener(new View.OnClickListener() {
+
+                    @Override
+                    public void onClick(View view) {
+                        switch (keyResult) {
+                            case KEY_CREATED:
+                                Log_OC.d(TAG, "New keys generated and stored.");
+
+                                dialog.dismiss();
+
+                                Intent intentCreated = new Intent();
+                                intentCreated.putExtra(SUCCESS, true);
+                                intentCreated.putExtra(ARG_POSITION, getArguments().getInt(ARG_POSITION));
+                                getTargetFragment().onActivityResult(getTargetRequestCode(),
+                                        SETUP_ENCRYPTION_RESULT_CODE, intentCreated);
+                                break;
+
+                            case KEY_EXISTING_USED:
+                                Log_OC.d(TAG, "Decrypt private key");
+
+                                textView.setText(R.string.end_to_end_encryption_decrypting);
+
+                                try {
+                                    String privateKey = task.get();
+                                    String decryptedPrivateKey = EncryptionUtils.decryptPrivateKey(privateKey,
+                                            passwordField.getText().toString().replaceAll("\\s", "")
+                                                    .toLowerCase(Locale.ROOT));
+
+                                    arbitraryDataProvider.storeOrUpdateKeyValue(account.name,
+                                            EncryptionUtils.PRIVATE_KEY, decryptedPrivateKey);
+
+                                    dialog.dismiss();
+                                    Log_OC.d(TAG, "Private key successfully decrypted and stored");
+
+                                    Intent intentExisting = new Intent();
+                                    intentExisting.putExtra(SUCCESS, true);
+                                    intentExisting.putExtra(ARG_POSITION, getArguments().getInt(ARG_POSITION));
+                                    getTargetFragment().onActivityResult(getTargetRequestCode(),
+                                            SETUP_ENCRYPTION_RESULT_CODE, intentExisting);
+
+                                } catch (Exception e) {
+                                    textView.setText(R.string.end_to_end_encryption_wrong_password);
+                                    Log_OC.d(TAG, "Error while decrypting private key: " + e.getMessage());
+                                }
+                                break;
+
+                            case KEY_GENERATE:
+                                passphraseTextView.setVisibility(View.GONE);
+                                positiveButton.setVisibility(View.GONE);
+                                negativeButton.setVisibility(View.GONE);
+                                getDialog().setTitle(ThemeUtils.getColoredTitle(getString(
+                                        R.string.end_to_end_encryption_storing_keys), ThemeUtils.primaryColor()));
+
+                                GenerateNewKeysAsyncTask newKeysTask = new GenerateNewKeysAsyncTask();
+                                newKeysTask.execute();
+                                break;
+                            
+                            default:
+                                dialog.dismiss();
+                                break;
+                        }
+                    }
+                });
+            }
+        });
+
+        return dialog;
+    }
+
+    private class DownloadKeysAsyncTask extends AsyncTask<Void, Void, String> {
+        @Override
+        protected void onPreExecute() {
+            super.onPreExecute();
+
+            textView.setText(R.string.end_to_end_encryption_retrieving_keys);
+            positiveButton.setVisibility(View.INVISIBLE);
+            negativeButton.setVisibility(View.INVISIBLE);
+        }
+
+        @Override
+        protected String doInBackground(Void... voids) {
+            // fetch private/public key
+            // if available
+            //  - store public key
+            //  - decrypt private key, store unencrypted private key in database
+
+            GetPublicKeyOperation publicKeyOperation = new GetPublicKeyOperation();
+            RemoteOperationResult publicKeyResult = publicKeyOperation.execute(account, getContext(), true);
+
+            if (publicKeyResult.isSuccess()) {
+                Log_OC.d(TAG, "public key successful downloaded for " + account.name);
+
+                String publicKeyFromServer = (String) publicKeyResult.getData().get(0);
+                arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.PUBLIC_KEY,
+                        publicKeyFromServer);
+            } else {
+                return null;
+            }
+
+            GetPrivateKeyOperation privateKeyOperation = new GetPrivateKeyOperation();
+            RemoteOperationResult privateKeyResult = privateKeyOperation.execute(account, getContext(), true);
+
+            if (privateKeyResult.isSuccess()) {
+                Log_OC.d(TAG, "private key successful downloaded for " + account.name);
+
+                keyResult = KEY_EXISTING_USED;
+                return (String) privateKeyResult.getData().get(0);
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(String privateKey) {
+            super.onPostExecute(privateKey);
+
+            if (privateKey == null) {
+                // first show info
+                try {
+                    keyWords = EncryptionUtils.getRandomWords(12, getContext());
+
+                    getDialog().setTitle(ThemeUtils.getColoredTitle(
+                            getString(R.string.end_to_end_encryption_passphrase_title),
+                            ThemeUtils.primaryColor()));
+
+                    textView.setText(R.string.end_to_end_encryption_keywords_description);
+
+                    StringBuilder stringBuilder = new StringBuilder();
+
+                    for (String string : keyWords) {
+                        stringBuilder.append(string).append(" ");
+                    }
+                    String keys = stringBuilder.toString();
+
+                    passphraseTextView.setText(keys);
+
+                    passphraseTextView.setVisibility(View.VISIBLE);
+                    positiveButton.setText(R.string.end_to_end_encryption_confirm_button);
+                    positiveButton.setVisibility(View.VISIBLE);
+
+                    negativeButton.setVisibility(View.VISIBLE);
+
+                    keyResult = KEY_GENERATE;
+                } catch (IOException e) {
+                    textView.setText(R.string.common_error);
+                }
+            } else if (!privateKey.isEmpty()) {
+                textView.setText(R.string.end_to_end_encryption_enter_password);
+                passwordLayout.setVisibility(View.VISIBLE);
+                positiveButton.setVisibility(View.VISIBLE);
+            } else {
+                Log_OC.e(TAG, "Got empty private key string");
+            }
+        }
+    }
+
+    private class GenerateNewKeysAsyncTask extends AsyncTask<Void, Void, String> {
+        @Override
+        protected void onPreExecute() {
+            super.onPreExecute();
+
+            textView.setText(R.string.end_to_end_encryption_generating_keys);
+        }
+
+        @Override
+        protected String doInBackground(Void... voids) {
+            //  - create CSR, push to server, store returned public key in database
+            //  - encrypt private key, push key to server, store unencrypted private key in database
+
+            try {
+                String publicKey;
+
+                // Create public/private key pair
+                KeyPair keyPair = EncryptionUtils.generateKeyPair();
+                PrivateKey privateKey = keyPair.getPrivate();
+
+                // create CSR
+                String urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair, account.name);
+
+                SendCSROperation operation = new SendCSROperation(urlEncoded);
+                RemoteOperationResult result = operation.execute(account, getContext(), true);
+
+                if (result.isSuccess()) {
+                    Log_OC.d(TAG, "public key success");
+
+                    publicKey = (String) result.getData().get(0);
+                } else {
+                    keyResult = KEY_FAILED;
+                    return "";
+                }
+
+                StringBuilder stringBuilder = new StringBuilder();
+                for (String string : keyWords) {
+                    stringBuilder.append(string);
+                }
+                String keyPhrase = stringBuilder.toString();
+
+                String privateKeyString = EncryptionUtils.encodeBytesToBase64String(privateKey.getEncoded());
+                String privatePemKeyString = EncryptionUtils.privateKeyToPEM(privateKey);
+                String encryptedPrivateKey = EncryptionUtils.encryptPrivateKey(privatePemKeyString, keyPhrase);
+
+                // upload encryptedPrivateKey
+                StorePrivateKeyOperation storePrivateKeyOperation = new StorePrivateKeyOperation(encryptedPrivateKey);
+                RemoteOperationResult storePrivateKeyResult = storePrivateKeyOperation.execute(account, getContext(),
+                        true);
+
+                if (storePrivateKeyResult.isSuccess()) {
+                    Log_OC.d(TAG, "private key success");
+
+                    arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.PRIVATE_KEY,
+                            privateKeyString);
+                    arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.PUBLIC_KEY, publicKey);
+
+                    keyResult = KEY_CREATED;
+                    return (String) storePrivateKeyResult.getData().get(0);
+                }
+
+            } catch (Exception e) {
+                Log_OC.e(TAG, e.getMessage());
+            }
+
+            keyResult = KEY_FAILED;
+            return "";
+        }
+
+        @Override
+        protected void onPostExecute(String s) {
+            super.onPostExecute(s);
+
+            if (s.isEmpty()) {
+                keyResult = KEY_FAILED;
+
+                getDialog().setTitle(ThemeUtils.getColoredTitle(
+                        getString(R.string.common_error), ThemeUtils.primaryColor()));
+                textView.setText(R.string.end_to_end_encryption_unsuccessful);
+                positiveButton.setText(R.string.end_to_end_encryption_dialog_close);
+                positiveButton.setVisibility(View.VISIBLE);
+            } else {
+                getDialog().dismiss();
+
+                Intent intentExisting = new Intent();
+                intentExisting.putExtra(SUCCESS, true);
+                intentExisting.putExtra(ARG_POSITION, getArguments().getInt(ARG_POSITION));
+                getTargetFragment().onActivityResult(getTargetRequestCode(),
+                        SETUP_ENCRYPTION_RESULT_CODE, intentExisting);
+            }
+        }
+    }
+}

+ 37 - 0
src/main/java/com/owncloud/android/ui/events/EncryptionEvent.java

@@ -0,0 +1,37 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2017 Tobias Kaminsky
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.ui.events;
+
+/**
+ * Event for set folder as encrypted/decrypted
+ */
+public class EncryptionEvent {
+    public final String localId;
+    public final String remotePath;
+    public final String remoteId;
+    public final boolean shouldBeEncrypted;
+
+    public EncryptionEvent(String localId, String remoteId, String remotePath, boolean shouldBeEncrypted) {
+        this.localId = localId;
+        this.remoteId = remoteId;
+        this.remotePath = remotePath;
+        this.shouldBeEncrypted = shouldBeEncrypted;
+    }
+}

+ 110 - 8
src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -58,6 +58,7 @@ import android.widget.TextView;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.VirtualFolderType;
@@ -70,6 +71,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.SearchOperation;
+import com.owncloud.android.lib.resources.files.ToggleEncryptionOperation;
 import com.owncloud.android.lib.resources.files.ToggleFavoriteOperation;
 import com.owncloud.android.lib.resources.shares.GetRemoteSharesOperation;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
@@ -83,8 +85,10 @@ import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
 import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
 import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
 import com.owncloud.android.ui.dialog.RenameFileDialogFragment;
+import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment;
 import com.owncloud.android.ui.events.ChangeMenuEvent;
 import com.owncloud.android.ui.events.DummyDrawerEvent;
+import com.owncloud.android.ui.events.EncryptionEvent;
 import com.owncloud.android.ui.events.FavoriteEvent;
 import com.owncloud.android.ui.events.SearchEvent;
 import com.owncloud.android.ui.helpers.SparseBooleanArrayParcelable;
@@ -94,10 +98,12 @@ import com.owncloud.android.ui.preview.PreviewMediaFragment;
 import com.owncloud.android.ui.preview.PreviewTextFragment;
 import com.owncloud.android.utils.AnalyticsUtils;
 import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.EncryptionUtils;
 import com.owncloud.android.utils.FileSortOrder;
 import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.ThemeUtils;
 
+import org.apache.commons.httpclient.HttpStatus;
 import org.greenrobot.eventbus.EventBus;
 import org.greenrobot.eventbus.Subscribe;
 import org.greenrobot.eventbus.ThreadMode;
@@ -873,13 +879,47 @@ public class OCFileListFragment extends ExtendedListFragment implements OCFileLi
 
         if (file != null) {
             if (file.isFolder()) {
-                // update state and view of this fragment
-                searchFragment = false;
-                listDirectory(file, MainApp.isOnlyOnDevice(), false);
-                // then, notify parent activity to let it update its state and view
-                mContainerActivity.onBrowsedDownTo(file);
-                // save index and top position
-                saveIndexAndTopPosition(position);
+                if (file.isEncrypted()) {
+                    // check if API >= 19
+                    if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) {
+                        Snackbar.make(mCurrentListView, R.string.end_to_end_encryption_not_supported,
+                                Snackbar.LENGTH_LONG).show();
+                        return;
+                    }
+
+                    // check if keys are stored
+                    ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(
+                            getContext().getContentResolver());
+
+                    Account account = ((FileActivity) mContainerActivity).getAccount();
+                    String publicKey = arbitraryDataProvider.getValue(account, EncryptionUtils.PUBLIC_KEY);
+                    String privateKey = arbitraryDataProvider.getValue(account, EncryptionUtils.PRIVATE_KEY);
+
+                    if (publicKey.isEmpty() || privateKey.isEmpty()) {
+                        Log_OC.d(TAG, "no public key for " + account.name);
+
+                        SetupEncryptionDialogFragment dialog = SetupEncryptionDialogFragment.newInstance(account,
+                                position);
+                        dialog.setTargetFragment(this, SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE);
+                        dialog.show(getFragmentManager(), SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG);
+                    } else {
+                        // update state and view of this fragment
+                        searchFragment = false;
+                        listDirectory(file, MainApp.isOnlyOnDevice(), false);
+                        // then, notify parent activity to let it update its state and view
+                        mContainerActivity.onBrowsedDownTo(file);
+                        // save index and top position
+                        saveIndexAndTopPosition(position);
+                    }
+                } else {
+                    // update state and view of this fragment
+                    searchFragment = false;
+                    listDirectory(file, MainApp.isOnlyOnDevice(), false);
+                    // then, notify parent activity to let it update its state and view
+                    mContainerActivity.onBrowsedDownTo(file);
+                    // save index and top position
+                    saveIndexAndTopPosition(position);
+                }
 
             } else { /// Click on a file
                 if (PreviewImageFragment.canBePreviewed(file)) {
@@ -927,6 +967,27 @@ public class OCFileListFragment extends ExtendedListFragment implements OCFileLi
 
     }
 
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == SetupEncryptionDialogFragment.SETUP_ENCRYPTION_REQUEST_CODE &&
+                resultCode == SetupEncryptionDialogFragment.SETUP_ENCRYPTION_RESULT_CODE &&
+                data.getBooleanExtra(SetupEncryptionDialogFragment.SUCCESS, false)) {
+
+            int position = data.getIntExtra(SetupEncryptionDialogFragment.ARG_POSITION, -1);
+            OCFile file = (OCFile) mAdapter.getItem(position);
+
+            // update state and view of this fragment
+            searchFragment = false;
+            listDirectory(file, MainApp.isOnlyOnDevice(), false);
+            // then, notify parent activity to let it update its state and view
+            mContainerActivity.onBrowsedDownTo(file);
+            // save index and top position
+            saveIndexAndTopPosition(position);
+        } else {
+            super.onActivityResult(requestCode, resultCode, data);
+        }
+    }
+
     /**
      * Start the appropriate action(s) on the currently selected files given menu selected by the user.
      *
@@ -967,6 +1028,14 @@ public class OCFileListFragment extends ExtendedListFragment implements OCFileLi
                     mContainerActivity.getFileOperationsHelper().setPictureAs(singleFile, getView());
                     return true;
                 }
+                case R.id. action_encrypted: {
+                    mContainerActivity.getFileOperationsHelper().toggleEncryption(singleFile, true);
+                    return true;
+                }
+                case R.id. action_unset_encrypted: {
+                    mContainerActivity.getFileOperationsHelper().toggleEncryption(singleFile, false);
+                    return true;
+                }
             }
         }
 
@@ -1112,7 +1181,6 @@ public class OCFileListFragment extends ExtendedListFragment implements OCFileLi
                 mFile = directory;
 
                 updateLayout();
-
             }
         }
     }
@@ -1504,6 +1572,40 @@ public class OCFileListFragment extends ExtendedListFragment implements OCFileLi
         remoteOperationAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, true);
     }
 
+    @Subscribe(threadMode = ThreadMode.BACKGROUND)
+    public void onMessageEvent(EncryptionEvent event) {
+        Account currentAccount = AccountUtils.getCurrentOwnCloudAccount(MainApp.getAppContext());
+
+        OwnCloudAccount ocAccount = null;
+        try {
+            ocAccount = new OwnCloudAccount(currentAccount, MainApp.getAppContext());
+
+            OwnCloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton().
+                    getClientFor(ocAccount, MainApp.getAppContext());
+
+            ToggleEncryptionOperation toggleEncryptionOperation = new ToggleEncryptionOperation(event.localId,
+                    event.remotePath, event.shouldBeEncrypted);
+            RemoteOperationResult remoteOperationResult = toggleEncryptionOperation.execute(mClient, true);
+
+            if (remoteOperationResult.isSuccess()) {
+                mAdapter.setEncryptionAttributeForItemID(event.remoteId, event.shouldBeEncrypted);
+            } else if (remoteOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
+                Snackbar.make(mCurrentListView, R.string.end_to_end_encryption_folder_not_empty, Snackbar.LENGTH_LONG).show();
+            } else {
+                Snackbar.make(mCurrentListView, R.string.common_error_unknown, Snackbar.LENGTH_LONG).show();
+            }
+
+        } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
+            Log_OC.e(TAG, "Account not found", e);
+        } catch (AuthenticatorException e) {
+            Log_OC.e(TAG, "Authentication failed", e);
+        } catch (IOException e) {
+            Log_OC.e(TAG, "IO error", e);
+        } catch (OperationCanceledException e) {
+            Log_OC.e(TAG, "Operation has been canceled", e);
+        }
+    }
+
     private void setTitle(@StringRes final int title) {
         getActivity().runOnUiThread(new Runnable() {
             @Override

+ 7 - 0
src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java

@@ -57,6 +57,7 @@ import com.owncloud.android.ui.activity.ConflictsResolveActivity;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.ShareActivity;
 import com.owncloud.android.ui.dialog.SendShareDialog;
+import com.owncloud.android.ui.events.EncryptionEvent;
 import com.owncloud.android.ui.events.FavoriteEvent;
 import com.owncloud.android.ui.events.SyncEventFinished;
 import com.owncloud.android.utils.DisplayUtils;
@@ -678,6 +679,12 @@ public class FileOperationsHelper {
         }
     }
 
+    public void toggleEncryption(OCFile file, boolean shouldBeEncrypted) {
+        if (file.isEncrypted() != shouldBeEncrypted) {
+            EventBus.getDefault().post(new EncryptionEvent(file.getLocalId(), file.getRemoteId(), file.getRemotePath(),
+                    shouldBeEncrypted));
+        }
+    }
 
     public void toggleOfflineFiles(Collection<OCFile> files, boolean isAvailableOffline) {
         List<OCFile> alreadyRightStateList = new ArrayList<>();

+ 12 - 2
src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java

@@ -138,9 +138,14 @@ public class PreviewImageActivity extends FileActivity implements
             mPreviewImagePagerAdapter = new PreviewImagePagerAdapter(getSupportFragmentManager(),
                     type, getAccount(), getStorageManager());
         } else {
+            String filename;
+            if (getFile().isEncrypted()) {
+                filename = getFile().getEncryptedFileName();
+            } else {
+                filename = getFile().getFileName();
+            }
             // get parent from path
-            String parentPath = getFile().getRemotePath().substring(0,
-                    getFile().getRemotePath().lastIndexOf(getFile().getFileName()));
+            String parentPath = getFile().getRemotePath().substring(0, getFile().getRemotePath().lastIndexOf(filename));
             OCFile parentFolder = getStorageManager().getFileByPath(parentPath);
 
             if (parentFolder == null) {
@@ -353,6 +358,11 @@ public class PreviewImageActivity extends FileActivity implements
             }
             setDrawerIndicatorEnabled(false);
 
+            if (currentFile.isEncrypted() && !currentFile.isDown() &&
+                    !mPreviewImagePagerAdapter.pendingErrorAt(position)) {
+                requestForDownload(currentFile);
+            }
+
             // Call to reset image zoom to initial state
             ((PreviewImagePagerAdapter) mViewPager.getAdapter()).resetZoom();
         }

+ 10 - 1
src/main/java/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java

@@ -151,7 +151,11 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
                 ((FileDownloadFragment) fragment).setError(true);
                 mDownloadErrors.remove(i);
             } else {
-                fragment = PreviewImageFragment.newInstance(file, mObsoletePositions.contains(i), true);
+                if (file.isEncrypted()) {
+                    fragment = FileDownloadFragment.newInstance(file, mAccount, mObsoletePositions.contains(i));
+                } else {
+                    fragment = PreviewImageFragment.newInstance(file, mObsoletePositions.contains(i), true);
+                }
             }
         }
 
@@ -214,6 +218,11 @@ public class PreviewImagePagerAdapter extends FragmentStatePagerAdapter {
        super.destroyItem(container, position, object);
     }
 
+
+    public boolean pendingErrorAt(int position) {
+        return mDownloadErrors.contains(position);
+    }
+
     /**
      * Reset the image zoom to default value for each CachedFragments
      */

+ 2 - 2
src/main/java/com/owncloud/android/utils/ConnectivityUtils.java

@@ -91,9 +91,9 @@ public class ConnectivityUtils {
             } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
                 Log_OC.e(TAG, "Account not found", e);
             } catch (OperationCanceledException e) {
-                e.printStackTrace();
+                Log_OC.e(TAG, e.getMessage());
             } catch (AuthenticatorException e) {
-                e.printStackTrace();
+                Log_OC.e(TAG, e.getMessage());
             }
         }
 

+ 75 - 0
src/main/java/com/owncloud/android/utils/CsrHelper.java

@@ -0,0 +1,75 @@
+package com.owncloud.android.utils;
+
+import org.spongycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.spongycastle.asn1.x500.X500Name;
+import org.spongycastle.asn1.x509.AlgorithmIdentifier;
+import org.spongycastle.asn1.x509.BasicConstraints;
+import org.spongycastle.asn1.x509.Extension;
+import org.spongycastle.asn1.x509.ExtensionsGenerator;
+import org.spongycastle.crypto.params.AsymmetricKeyParameter;
+import org.spongycastle.crypto.util.PrivateKeyFactory;
+import org.spongycastle.operator.ContentSigner;
+import org.spongycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
+import org.spongycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
+import org.spongycastle.operator.OperatorCreationException;
+import org.spongycastle.operator.bc.BcRSAContentSignerBuilder;
+import org.spongycastle.pkcs.PKCS10CertificationRequest;
+import org.spongycastle.pkcs.PKCS10CertificationRequestBuilder;
+import org.spongycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
+
+import java.io.IOException;
+import java.security.KeyPair;
+
+/**
+ * copied & modified from:
+ * https://github.com/awslabs/aws-sdk-android-samples/blob/master/CreateIotCertWithCSR/src/com/amazonaws/demo/csrcert/CsrHelper.java
+ * accessed at 31.08.17
+ * Original parts are licensed under the Apache License, Version 2.0: http://aws.amazon.com/apache2.0
+ * Own parts are licensed unter GPLv3+.
+ */
+
+public class CsrHelper {
+
+    /**
+     * Generate CSR with PEM encoding
+     *
+     * @param keyPair the KeyPair with private and public keys
+     * @param userId  userId of CSR owner
+     * @return PEM encoded CSR string
+     * @throws IOException               thrown if key cannot be created
+     * @throws OperatorCreationException thrown if contentSigner cannot be build
+     */
+    public static String generateCsrPemEncodedString(KeyPair keyPair, String userId)
+            throws IOException, OperatorCreationException {
+        PKCS10CertificationRequest csr = CsrHelper.generateCSR(keyPair, userId);
+        byte[] derCSR = csr.getEncoded();
+        return "-----BEGIN CERTIFICATE REQUEST-----\n" + android.util.Base64.encodeToString(derCSR,
+                android.util.Base64.NO_WRAP) + "\n-----END CERTIFICATE REQUEST-----";
+    }
+    
+    /**
+     * Create the certificate signing request (CSR) from private and public keys
+     *
+     * @param keyPair the KeyPair with private and public keys
+     * @param userId userId of CSR owner
+     * @return PKCS10CertificationRequest with the certificate signing request (CSR) data
+     * @throws IOException thrown if key cannot be created
+     * @throws OperatorCreationException thrown if contentSigner cannot be build
+     */
+    private static PKCS10CertificationRequest generateCSR(KeyPair keyPair, String userId) throws IOException,
+    OperatorCreationException {
+        String principal = "CN=" + userId.split("@")[0];
+        AsymmetricKeyParameter privateKey = PrivateKeyFactory.createKey(keyPair.getPrivate().getEncoded());
+        AlgorithmIdentifier signatureAlgorithm = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1WITHRSA");
+        AlgorithmIdentifier digestAlgorithm = new DefaultDigestAlgorithmIdentifierFinder().find("SHA-1");
+        ContentSigner signer = new BcRSAContentSignerBuilder(signatureAlgorithm, digestAlgorithm).build(privateKey);
+
+        PKCS10CertificationRequestBuilder csrBuilder = new JcaPKCS10CertificationRequestBuilder(new X500Name(principal),
+                keyPair.getPublic());
+        ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator();
+        extensionsGenerator.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
+        csrBuilder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate());
+
+        return csrBuilder.build(signer);
+    }
+}

+ 643 - 0
src/main/java/com/owncloud/android/utils/EncryptionUtils.java

@@ -0,0 +1,643 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2017 Tobias Kaminsky
+ * Copyright (C) 2017 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.utils;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.util.Base64;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.datamodel.DecryptedFolderMetadata;
+import com.owncloud.android.datamodel.EncryptedFolderMetadata;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.lib.resources.files.GetMetadataOperation;
+
+import org.apache.commons.codec.binary.Hex;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.RandomAccessFile;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Utils for encryption
+ */
+
+public class EncryptionUtils {
+    private static String TAG = EncryptionUtils.class.getSimpleName();
+
+    public static final String PUBLIC_KEY = "PUBLIC_KEY";
+    public static final String PRIVATE_KEY = "PRIVATE_KEY";
+    public static final int ivLength = 16;
+    public static final int saltLength = 40;
+
+    private static final String ivDelimiter = "fA=="; // "|" base64 encoded
+    private static final int iterationCount = 1024;
+    private static final int keyStrength = 256;
+    private static final String AES_CIPHER = "AES/GCM/NoPadding";
+    private static final String AES = "AES";
+    private static final String RSA_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
+    private static final String RSA = "RSA";
+
+    /*
+    JSON
+     */
+
+    public static <T> T deserializeJSON(String json, TypeToken<T> type) {
+        return new Gson().fromJson(json, type.getType());
+    }
+
+    public static String serializeJSON(Object data) {
+        return new Gson().toJson(data);
+    }
+
+    /*
+    METADATA
+     */
+
+    /**
+     * Encrypt folder metaData
+     *
+     * @param decryptedFolderMetadata folder metaData to encrypt
+     * @return EncryptedFolderMetadata encrypted folder metadata
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static EncryptedFolderMetadata encryptFolderMetadata(DecryptedFolderMetadata decryptedFolderMetadata,
+                                                                String privateKey)
+            throws IOException, NoSuchAlgorithmException, ShortBufferException, InvalidKeyException,
+            InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
+            NoSuchProviderException, IllegalBlockSizeException, InvalidKeySpecException, CertificateException {
+
+        HashMap<String, EncryptedFolderMetadata.EncryptedFile> files = new HashMap<>();
+        EncryptedFolderMetadata encryptedFolderMetadata = new EncryptedFolderMetadata(decryptedFolderMetadata
+                .getMetadata(), files);
+
+        // Encrypt each file in "files"
+        for (Map.Entry<String, DecryptedFolderMetadata.DecryptedFile> entry : decryptedFolderMetadata
+                .getFiles().entrySet()) {
+            String key = entry.getKey();
+            DecryptedFolderMetadata.DecryptedFile decryptedFile = entry.getValue();
+
+            EncryptedFolderMetadata.EncryptedFile encryptedFile = new EncryptedFolderMetadata.EncryptedFile();
+            encryptedFile.setInitializationVector(decryptedFile.getInitializationVector());
+            encryptedFile.setMetadataKey(decryptedFile.getMetadataKey());
+            encryptedFile.setAuthenticationTag(decryptedFile.getAuthenticationTag());
+
+            byte[] decryptedMetadataKey = EncryptionUtils.decodeStringToBase64Bytes(EncryptionUtils.decryptStringAsymmetric(
+                    decryptedFolderMetadata.getMetadata().getMetadataKeys().get(encryptedFile.getMetadataKey()),
+                    privateKey));
+
+            // encrypt
+            String dataJson = EncryptionUtils.serializeJSON(decryptedFile.getEncrypted());
+            encryptedFile.setEncrypted(EncryptionUtils.encryptStringSymmetric(dataJson, decryptedMetadataKey));
+
+            files.put(key, encryptedFile);
+        }
+
+        return encryptedFolderMetadata;
+    }
+
+    /*
+     * decrypt folder metaData with private key
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static DecryptedFolderMetadata decryptFolderMetaData(EncryptedFolderMetadata encryptedFolderMetadata,
+                                                                String privateKey)
+            throws IOException, NoSuchAlgorithmException, ShortBufferException, InvalidKeyException,
+            InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
+            NoSuchProviderException, IllegalBlockSizeException, CertificateException, InvalidKeySpecException {
+
+        HashMap<String, DecryptedFolderMetadata.DecryptedFile> files = new HashMap<>();
+        DecryptedFolderMetadata decryptedFolderMetadata = new DecryptedFolderMetadata(
+                encryptedFolderMetadata.getMetadata(), files);
+
+        for (Map.Entry<String, EncryptedFolderMetadata.EncryptedFile> entry : encryptedFolderMetadata
+                .getFiles().entrySet()) {
+            String key = entry.getKey();
+            EncryptedFolderMetadata.EncryptedFile encryptedFile = entry.getValue();
+
+            DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
+            decryptedFile.setInitializationVector(encryptedFile.getInitializationVector());
+            decryptedFile.setMetadataKey(encryptedFile.getMetadataKey());
+            decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag());
+
+            byte[] decryptedMetadataKey = EncryptionUtils.decodeStringToBase64Bytes(
+                    EncryptionUtils.decryptStringAsymmetric(decryptedFolderMetadata.getMetadata()
+                            .getMetadataKeys().get(encryptedFile.getMetadataKey()), privateKey));
+
+            // decrypt
+            String dataJson = EncryptionUtils.decryptStringSymmetric(encryptedFile.getEncrypted(), decryptedMetadataKey);
+            decryptedFile.setEncrypted(EncryptionUtils.deserializeJSON(dataJson,
+                    new TypeToken<DecryptedFolderMetadata.Data>() {
+                    }));
+
+            files.put(key, decryptedFile);
+        }
+
+        return decryptedFolderMetadata;
+    }
+
+    /**
+     * Download metadata for folder and decrypt it
+     *
+     * @return decrypted metadata or null
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static @Nullable
+    DecryptedFolderMetadata downloadFolderMetadata(OCFile folder, OwnCloudClient client,
+                                                   Context context, Account account) {
+        GetMetadataOperation getMetadataOperation = new GetMetadataOperation(folder.getLocalId());
+        RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client, true);
+
+        if (!getMetadataOperationResult.isSuccess()) {
+            return null;
+        }
+
+        // decrypt metadata
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
+        String serializedEncryptedMetadata = (String) getMetadataOperationResult.getData().get(0);
+        String privateKey = arbitraryDataProvider.getValue(account.name, EncryptionUtils.PRIVATE_KEY);
+
+        EncryptedFolderMetadata encryptedFolderMetadata = EncryptionUtils.deserializeJSON(
+                serializedEncryptedMetadata, new TypeToken<EncryptedFolderMetadata>() {
+                });
+
+        try {
+            return EncryptionUtils.decryptFolderMetaData(encryptedFolderMetadata, privateKey);
+        } catch (Exception e) {
+            Log_OC.e(TAG, e.getMessage());
+            return null;
+        }
+    }
+
+    /*
+    BASE 64
+     */
+
+    public static byte[] encodeStringToBase64Bytes(String string) {
+        try {
+            return Base64.encode(string.getBytes(), Base64.NO_WRAP);
+        } catch (Exception e) {
+            return new byte[0];
+        }
+    }
+
+    public static String decodeBase64BytesToString(byte[] bytes) {
+        try {
+            return new String(Base64.decode(bytes, Base64.NO_WRAP));
+        } catch (Exception e) {
+            return "";
+        }
+    }
+
+    public static String encodeBytesToBase64String(byte[] bytes) {
+        return Base64.encodeToString(bytes, Base64.NO_WRAP);
+    }
+
+    public static byte[] decodeStringToBase64Bytes(String string) {
+        return Base64.decode(string, Base64.NO_WRAP);
+    }
+
+    /*
+    ENCRYPTION
+     */
+
+    /**
+     * @param ocFile             file do crypt
+     * @param encryptionKeyBytes key, either from metadata or {@link EncryptionUtils#generateKey()}
+     * @param iv                 initialization vector, either from metadata or {@link EncryptionUtils#randomBytes(int)}
+     * @return encryptedFile with encryptedBytes and authenticationTag
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static EncryptedFile encryptFile(OCFile ocFile, byte[] encryptionKeyBytes, byte[] iv)
+            throws NoSuchProviderException, NoSuchAlgorithmException,
+            InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
+            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException {
+        File file = new File(ocFile.getStoragePath());
+
+        return encryptFile(file, encryptionKeyBytes, iv);
+    }
+
+    /**
+     * @param file               file do crypt
+     * @param encryptionKeyBytes key, either from metadata or {@link EncryptionUtils#generateKey()}
+     * @param iv                 initialization vector, either from metadata or {@link EncryptionUtils#randomBytes(int)}
+     * @return encryptedFile with encryptedBytes and authenticationTag
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static EncryptedFile encryptFile(File file, byte[] encryptionKeyBytes, byte[] iv)
+            throws NoSuchProviderException, NoSuchAlgorithmException,
+            InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
+            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException {
+
+        Cipher cipher = Cipher.getInstance(AES_CIPHER);
+
+        Key key = new SecretKeySpec(encryptionKeyBytes, AES);
+
+        GCMParameterSpec spec = new GCMParameterSpec(128, iv);
+        cipher.init(Cipher.ENCRYPT_MODE, key, spec);
+
+        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
+        byte[] fileBytes = new byte[(int) randomAccessFile.length()];
+        randomAccessFile.readFully(fileBytes);
+
+        byte[] cryptedBytes = cipher.doFinal(fileBytes);
+        String authenticationTag = encodeBytesToBase64String(Arrays.copyOfRange(cryptedBytes,
+                cryptedBytes.length - (128 / 8), cryptedBytes.length));
+
+        return new EncryptedFile(cryptedBytes, authenticationTag);
+    }
+
+    /**
+     * @param file               encrypted file
+     * @param encryptionKeyBytes key from metadata
+     * @param iv                 initialization vector from metadata
+     * @param authenticationTag  authenticationTag from metadata
+     * @return decrypted byte[]
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static byte[] decryptFile(File file, byte[] encryptionKeyBytes, byte[] iv, byte[] authenticationTag)
+            throws NoSuchProviderException, NoSuchAlgorithmException,
+            InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
+            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException {
+
+
+        Cipher cipher = Cipher.getInstance(AES_CIPHER);
+        Key key = new SecretKeySpec(encryptionKeyBytes, AES);
+        GCMParameterSpec spec = new GCMParameterSpec(128, iv);
+        cipher.init(Cipher.DECRYPT_MODE, key, spec);
+
+        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
+        byte[] fileBytes = new byte[(int) randomAccessFile.length()];
+        randomAccessFile.readFully(fileBytes);
+
+        // check authentication tag
+        byte[] extractedAuthenticationTag = Arrays.copyOfRange(fileBytes,
+                fileBytes.length - (128 / 8), fileBytes.length);
+
+        if (!Arrays.equals(extractedAuthenticationTag, authenticationTag)) {
+            throw new SecurityException("Tag not correct");
+        }
+
+        return cipher.doFinal(fileBytes);
+    }
+
+    public static class EncryptedFile {
+        public byte[] encryptedBytes;
+        public String authenticationTag;
+
+        public EncryptedFile(byte[] encryptedBytes, String authenticationTag) {
+            this.encryptedBytes = encryptedBytes;
+            this.authenticationTag = authenticationTag;
+        }
+    }
+
+    /**
+     * Encrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding
+     * Asymmetric encryption, with private and public key
+     *
+     * @param string String to encrypt
+     * @param cert   contains public key in it
+     * @return encrypted string
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static String encryptStringAsymmetric(String string, String cert)
+            throws NoSuchProviderException, NoSuchAlgorithmException,
+            InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
+            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, InvalidKeySpecException,
+            CertificateException {
+
+        Cipher cipher = Cipher.getInstance(RSA_CIPHER);
+
+        String trimmedCert = cert.replace("-----BEGIN CERTIFICATE-----\n", "")
+                .replace("-----END CERTIFICATE-----\n", "");
+        byte[] encodedCert = trimmedCert.getBytes("UTF-8");
+        byte[] decodedCert = org.apache.commons.codec.binary.Base64.decodeBase64(encodedCert);
+
+        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+        InputStream in = new ByteArrayInputStream(decodedCert);
+        X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(in);
+        PublicKey realPublicKey = certificate.getPublicKey();
+
+        cipher.init(Cipher.ENCRYPT_MODE, realPublicKey);
+
+        byte[] bytes = encodeStringToBase64Bytes(string);
+        byte[] cryptedBytes = cipher.doFinal(bytes);
+
+        return encodeBytesToBase64String(cryptedBytes);
+    }
+
+
+    /**
+     * Decrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding
+     * Asymmetric encryption, with private and public key
+     *
+     * @param string           string to decrypt
+     * @param privateKeyString private key
+     * @return decrypted string
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static String decryptStringAsymmetric(String string, String privateKeyString)
+            throws NoSuchProviderException, NoSuchAlgorithmException,
+            InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
+            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, CertificateException,
+            InvalidKeySpecException {
+
+        Cipher cipher = Cipher.getInstance(RSA_CIPHER);
+
+        byte[] privateKeyBytes = decodeStringToBase64Bytes(privateKeyString);
+        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
+        KeyFactory kf = KeyFactory.getInstance(RSA);
+        PrivateKey privateKey = kf.generatePrivate(keySpec);
+
+        cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+        byte[] bytes = decodeStringToBase64Bytes(string);
+        byte[] encodedBytes = cipher.doFinal(bytes);
+
+        return decodeBase64BytesToString(encodedBytes);
+    }
+
+
+    /**
+     * Encrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding
+     * Asymmetric encryption, with private and public key
+     *
+     * @param string             String to encrypt
+     * @param encryptionKeyBytes key, either from metadata or {@link EncryptionUtils#generateKey()}
+     * @return encrypted string
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static String encryptStringSymmetric(String string, byte[] encryptionKeyBytes)
+            throws NoSuchProviderException, NoSuchAlgorithmException,
+            InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
+            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, InvalidKeySpecException,
+            CertificateException {
+
+        Cipher cipher = Cipher.getInstance(AES_CIPHER);
+        byte[] iv = randomBytes(ivLength);
+
+        Key key = new SecretKeySpec(encryptionKeyBytes, AES);
+        GCMParameterSpec spec = new GCMParameterSpec(128, iv);
+        cipher.init(Cipher.ENCRYPT_MODE, key, spec);
+
+        byte[] bytes = encodeStringToBase64Bytes(string);
+        byte[] cryptedBytes = cipher.doFinal(bytes);
+
+        String encodedCryptedBytes = encodeBytesToBase64String(cryptedBytes);
+        String encodedIV = encodeBytesToBase64String(iv);
+
+        return encodedCryptedBytes + ivDelimiter + encodedIV;
+    }
+
+
+    /**
+     * Decrypt string with RSA algorithm, ECB mode, OAEPWithSHA-256AndMGF1 padding
+     * Asymmetric encryption, with private and public key
+     *
+     * @param string             string to decrypt
+     * @param encryptionKeyBytes key from metadata
+     * @return decrypted string
+     */
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    public static String decryptStringSymmetric(String string, byte[] encryptionKeyBytes)
+            throws NoSuchProviderException, NoSuchAlgorithmException,
+            InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
+            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, CertificateException,
+            InvalidKeySpecException {
+
+        Cipher cipher = Cipher.getInstance(AES_CIPHER);
+
+        String[] strings = string.split(ivDelimiter);
+        String cipherString = strings[0];
+
+        byte[] iv = new IvParameterSpec(decodeStringToBase64Bytes(strings[1])).getIV();
+
+        Key key = new SecretKeySpec(encryptionKeyBytes, AES);
+
+        GCMParameterSpec spec = new GCMParameterSpec(128, iv);
+        cipher.init(Cipher.DECRYPT_MODE, key, spec);
+
+        byte[] bytes = decodeStringToBase64Bytes(cipherString);
+        byte[] encodedBytes = cipher.doFinal(bytes);
+
+        return decodeBase64BytesToString(encodedBytes);
+    }
+
+    /**
+     * Encrypt private key with symmetric AES encryption, GCM mode mode and no padding
+     *
+     * @param privateKey byte64 encoded string representation of private key
+     * @param keyPhrase  key used for encryption, e.g. 12 random words
+     *                   {@link EncryptionUtils#getRandomWords(int, Context)}
+     * @return encrypted string, bytes first encoded base64, IV separated with "|", then to string
+     */
+    public static String encryptPrivateKey(String privateKey, String keyPhrase) throws NoSuchPaddingException,
+            NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, BadPaddingException,
+            IllegalBlockSizeException, InvalidKeySpecException, InvalidParameterSpecException {
+        Cipher cipher = Cipher.getInstance(AES_CIPHER);
+
+        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+        byte[] salt = randomBytes(saltLength);
+        KeySpec spec = new PBEKeySpec(keyPhrase.toCharArray(), salt, iterationCount, keyStrength);
+        SecretKey tmp = factory.generateSecret(spec);
+        SecretKeySpec key = new SecretKeySpec(tmp.getEncoded(), AES);
+
+        cipher.init(Cipher.ENCRYPT_MODE, key);
+        byte[] bytes = encodeStringToBase64Bytes(privateKey);
+        byte[] encrypted = cipher.doFinal(bytes);
+
+        byte[] iv = cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV();
+        String encodedIV = encodeBytesToBase64String(iv);
+        String encodedSalt = encodeBytesToBase64String(salt);
+        String encodedEncryptedBytes = encodeBytesToBase64String(encrypted);
+
+        return encodedEncryptedBytes + ivDelimiter + encodedIV + ivDelimiter + encodedSalt;
+    }
+
+    /**
+     * Decrypt private key with symmetric AES encryption, GCM mode mode and no padding
+     *
+     * @param privateKey byte64 encoded string representation of private key, IV separated with "|"
+     * @param keyPhrase  key used for encryption, e.g. 12 random words
+     *                   {@link EncryptionUtils#getRandomWords(int, Context)}
+     * @return decrypted string
+     */
+    public static String decryptPrivateKey(String privateKey, String keyPhrase) throws NoSuchPaddingException,
+            NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, BadPaddingException,
+            IllegalBlockSizeException, InvalidKeySpecException, InvalidAlgorithmParameterException {
+
+        // split up iv, salt
+        String[] strings = privateKey.split(ivDelimiter);
+        String realPrivateKey = strings[0];
+        byte[] iv = decodeStringToBase64Bytes(strings[1]);
+        byte[] salt = decodeStringToBase64Bytes(strings[2]);
+
+        Cipher cipher = Cipher.getInstance(AES_CIPHER);
+        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+        KeySpec spec = new PBEKeySpec(keyPhrase.toCharArray(), salt, iterationCount, keyStrength);
+        SecretKey tmp = factory.generateSecret(spec);
+        SecretKeySpec key = new SecretKeySpec(tmp.getEncoded(), AES);
+
+        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
+
+        byte[] bytes = decodeStringToBase64Bytes(realPrivateKey);
+        byte[] decrypted = cipher.doFinal(bytes);
+
+        String pemKey = decodeBase64BytesToString(decrypted);
+
+        return pemKey.replaceAll("\n", "").replace("-----BEGIN PRIVATE KEY-----", "")
+                .replace("-----END PRIVATE KEY-----", "");
+    }
+
+    public static String privateKeyToPEM(PrivateKey privateKey) throws IOException {
+        String privateKeyString = encodeBytesToBase64String(privateKey.getEncoded());
+
+        return "-----BEGIN PRIVATE KEY-----\n" + privateKeyString.replaceAll("(.{65})", "$1\n")
+                + "\n-----END PRIVATE KEY-----";
+    }
+
+    /*
+    Helper
+     */
+
+    public static String getMD5Sum(File file) {
+        try {
+            FileInputStream fileInputStream = new FileInputStream(file);
+            MessageDigest md5 = MessageDigest.getInstance("MD5");
+            byte[] bytes = new byte[2048];
+            int readBytes;
+
+            while ((readBytes = fileInputStream.read(bytes)) != -1) {
+                md5.update(bytes, 0, readBytes);
+            }
+
+            return new String(Hex.encodeHex(md5.digest()));
+
+        } catch (Exception e) {
+            Log_OC.e(TAG, e.getMessage());
+        }
+
+        return "";
+    }
+
+    public static ArrayList<String> getRandomWords(int count, Context context) throws IOException {
+        InputStream ins = context.getResources().openRawResource(context.getResources()
+                .getIdentifier("encryption_key_words", "raw", context.getPackageName()));
+
+        InputStreamReader inputStreamReader = new InputStreamReader(ins);
+
+        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
+
+        ArrayList<String> lines = new ArrayList<>();
+        String line;
+        while ((line = bufferedReader.readLine()) != null) {
+            lines.add(line);
+        }
+
+        SecureRandom random = new SecureRandom();
+
+        ArrayList<String> outputLines = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            int randomLine = random.nextInt(lines.size());
+            outputLines.add(lines.get(randomLine));
+        }
+
+        return outputLines;
+    }
+
+    public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA);
+        keyGen.initialize(2048, new SecureRandom());
+        return keyGen.generateKeyPair();
+    }
+
+    public static byte[] generateKey() {
+        KeyGenerator keyGenerator;
+        try {
+            keyGenerator = KeyGenerator.getInstance(AES);
+            keyGenerator.init(128);
+
+            return keyGenerator.generateKey().getEncoded();
+        } catch (NoSuchAlgorithmException e) {
+            Log_OC.e(TAG, e.getMessage());
+        }
+
+        return null;
+    }
+
+    public static byte[] randomBytes(int size) {
+        SecureRandom random = new SecureRandom();
+        final byte[] iv = new byte[size];
+        random.nextBytes(iv);
+
+        return iv;
+    }
+}

+ 12 - 1
src/main/java/com/owncloud/android/utils/FileStorageUtils.java

@@ -79,7 +79,7 @@ public class FileStorageUtils {
      * file.
      */
     public static String getDefaultSavePathFor(String accountName, OCFile file) {
-        return getSavePath(accountName) + file.getRemotePath();
+        return getSavePath(accountName) + file.getDecryptedRemotePath();
     }
 
     /**
@@ -217,6 +217,7 @@ public class FileStorageUtils {
         file.setPermissions(remote.getPermissions());
         file.setRemoteId(remote.getRemoteId());
         file.setFavorite(remote.getIsFavorite());
+        file.setEncrypted(remote.getIsEncrypted());
         return file;
     }
 
@@ -381,4 +382,14 @@ public class FileStorageUtils {
 
         return true;
     }
+
+    public static boolean checkIfInEncryptedFolder(OCFile file, FileDataStorageManager storageManager) {
+        while (!OCFile.ROOT_PATH.equals(file.getRemotePath())) {
+            if (file.isEncrypted()) {
+                return true;
+            }
+            file = storageManager.getFileById(file.getParentId());
+        }
+        return false;
+    }
 }

+ 8 - 4
src/main/java/com/owncloud/android/utils/MimeTypeUtil.java

@@ -128,8 +128,8 @@ public class MimeTypeUtil {
      * @param isSharedViaLink  flag if the folder is publicly shared via link
      * @return Identifier of an image resource.
      */
-    public static Drawable getFolderTypeIcon(boolean isSharedViaUsers, boolean isSharedViaLink) {
-        return getFolderTypeIcon(isSharedViaUsers, isSharedViaLink, null);
+    public static Drawable getFolderTypeIcon(boolean isSharedViaUsers, boolean isSharedViaLink, boolean isEncrypted) {
+        return getFolderTypeIcon(isSharedViaUsers, isSharedViaLink, isEncrypted, null);
     }
 
     /**
@@ -137,16 +137,20 @@ public class MimeTypeUtil {
      *
      * @param isSharedViaUsers flag if the folder is shared via the users system
      * @param isSharedViaLink flag if the folder is publicly shared via link
+     * @param isEncrypted flag if the folder is encrypted
      * @param account account which color should be used
      * @return Identifier of an image resource.
      */
-    public static Drawable getFolderTypeIcon(boolean isSharedViaUsers, boolean isSharedViaLink, Account account) {
+    public static Drawable getFolderTypeIcon(boolean isSharedViaUsers, boolean isSharedViaLink,
+                                             boolean isEncrypted, Account account) {
         int drawableId;
 
         if (isSharedViaLink) {
             drawableId = R.drawable.folder_public;
         } else if (isSharedViaUsers) {
             drawableId = R.drawable.shared_with_me_folder;
+        } else if (isEncrypted) {
+            drawableId = R.drawable.ic_list_encrypted_folder;
         } else {
             drawableId = R.drawable.folder;
         }
@@ -155,7 +159,7 @@ public class MimeTypeUtil {
     }
 
     public static Drawable getDefaultFolderIcon() {
-        return getFolderTypeIcon(false, false);
+        return getFolderTypeIcon(false, false, false);
     }
 
 

BIN
src/main/res/drawable-hdpi/ic_list_encrypted_folder.png


BIN
src/main/res/drawable-mdpi/ic_list_encrypted_folder.png


BIN
src/main/res/drawable-xhdpi/ic_list_encrypted_folder.png


BIN
src/main/res/drawable-xxhdpi/ic_list_encrypted_folder.png


BIN
src/main/res/drawable-xxxhdpi/ic_list_encrypted_folder.png


+ 29 - 0
src/main/res/drawable/e2e_border.xml

@@ -0,0 +1,29 @@
+<!--
+    Nextcloud Android client application
+
+    @author Tobias Kaminsky
+    Copyright (C) 2017 Tobias Kaminsky
+    Copyright (C) 2017 Nextcloud GmbH.
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <stroke
+        android:width="1dp"
+        android:color="#000000"/>
+
+    <solid android:color="@color/grey_200"/>
+</shape>

+ 60 - 0
src/main/res/layout/setup_encryption_dialog.xml

@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 
+    ownCloud Android client application
+
+    Copyright (C) 2012  Bartek Przybylski
+    Copyright (C) 2015 ownCloud Inc.
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License version 2,
+    as published by the Free Software Foundation.
+  
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="clip_horizontal"
+    android:orientation="vertical"
+    android:padding="@dimen/standard_padding">
+
+    <TextView
+        android:id="@+id/encryption_status"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="10dp"/>
+
+    <TextView
+        android:id="@+id/encryption_passphrase"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="10dp"
+        android:background="@drawable/e2e_border"
+        android:textIsSelectable="true"
+        android:gravity="center"
+        android:padding="5dp"
+        android:visibility="gone"/>
+
+    <android.support.design.widget.TextInputLayout
+        android:id="@+id/encryption_passwordLayout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:passwordToggleEnabled="true">
+
+        <android.support.design.widget.TextInputEditText
+            android:id="@+id/encryption_passwordInput"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/end_to_end_encryption_password"
+            android:ems="10"
+            android:inputType="textPassword"/>
+    </android.support.design.widget.TextInputLayout>
+</LinearLayout>

+ 12 - 0
src/main/res/menu/file_actions_menu.xml

@@ -107,6 +107,18 @@
         app:showAsAction="never"
         android:showAsAction="never"
         android:orderInCategory="1" />
+    <item
+        android:id="@+id/action_encrypted"
+        android:title="@string/encrypted"
+        app:showAsAction="never"
+        android:showAsAction="never"
+        android:orderInCategory="1"/>
+    <item
+        android:id="@+id/action_unset_encrypted"
+        android:title="@string/unset_encrypted"
+        app:showAsAction="never"
+        android:showAsAction="never"
+        android:orderInCategory="1"/>
     <item
         android:id="@+id/action_set_as_wallpaper"
         android:title="@string/set_picture_as"

+ 2048 - 0
src/main/res/raw/encryption_key_words.txt

@@ -0,0 +1,2048 @@
+abandon
+ability
+able
+about
+above
+absent
+absorb
+abstract
+absurd
+abuse
+access
+accident
+account
+accuse
+achieve
+acid
+acoustic
+acquire
+across
+act
+action
+actor
+actress
+actual
+adapt
+add
+addict
+address
+adjust
+admit
+adult
+advance
+advice
+aerobic
+affair
+afford
+afraid
+again
+age
+agent
+agree
+ahead
+aim
+air
+airport
+aisle
+alarm
+album
+alcohol
+alert
+alien
+all
+alley
+allow
+almost
+alone
+alpha
+already
+also
+alter
+always
+amateur
+amazing
+among
+amount
+amused
+analyst
+anchor
+ancient
+anger
+angle
+angry
+animal
+ankle
+announce
+annual
+another
+answer
+antenna
+antique
+anxiety
+any
+apart
+apology
+appear
+apple
+approve
+april
+arch
+arctic
+area
+arena
+argue
+arm
+armed
+armor
+army
+around
+arrange
+arrest
+arrive
+arrow
+art
+artefact
+artist
+artwork
+ask
+aspect
+assault
+asset
+assist
+assume
+asthma
+athlete
+atom
+attack
+attend
+attitude
+attract
+auction
+audit
+august
+aunt
+author
+auto
+autumn
+average
+avocado
+avoid
+awake
+aware
+away
+awesome
+awful
+awkward
+axis
+baby
+bachelor
+bacon
+badge
+bag
+balance
+balcony
+ball
+bamboo
+banana
+banner
+bar
+barely
+bargain
+barrel
+base
+basic
+basket
+battle
+beach
+bean
+beauty
+because
+become
+beef
+before
+begin
+behave
+behind
+believe
+below
+belt
+bench
+benefit
+best
+betray
+better
+between
+beyond
+bicycle
+bid
+bike
+bind
+biology
+bird
+birth
+bitter
+black
+blade
+blame
+blanket
+blast
+bleak
+bless
+blind
+blood
+blossom
+blouse
+blue
+blur
+blush
+board
+boat
+body
+boil
+bomb
+bone
+bonus
+book
+boost
+border
+boring
+borrow
+boss
+bottom
+bounce
+box
+boy
+bracket
+brain
+brand
+brass
+brave
+bread
+breeze
+brick
+bridge
+brief
+bright
+bring
+brisk
+broccoli
+broken
+bronze
+broom
+brother
+brown
+brush
+bubble
+buddy
+budget
+buffalo
+build
+bulb
+bulk
+bullet
+bundle
+bunker
+burden
+burger
+burst
+bus
+business
+busy
+butter
+buyer
+buzz
+cabbage
+cabin
+cable
+cactus
+cage
+cake
+call
+calm
+camera
+camp
+can
+canal
+cancel
+candy
+cannon
+canoe
+canvas
+canyon
+capable
+capital
+captain
+car
+carbon
+card
+cargo
+carpet
+carry
+cart
+case
+cash
+casino
+castle
+casual
+cat
+catalog
+catch
+category
+cattle
+caught
+cause
+caution
+cave
+ceiling
+celery
+cement
+census
+century
+cereal
+certain
+chair
+chalk
+champion
+change
+chaos
+chapter
+charge
+chase
+chat
+cheap
+check
+cheese
+chef
+cherry
+chest
+chicken
+chief
+child
+chimney
+choice
+choose
+chronic
+chuckle
+chunk
+churn
+cigar
+cinnamon
+circle
+citizen
+city
+civil
+claim
+clap
+clarify
+claw
+clay
+clean
+clerk
+clever
+click
+client
+cliff
+climb
+clinic
+clip
+clock
+clog
+close
+cloth
+cloud
+clown
+club
+clump
+cluster
+clutch
+coach
+coast
+coconut
+code
+coffee
+coil
+coin
+collect
+color
+column
+combine
+come
+comfort
+comic
+common
+company
+concert
+conduct
+confirm
+congress
+connect
+consider
+control
+convince
+cook
+cool
+copper
+copy
+coral
+core
+corn
+correct
+cost
+cotton
+couch
+country
+couple
+course
+cousin
+cover
+coyote
+crack
+cradle
+craft
+cram
+crane
+crash
+crater
+crawl
+crazy
+cream
+credit
+creek
+crew
+cricket
+crime
+crisp
+critic
+crop
+cross
+crouch
+crowd
+crucial
+cruel
+cruise
+crumble
+crunch
+crush
+cry
+crystal
+cube
+culture
+cup
+cupboard
+curious
+current
+curtain
+curve
+cushion
+custom
+cute
+cycle
+dad
+damage
+damp
+dance
+danger
+daring
+dash
+daughter
+dawn
+day
+deal
+debate
+debris
+decade
+december
+decide
+decline
+decorate
+decrease
+deer
+defense
+define
+defy
+degree
+delay
+deliver
+demand
+demise
+denial
+dentist
+deny
+depart
+depend
+deposit
+depth
+deputy
+derive
+describe
+desert
+design
+desk
+despair
+destroy
+detail
+detect
+develop
+device
+devote
+diagram
+dial
+diamond
+diary
+dice
+diesel
+diet
+differ
+digital
+dignity
+dilemma
+dinner
+dinosaur
+direct
+dirt
+disagree
+discover
+disease
+dish
+dismiss
+disorder
+display
+distance
+divert
+divide
+divorce
+dizzy
+doctor
+document
+dog
+doll
+dolphin
+domain
+donate
+donkey
+donor
+door
+dose
+double
+dove
+draft
+dragon
+drama
+drastic
+draw
+dream
+dress
+drift
+drill
+drink
+drip
+drive
+drop
+drum
+dry
+duck
+dumb
+dune
+during
+dust
+dutch
+duty
+dwarf
+dynamic
+eager
+eagle
+early
+earn
+earth
+easily
+east
+easy
+echo
+ecology
+economy
+edge
+edit
+educate
+effort
+egg
+eight
+either
+elbow
+elder
+electric
+elegant
+element
+elephant
+elevator
+elite
+else
+embark
+embody
+embrace
+emerge
+emotion
+employ
+empower
+empty
+enable
+enact
+end
+endless
+endorse
+enemy
+energy
+enforce
+engage
+engine
+enhance
+enjoy
+enlist
+enough
+enrich
+enroll
+ensure
+enter
+entire
+entry
+envelope
+episode
+equal
+equip
+era
+erase
+erode
+erosion
+error
+erupt
+escape
+essay
+essence
+estate
+eternal
+ethics
+evidence
+evil
+evoke
+evolve
+exact
+example
+excess
+exchange
+excite
+exclude
+excuse
+execute
+exercise
+exhaust
+exhibit
+exile
+exist
+exit
+exotic
+expand
+expect
+expire
+explain
+expose
+express
+extend
+extra
+eye
+eyebrow
+fabric
+face
+faculty
+fade
+faint
+faith
+fall
+false
+fame
+family
+famous
+fan
+fancy
+fantasy
+farm
+fashion
+fat
+fatal
+father
+fatigue
+fault
+favorite
+feature
+february
+federal
+fee
+feed
+feel
+female
+fence
+festival
+fetch
+fever
+few
+fiber
+fiction
+field
+figure
+file
+film
+filter
+final
+find
+fine
+finger
+finish
+fire
+firm
+first
+fiscal
+fish
+fit
+fitness
+fix
+flag
+flame
+flash
+flat
+flavor
+flee
+flight
+flip
+float
+flock
+floor
+flower
+fluid
+flush
+fly
+foam
+focus
+fog
+foil
+fold
+follow
+food
+foot
+force
+forest
+forget
+fork
+fortune
+forum
+forward
+fossil
+foster
+found
+fox
+fragile
+frame
+frequent
+fresh
+friend
+fringe
+frog
+front
+frost
+frown
+frozen
+fruit
+fuel
+fun
+funny
+furnace
+fury
+future
+gadget
+gain
+galaxy
+gallery
+game
+gap
+garage
+garbage
+garden
+garlic
+garment
+gas
+gasp
+gate
+gather
+gauge
+gaze
+general
+genius
+genre
+gentle
+genuine
+gesture
+ghost
+giant
+gift
+giggle
+ginger
+giraffe
+girl
+give
+glad
+glance
+glare
+glass
+glide
+glimpse
+globe
+gloom
+glory
+glove
+glow
+glue
+goat
+goddess
+gold
+good
+goose
+gorilla
+gospel
+gossip
+govern
+gown
+grab
+grace
+grain
+grant
+grape
+grass
+gravity
+great
+green
+grid
+grief
+grit
+grocery
+group
+grow
+grunt
+guard
+guess
+guide
+guilt
+guitar
+gun
+gym
+habit
+hair
+half
+hammer
+hamster
+hand
+happy
+harbor
+hard
+harsh
+harvest
+hat
+have
+hawk
+hazard
+head
+health
+heart
+heavy
+hedgehog
+height
+hello
+helmet
+help
+hen
+hero
+hidden
+high
+hill
+hint
+hip
+hire
+history
+hobby
+hockey
+hold
+hole
+holiday
+hollow
+home
+honey
+hood
+hope
+horn
+horror
+horse
+hospital
+host
+hotel
+hour
+hover
+hub
+huge
+human
+humble
+humor
+hundred
+hungry
+hunt
+hurdle
+hurry
+hurt
+husband
+hybrid
+ice
+icon
+idea
+identify
+idle
+ignore
+ill
+illegal
+illness
+image
+imitate
+immense
+immune
+impact
+impose
+improve
+impulse
+inch
+include
+income
+increase
+index
+indicate
+indoor
+industry
+infant
+inflict
+inform
+inhale
+inherit
+initial
+inject
+injury
+inmate
+inner
+innocent
+input
+inquiry
+insane
+insect
+inside
+inspire
+install
+intact
+interest
+into
+invest
+invite
+involve
+iron
+island
+isolate
+issue
+item
+ivory
+jacket
+jaguar
+jar
+jazz
+jealous
+jeans
+jelly
+jewel
+job
+join
+joke
+journey
+joy
+judge
+juice
+jump
+jungle
+junior
+junk
+just
+kangaroo
+keen
+keep
+ketchup
+key
+kick
+kid
+kidney
+kind
+kingdom
+kiss
+kit
+kitchen
+kite
+kitten
+kiwi
+knee
+knife
+knock
+know
+lab
+label
+labor
+ladder
+lady
+lake
+lamp
+language
+laptop
+large
+later
+latin
+laugh
+laundry
+lava
+law
+lawn
+lawsuit
+layer
+lazy
+leader
+leaf
+learn
+leave
+lecture
+left
+leg
+legal
+legend
+leisure
+lemon
+lend
+length
+lens
+leopard
+lesson
+letter
+level
+liar
+liberty
+library
+license
+life
+lift
+light
+like
+limb
+limit
+link
+lion
+liquid
+list
+little
+live
+lizard
+load
+loan
+lobster
+local
+lock
+logic
+lonely
+long
+loop
+lottery
+loud
+lounge
+love
+loyal
+lucky
+luggage
+lumber
+lunar
+lunch
+luxury
+lyrics
+machine
+mad
+magic
+magnet
+maid
+mail
+main
+major
+make
+mammal
+man
+manage
+mandate
+mango
+mansion
+manual
+maple
+marble
+march
+margin
+marine
+market
+marriage
+mask
+mass
+master
+match
+material
+math
+matrix
+matter
+maximum
+maze
+meadow
+mean
+measure
+meat
+mechanic
+medal
+media
+melody
+melt
+member
+memory
+mention
+menu
+mercy
+merge
+merit
+merry
+mesh
+message
+metal
+method
+middle
+midnight
+milk
+million
+mimic
+mind
+minimum
+minor
+minute
+miracle
+mirror
+misery
+miss
+mistake
+mix
+mixed
+mixture
+mobile
+model
+modify
+mom
+moment
+monitor
+monkey
+monster
+month
+moon
+moral
+more
+morning
+mosquito
+mother
+motion
+motor
+mountain
+mouse
+move
+movie
+much
+muffin
+mule
+multiply
+muscle
+museum
+mushroom
+music
+must
+mutual
+myself
+mystery
+myth
+naive
+name
+napkin
+narrow
+nasty
+nation
+nature
+near
+neck
+need
+negative
+neglect
+neither
+nephew
+nerve
+nest
+net
+network
+neutral
+never
+news
+next
+nice
+night
+noble
+noise
+nominee
+noodle
+normal
+north
+nose
+notable
+note
+nothing
+notice
+novel
+now
+nuclear
+number
+nurse
+nut
+oak
+obey
+object
+oblige
+obscure
+observe
+obtain
+obvious
+occur
+ocean
+october
+odor
+off
+offer
+office
+often
+oil
+okay
+old
+olive
+olympic
+omit
+once
+one
+onion
+online
+only
+open
+opera
+opinion
+oppose
+option
+orange
+orbit
+orchard
+order
+ordinary
+organ
+orient
+original
+orphan
+ostrich
+other
+outdoor
+outer
+output
+outside
+oval
+oven
+over
+own
+owner
+oxygen
+oyster
+ozone
+pact
+paddle
+page
+pair
+palace
+palm
+panda
+panel
+panic
+panther
+paper
+parade
+parent
+park
+parrot
+party
+pass
+patch
+path
+patient
+patrol
+pattern
+pause
+pave
+payment
+peace
+peanut
+pear
+peasant
+pelican
+pen
+penalty
+pencil
+people
+pepper
+perfect
+permit
+person
+pet
+phone
+photo
+phrase
+physical
+piano
+picnic
+picture
+piece
+pig
+pigeon
+pill
+pilot
+pink
+pioneer
+pipe
+pistol
+pitch
+pizza
+place
+planet
+plastic
+plate
+play
+please
+pledge
+pluck
+plug
+plunge
+poem
+poet
+point
+polar
+pole
+police
+pond
+pony
+pool
+popular
+portion
+position
+possible
+post
+potato
+pottery
+poverty
+powder
+power
+practice
+praise
+predict
+prefer
+prepare
+present
+pretty
+prevent
+price
+pride
+primary
+print
+priority
+prison
+private
+prize
+problem
+process
+produce
+profit
+program
+project
+promote
+proof
+property
+prosper
+protect
+proud
+provide
+public
+pudding
+pull
+pulp
+pulse
+pumpkin
+punch
+pupil
+puppy
+purchase
+purity
+purpose
+purse
+push
+put
+puzzle
+pyramid
+quality
+quantum
+quarter
+question
+quick
+quit
+quiz
+quote
+rabbit
+raccoon
+race
+rack
+radar
+radio
+rail
+rain
+raise
+rally
+ramp
+ranch
+random
+range
+rapid
+rare
+rate
+rather
+raven
+raw
+razor
+ready
+real
+reason
+rebel
+rebuild
+recall
+receive
+recipe
+record
+recycle
+reduce
+reflect
+reform
+refuse
+region
+regret
+regular
+reject
+relax
+release
+relief
+rely
+remain
+remember
+remind
+remove
+render
+renew
+rent
+reopen
+repair
+repeat
+replace
+report
+require
+rescue
+resemble
+resist
+resource
+response
+result
+retire
+retreat
+return
+reunion
+reveal
+review
+reward
+rhythm
+rib
+ribbon
+rice
+rich
+ride
+ridge
+rifle
+right
+rigid
+ring
+riot
+ripple
+risk
+ritual
+rival
+river
+road
+roast
+robot
+robust
+rocket
+romance
+roof
+rookie
+room
+rose
+rotate
+rough
+round
+route
+royal
+rubber
+rude
+rug
+rule
+run
+runway
+rural
+sad
+saddle
+sadness
+safe
+sail
+salad
+salmon
+salon
+salt
+salute
+same
+sample
+sand
+satisfy
+satoshi
+sauce
+sausage
+save
+say
+scale
+scan
+scare
+scatter
+scene
+scheme
+school
+science
+scissors
+scorpion
+scout
+scrap
+screen
+script
+scrub
+sea
+search
+season
+seat
+second
+secret
+section
+security
+seed
+seek
+segment
+select
+sell
+seminar
+senior
+sense
+sentence
+series
+service
+session
+settle
+setup
+seven
+shadow
+shaft
+shallow
+share
+shed
+shell
+sheriff
+shield
+shift
+shine
+ship
+shiver
+shock
+shoe
+shoot
+shop
+short
+shoulder
+shove
+shrimp
+shrug
+shuffle
+shy
+sibling
+sick
+side
+siege
+sight
+sign
+silent
+silk
+silly
+silver
+similar
+simple
+since
+sing
+siren
+sister
+situate
+six
+size
+skate
+sketch
+ski
+skill
+skin
+skirt
+skull
+slab
+slam
+sleep
+slender
+slice
+slide
+slight
+slim
+slogan
+slot
+slow
+slush
+small
+smart
+smile
+smoke
+smooth
+snack
+snake
+snap
+sniff
+snow
+soap
+soccer
+social
+sock
+soda
+soft
+solar
+soldier
+solid
+solution
+solve
+someone
+song
+soon
+sorry
+sort
+soul
+sound
+soup
+source
+south
+space
+spare
+spatial
+spawn
+speak
+special
+speed
+spell
+spend
+sphere
+spice
+spider
+spike
+spin
+spirit
+split
+spoil
+sponsor
+spoon
+sport
+spot
+spray
+spread
+spring
+spy
+square
+squeeze
+squirrel
+stable
+stadium
+staff
+stage
+stairs
+stamp
+stand
+start
+state
+stay
+steak
+steel
+stem
+step
+stereo
+stick
+still
+sting
+stock
+stomach
+stone
+stool
+story
+stove
+strategy
+street
+strike
+strong
+struggle
+student
+stuff
+stumble
+style
+subject
+submit
+subway
+success
+such
+sudden
+suffer
+sugar
+suggest
+suit
+summer
+sun
+sunny
+sunset
+super
+supply
+supreme
+sure
+surface
+surge
+surprise
+surround
+survey
+suspect
+sustain
+swallow
+swamp
+swap
+swarm
+swear
+sweet
+swift
+swim
+swing
+switch
+sword
+symbol
+symptom
+syrup
+system
+table
+tackle
+tag
+tail
+talent
+talk
+tank
+tape
+target
+task
+taste
+tattoo
+taxi
+teach
+team
+tell
+ten
+tenant
+tennis
+tent
+term
+test
+text
+thank
+that
+theme
+then
+theory
+there
+they
+thing
+this
+thought
+three
+thrive
+throw
+thumb
+thunder
+ticket
+tide
+tiger
+tilt
+timber
+time
+tiny
+tip
+tired
+tissue
+title
+toast
+tobacco
+today
+toddler
+toe
+together
+toilet
+token
+tomato
+tomorrow
+tone
+tongue
+tonight
+tool
+tooth
+top
+topic
+topple
+torch
+tornado
+tortoise
+toss
+total
+tourist
+toward
+tower
+town
+toy
+track
+trade
+traffic
+tragic
+train
+transfer
+trap
+trash
+travel
+tray
+treat
+tree
+trend
+trial
+tribe
+trick
+trigger
+trim
+trip
+trophy
+trouble
+truck
+true
+truly
+trumpet
+trust
+truth
+try
+tube
+tuition
+tumble
+tuna
+tunnel
+turkey
+turn
+turtle
+twelve
+twenty
+twice
+twin
+twist
+two
+type
+typical
+ugly
+umbrella
+unable
+unaware
+uncle
+uncover
+under
+undo
+unfair
+unfold
+unhappy
+uniform
+unique
+unit
+universe
+unknown
+unlock
+until
+unusual
+unveil
+update
+upgrade
+uphold
+upon
+upper
+upset
+urban
+urge
+usage
+use
+used
+useful
+useless
+usual
+utility
+vacant
+vacuum
+vague
+valid
+valley
+valve
+van
+vanish
+vapor
+various
+vast
+vault
+vehicle
+velvet
+vendor
+venture
+venue
+verb
+verify
+version
+very
+vessel
+veteran
+viable
+vibrant
+vicious
+victory
+video
+view
+village
+vintage
+violin
+virtual
+virus
+visa
+visit
+visual
+vital
+vivid
+vocal
+voice
+void
+volcano
+volume
+vote
+voyage
+wage
+wagon
+wait
+walk
+wall
+walnut
+want
+warfare
+warm
+warrior
+wash
+wasp
+waste
+water
+wave
+way
+wealth
+weapon
+wear
+weasel
+weather
+web
+wedding
+weekend
+weird
+welcome
+west
+wet
+whale
+what
+wheat
+wheel
+when
+where
+whip
+whisper
+wide
+width
+wife
+wild
+will
+win
+window
+wine
+wing
+wink
+winner
+winter
+wire
+wisdom
+wise
+wish
+witness
+wolf
+woman
+wonder
+wood
+wool
+word
+work
+world
+worry
+worth
+wrap
+wreck
+wrestle
+wrist
+write
+wrong
+yard
+year
+yellow
+you
+young
+youth
+zebra
+zero
+zone
+zoo

+ 14 - 2
src/main/res/values-de/strings.xml

@@ -695,5 +695,17 @@
     <string name="screenshot_04_accounts">Mit verschiedenen Kontos verbinden</string>
     <string name="screenshot_05_autoUpload">Automatisches Hochladen von Bildern &amp; Videos</string>
     <string name="screenshot_06_davdroid">Kalender &amp; Kontakte mit DAVdroid synchronisieren</string>
-    
-    </resources>
+
+    <string name="end_to_end_encryption_folder_not_empty">Verzeichnis nicht leer!</string>
+    <string name="end_to_end_encryption_wrong_password">Fehler beim entschlüsseln. Falsches Passwort??</string>
+    <string name="end_to_end_encryption_decrypting">Entschlüsseln…</string>
+    <string name="end_to_end_encryption_retrieving_keys">Hole Schlüssel…</string>
+    <string name="end_to_end_encryption_enter_password">Zum entschlüsseln des privaten Schlüssels bitte Passwort eingeben!</string>
+    <string name="end_to_end_encryption_generating_keys">Erstelle neue Schlüssel…</string>
+    <string name="end_to_end_encryption_keywords_description">Dieser 12 Worte Satz ist wie ein starkes Passwort: Es emöglicht Zugang zu den verschlüsselten Dateien. Bitte notieren Sie sich den Satz und bewahren ihn sicher auf.</string>
+    <string name="end_to_end_encryption_title">Verschlüsselung einrichten</string>
+    <string name="end_to_end_encryption_passphrase_title">Notieren Sie sich den Verschlüsselungssatz.</string>
+    <string name="end_to_end_encryption_not_supported">Verschlüsselung erst ab KitKat unterstützt.</string>
+    <string name="end_to_end_encryption_confirm_button">Verschlüsselung einrichten</string>
+    <string name="end_to_end_encryption_password">Passwort…</string>
+</resources>

+ 1 - 0
src/main/res/values/colors.xml

@@ -38,6 +38,7 @@
     <!-- Colors -->
     <color name="standard_grey">#757575</color>
     <color name="elementFallbackColor">#555555</color>
+    <color name="grey_200">#EEEEEE</color>
 
     <!-- standard material color definitions -->
 

+ 1 - 0
src/main/res/values/setup.xml

@@ -19,6 +19,7 @@
     <string name="log_name">nextcloud</string>
     <string name="default_display_name_for_root_folder">Nextcloud</string>
     <string name="user_agent">Mozilla/5.0 (Android) ownCloud-android/%1$s</string>
+    <string name="nextcloud_user_agent">Mozilla/5.0 (Android) Nextcloud-android/%1$s</string>
     
     <!-- URLs and flags related -->
     <string name="server_url"></string>

+ 19 - 0
src/main/res/values/strings.xml

@@ -126,6 +126,7 @@
     <string name="common_cancel">Cancel</string>
     <string name="common_back">Back</string>
     <string name="common_save">Save</string>
+    <string name="common_error">Error</string>
     <string name="common_loading">Loading &#8230;</string>
     <string name="common_unknown">unknown</string>
     <string name="common_error_unknown">Unknown error</string>
@@ -268,6 +269,8 @@
     <string name="favorite_real">Set as favorite</string>
     <string name="unset_favorite_real">Unset favorite</string>
     <string name="favorite_switch">Available offline</string>
+    <string name="encrypted">Set as encrypted</string>
+    <string name="unset_encrypted">Unset encryption</string>
     <string name="common_rename">Rename</string>
     <string name="common_remove">Delete</string>
     <string name="confirmation_remove_file_alert">Do you really want to delete %1$s?</string>
@@ -753,4 +756,20 @@
     <string name="userinfo_no_info_text">Add name, picture and contact details on your profile page.</string>
     <string name="drawer_header_background">Background image of drawer header</string>
     <string name="account_icon">Account icon</string>
+    
+    <string name="end_to_end_encryption_folder_not_empty">Folder not empty!</string>
+    <string name="end_to_end_encryption_wrong_password">Error while decrypting. Wrong password?</string>
+    <string name="end_to_end_encryption_decrypting">Decrypting…</string>
+    <string name="end_to_end_encryption_retrieving_keys">Retrieving keys…</string>
+    <string name="end_to_end_encryption_enter_password">Please enter password to decrypt private key!</string>
+    <string name="end_to_end_encryption_generating_keys">Generating new keys…</string>
+    <string name="end_to_end_encryption_keywords_description">This 12 word phrase is like a very strong password: It provides full access to view and use your encrypted files. Please write it down and keep it somewhere safe.</string>
+    <string name="end_to_end_encryption_title">Set up encryption</string>
+    <string name="end_to_end_encryption_passphrase_title">Note your encryption passphrase</string>
+    <string name="end_to_end_encryption_not_supported">Encryption not supported before KitKat.</string>
+    <string name="end_to_end_encryption_confirm_button">Set up encryption</string>
+    <string name="end_to_end_encryption_password">Password…</string>
+    <string name="end_to_end_encryption_unsuccessful">Storing keys was unsuccessful, please try it again!</string>
+    <string name="end_to_end_encryption_dialog_close">Close</string>
+    <string name="end_to_end_encryption_storing_keys">Storing keys</string>
 </resources>

+ 5 - 1
src/test/java/com/owncloud/android/utils/ErrorMessageAdapterUnitTest.java

@@ -21,8 +21,10 @@
 
 package com.owncloud.android.utils;
 
+import android.accounts.Account;
 import android.content.res.Resources;
 
+import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.operations.RemoveFileOperation;
@@ -65,10 +67,12 @@ public class ErrorMessageAdapterUnitTest {
         when(mMockResources.getString(R.string.forbidden_permissions_delete))
             .thenReturn(MOCK_TO_DELETE);
 
+        Account account = new Account("name", MainApp.getAccountType());
+
         // ... when method under test is called ...
         String errorMessage = ErrorMessageAdapter.getErrorCauseMessage(
             new RemoteOperationResult(RemoteOperationResult.ResultCode.FORBIDDEN),
-            new RemoveFileOperation(PATH_TO_DELETE, false),
+                new RemoveFileOperation(PATH_TO_DELETE, false, account, MainApp.getAppContext()),
             mMockResources
         );