First release of E2E

This commit is contained in:
Jan-Christoph Borchardt 2017-08-23 15:52:14 +02:00 committed by tobiasKaminsky
parent de2e7942f1
commit b6e84b66fd
No known key found for this signature in database
GPG Key ID: 0E00D4D47D0C5AF7
66 changed files with 6505 additions and 213 deletions

View File

@ -212,6 +212,9 @@ 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'
// uncomment for gplay, modified
// implementation "com.google.firebase:firebase-messaging:${googleLibraryVersion}"
// implementation "com.google.android.gms:play-services-base:${googleLibraryVersion}"
@ -236,7 +239,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'

View File

@ -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>

After

Width:  |  Height:  |  Size: 294 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 281 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 562 B

View File

@ -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">

View File

@ -1 +1 @@
include ':'
include ':nextcloud-android-library'

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -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>

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -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

View File

@ -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));

View File

@ -10,7 +10,6 @@ import android.support.test.runner.AndroidJUnit4;
import com.owncloud.android.db.OCUpload;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -40,14 +39,14 @@ public class UploadStorageManagerTest {
public void testDeleteAllUploads() {
//Clean
for (Account account : Accounts) {
uploadsStorageManager.removeAccountUploads(account);
// uploadsStorageManager.removeAccountUploads(account);
}
int accountRowsA = 3;
int accountRowsB = 4;
insertUploads(Accounts[0], accountRowsA);
insertUploads(Accounts[1], accountRowsB);
Assert.assertTrue("Expected 4 removed uploads files", uploadsStorageManager.removeAccountUploads(Accounts[1]) == 4);
// Assert.assertTrue("Expected 4 removed uploads files", uploadsStorageManager.removeAccountUploads(Accounts[1]) == 4);
}
private void insertUploads(Account account, int rowsToInsert) {
@ -66,7 +65,7 @@ public class UploadStorageManagerTest {
@After
public void tearDown() {
for (Account account : Accounts) {
uploadsStorageManager.removeAccountUploads(account);
// uploadsStorageManager.removeAccountUploads(account);
}
}
}

View File

@ -0,0 +1,357 @@
/*
* 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.lib.common.utils.Log_OC;
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.net.URLEncoder;
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 com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes;
import static com.owncloud.android.utils.EncryptionUtils.encodeBytesToBase64String;
import static com.owncloud.android.utils.EncryptionUtils.generateIV;
import static com.owncloud.android.utils.EncryptionUtils.generateKey;
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 static String TAG = EncryptionTestIT.class.getSimpleName();
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 = 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 = 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 = 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();
String string = CsrHelper.generateCsrPemEncodedString(keyPair);
String urlEncoded = URLEncoder.encode("-----BEGIN CERTIFICATE REQUEST-----\n" + string +
"\n-----END CERTIFICATE REQUEST-----", "UTF-8");
Log_OC.d(TAG, "public: " + encodeBytesToBase64String(keyPair.getPublic().getEncoded()));
Log_OC.d(TAG, "csrPEM: " + string);
Log_OC.d(TAG, "csrPEM: " + urlEncoded);
}
/**
* 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==");
cryptFile("ia7OEEEyXMoRa1QWQk8r", "78f42172166f9dc8fd1a7156b1753353", key, iv, authTag);
}
@Test
public void cryptFileWithMetadata() throws Exception {
DecryptedFolderMetadata metadata = generateFolderMetadata();
// n9WXAIXO2wRY4R8nXwmo
cryptFile("ia7OEEEyXMoRa1QWQk8r",
"78f42172166f9dc8fd1a7156b1753353",
EncryptionUtils.decodeStringToBase64Bytes(metadata.files.get("ia7OEEEyXMoRa1QWQk8r").encrypted.key),
EncryptionUtils.decodeStringToBase64Bytes(metadata.files.get("ia7OEEEyXMoRa1QWQk8r").initializationVector),
EncryptionUtils.decodeStringToBase64Bytes(metadata.files.get("ia7OEEEyXMoRa1QWQk8r").authenticationTag));
// n9WXAIXO2wRY4R8nXwmo
cryptFile("n9WXAIXO2wRY4R8nXwmo",
"825143ed1f21ebb0c3b3c3f005b2f5db",
EncryptionUtils.decodeStringToBase64Bytes(metadata.files.get("n9WXAIXO2wRY4R8nXwmo").encrypted.key),
EncryptionUtils.decodeStringToBase64Bytes(metadata.files.get("n9WXAIXO2wRY4R8nXwmo").initializationVector),
EncryptionUtils.decodeStringToBase64Bytes(metadata.files.get("n9WXAIXO2wRY4R8nXwmo").authenticationTag));
}
/**
* 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(encodeBytesToBase64String(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(encodeBytesToBase64String(generateIV())));
}
}
// 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 = encodeBytesToBase64String(EncryptionUtils.generateKey());
String metadataKey1 = encodeBytesToBase64String(EncryptionUtils.generateKey());
String metadataKey2 = 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.metadataKeys = metadataKeys;
DecryptedFolderMetadata.Metadata metadata1 = new DecryptedFolderMetadata.Metadata();
metadata1.metadataKeys = metadataKeys;
metadata1.version = 1;
DecryptedFolderMetadata.Sharing sharing = new DecryptedFolderMetadata.Sharing();
sharing.signature = "HMACOFRECIPIENTANDNEWESTMETADATAKEY";
HashMap<String, String> recipient = new HashMap<>();
recipient.put("blah@schiessle.org", "PUBLIC KEY");
recipient.put("bjoern@schiessle.org", "PUBLIC KEY");
sharing.recipient = recipient;
metadata1.sharing = sharing;
HashMap<String, DecryptedFolderMetadata.DecryptedFile> files = new HashMap<>();
DecryptedFolderMetadata.Data data1 = new DecryptedFolderMetadata.Data();
data1.key = "WANM0gRv+DhaexIsI0T3Lg==";
data1.filename = "test.txt";
data1.version = 1;
DecryptedFolderMetadata.DecryptedFile file1 = new DecryptedFolderMetadata.DecryptedFile();
file1.initializationVector = "gKm3n+mJzeY26q4OfuZEqg==";
file1.encrypted = data1;
file1.metadataKey = 0;
file1.authenticationTag = "PboI9tqHHX3QeAA22PIu4w==";
files.put("ia7OEEEyXMoRa1QWQk8r", file1);
DecryptedFolderMetadata.Data data2 = new DecryptedFolderMetadata.Data();
data2.key = "9dfzbIYDt28zTyZfbcll+g==";
data2.filename = "test2.txt";
data2.version = 1;
DecryptedFolderMetadata.DecryptedFile file2 = new DecryptedFolderMetadata.DecryptedFile();
file2.initializationVector = "hnJLF8uhDvDoFK4ajuvwrg==";
file2.encrypted = data2;
file2.metadataKey = 0;
file2.authenticationTag = "qOQZdu5soFO77Y7y4rAOVA==";
files.put("n9WXAIXO2wRY4R8nXwmo", file2);
return new DecryptedFolderMetadata(metadata1, files);
}
private void 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 = 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();
assertEquals(md5, EncryptionUtils.getMD5Sum(decryptedFile));
}
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;
}
}

View File

@ -18,7 +18,9 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.owncloud.android">
package="com.owncloud.android"
android:versionCode="20000052"
android:versionName="2.0.0-e2e-02">
<application
android:name=".MainApp"

View File

@ -19,7 +19,9 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.owncloud.android">
package="com.owncloud.android"
android:versionCode="20000052"
android:versionName="2.0.0-e2e-02">
<!-- GET_ACCOUNTS is needed for API <= 22.
For API >= 23 results in the addition of CONTACTS group to the list of permissions that may be

View File

@ -64,6 +64,7 @@ import com.owncloud.android.utils.AnalyticsUtils;
import com.owncloud.android.utils.FilesSyncHelper;
import com.owncloud.android.utils.PermissionUtil;
import com.owncloud.android.utils.ReceiversHelper;
import com.owncloud.android.utils.EncryptionUtils;
import java.lang.reflect.Method;
import java.util.ArrayList;
@ -71,9 +72,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import javax.crypto.Cipher;
import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
@ -328,7 +329,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);

View File

@ -0,0 +1,77 @@
/*
* 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 {
public Metadata metadata;
public 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 static class Metadata {
public HashMap<Integer, String> metadataKeys; // each keys is encrypted on its own, decrypt on use
public Sharing sharing;
public int version;
@Override
public String toString() {
return String.valueOf(version);
}
}
public static class Encrypted {
public HashMap<Integer, String> metadataKeys;
}
public static class Sharing {
public HashMap<String, String> recipient;
public String signature;
}
public static class DecryptedFile {
public Data encrypted;
public String initializationVector;
public String authenticationTag;
public int metadataKey;
}
public static class Data {
public String key;
public String filename;
public String mimetype;
public int version;
}
}

View File

@ -0,0 +1,46 @@
/*
* 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 {
public DecryptedFolderMetadata.Metadata metadata;
public HashMap<String, EncryptedFile> files;
public EncryptedFolderMetadata(DecryptedFolderMetadata.Metadata metadata, HashMap<String, EncryptedFile> files) {
this.metadata = metadata;
this.files = files;
}
public static class EncryptedFile {
public String encrypted;
public String initializationVector;
public String authenticationTag;
public int metadataKey;
}
}

View File

@ -0,0 +1,22 @@
package com.owncloud.android.datamodel;
import com.google.gson.JsonElement;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
/**
* Created by tobi on 03.08.17.
*/
public class EncryptedSerializer implements JsonSerializer<DecryptedFolderMetadata.Encrypted> {
@Override
public JsonElement serialize(DecryptedFolderMetadata.Encrypted src, Type typeOfSrc,
JsonSerializationContext context) {
// DecryptedFolderMetadata.Encrypted encrypted = new Gson().fromJson(src, DecryptedFolderMetadata.Encrypted.class);
return null;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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()) {

View File

@ -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);

View File

@ -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);

View File

@ -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 = "";
@ -175,10 +181,36 @@ 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.files.get(mFile.getEncryptedFileName()).encrypted.key);
byte[] iv = EncryptionUtils.decodeStringToBase64Bytes(metadata.files.get(mFile.getEncryptedFileName()).initializationVector);
byte[] authenticationTag = EncryptionUtils.decodeStringToBase64Bytes(metadata.files.get(mFile.getEncryptedFileName()).authenticationTag);
try {
byte[] decryptedBytes = EncryptionUtils.decryptFile(tmpFile, key, iv, authenticationTag);
FileOutputStream fileOutputStream = new FileOutputStream(tmpFile);
fileOutputStream.write(decryptedBytes);
fileOutputStream.close();
} catch (Exception e) {
// TODO TOBI better handling
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() + ": " +

View File

@ -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()){
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;
}
@ -313,30 +315,30 @@ public class RefreshFolderOperation extends RemoteOperation {
ReadRemoteFolderOperation operation = new ReadRemoteFolderOperation(remotePath);
RemoteOperationResult result = operation.execute(client);
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,39 @@ 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;
if (mLocalFolder.isEncrypted() && 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) {
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 +406,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 +441,34 @@ 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.files.get(updatedFile.getFileName()).encrypted.filename;
String mimetype = metadata.files.get(updatedFile.getFileName()).encrypted.mimetype;
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(true);
}
updatedFiles.add(updatedFile);
}
@ -448,15 +481,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 +497,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 +513,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 +535,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 +548,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);

View File

@ -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));

View File

@ -0,0 +1,174 @@
/*
* 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);
String publicKey = arbitraryDataProvider.getValue(account.name, EncryptionUtils.PUBLIC_KEY);
// unlock
try {
// Lock folder
LockFileOperation lockFileOperation = new LockFileOperation(parentId);
RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client);
if (lockFileOperationResult.isSuccess()) {
token = (String) lockFileOperationResult.getData().get(0);
} else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
throw new Exception("Forbidden! Please try again later.)");
} else {
throw new Exception("Unknown error!");
}
// refresh metadata
GetMetadataOperation getMetadataOperation = new GetMetadataOperation(parentId);
RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client);
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 Exception("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.files.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);
if (!uploadMetadataOperationResult.isSuccess()) {
throw new Exception();
}
// 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);
if (!unlockFileOperationResult.isSuccess()) {
Log_OC.e(TAG, "Failed to unlock " + parentId);
}
}
}
return result;
}
}

View File

@ -0,0 +1,880 @@
/*
* 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.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
import com.owncloud.android.lib.common.network.ProgressiveDataTransferer;
import com.owncloud.android.lib.common.operations.OperationCancelledException;
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.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.EncryptionUtils;
import com.owncloud.android.utils.FileStorageUtils;
import com.owncloud.android.utils.MimeType;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.RequestEntity;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
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;
import static com.owncloud.android.utils.EncryptionUtils.encodeStringToBase64Bytes;
/**
* Operation performing the update in the ownCloud server
* of a file that was modified locally.
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class UploadEncryptedFileOperation extends SyncOperation {
private static final String TAG = UploadEncryptedFileOperation.class.getSimpleName();
public static final int CREATED_BY_USER = 0;
public static final int CREATED_AS_INSTANT_PICTURE = 1;
public static final int CREATED_AS_INSTANT_VIDEO = 2;
private OCFile parentFile;
/**
* OCFile which is to be uploaded.
*/
private OCFile ocFile;
private int localBehaviour = FileUploader.LOCAL_BEHAVIOUR_COPY;
private final String originalStoragePath;
private boolean chunked = false;
private boolean mRemoteFolderToBeCreated = false;
private int mCreatedBy = CREATED_BY_USER;
private long mOCUploadId = -1;
/**
* Local path to file which is to be uploaded (before any possible renaming or moving).
*/
private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<OnDatatransferProgressListener>();
private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false);
private final AtomicBoolean mUploadStarted = new AtomicBoolean(false);
private Context context;
private UploadRemoteFileOperation mUploadOperation;
protected RequestEntity mEntity = null;
private Account account;
private ArbitraryDataProvider arbitraryDataProvider;
public UploadEncryptedFileOperation(OCFile parent, UploadFileOperation uploadFileOperation) {
parentFile = parent;
ocFile = uploadFileOperation.getFile();
account = uploadFileOperation.getAccount();
chunked = uploadFileOperation.isChunkedUploadSupported();
context = uploadFileOperation.getContext();
localBehaviour = uploadFileOperation.getLocalBehaviour();
originalStoragePath = uploadFileOperation.getStoragePath();
arbitraryDataProvider = new ArbitraryDataProvider(context.getContentResolver());
}
public Account getAccount() {
return account;
}
public String getFileName() {
return (ocFile != null) ? ocFile.getFileName() : null;
}
public OCFile getFile() {
return ocFile;
}
public String getStoragePath() {
return ocFile.getStoragePath();
}
public String getRemotePath() {
return ocFile.getParentRemotePath() + ocFile.getEncryptedFileName();
}
public String getMimeType() {
return ocFile.getMimetype();
}
public void setRemoteFolderToBeCreated() {
mRemoteFolderToBeCreated = true;
}
public void setCreatedBy(int createdBy) {
mCreatedBy = createdBy;
if (createdBy < CREATED_BY_USER || CREATED_AS_INSTANT_VIDEO < createdBy) {
mCreatedBy = CREATED_BY_USER;
}
}
public int getCreatedBy() {
return mCreatedBy;
}
public boolean isInstantPicture() {
return mCreatedBy == CREATED_AS_INSTANT_PICTURE;
}
public boolean isInstantVideo() {
return mCreatedBy == CREATED_AS_INSTANT_VIDEO;
}
public void setOCUploadId(long id) {
mOCUploadId = id;
}
public long getOCUploadId() {
return mOCUploadId;
}
public Set<OnDatatransferProgressListener> getDataTransferListeners() {
return mDataTransferListeners;
}
public void addDatatransferProgressListener(OnDatatransferProgressListener listener) {
synchronized (mDataTransferListeners) {
mDataTransferListeners.add(listener);
}
if (mEntity != null) {
((ProgressiveDataTransferer) mEntity).addDatatransferProgressListener(listener);
}
if (mUploadOperation != null) {
mUploadOperation.addDatatransferProgressListener(listener);
}
}
public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) {
synchronized (mDataTransferListeners) {
mDataTransferListeners.remove(listener);
}
if (mEntity != null) {
((ProgressiveDataTransferer) mEntity).removeDatatransferProgressListener(listener);
}
if (mUploadOperation != null) {
mUploadOperation.removeDatatransferProgressListener(listener);
}
}
@Override
protected RemoteOperationResult run(OwnCloudClient client) {
RemoteOperationResult result = null;
boolean metadataExists = false;
String token = null;
mCancellationRequested.set(false);
mUploadStarted.set(true);
File temporalFile = null;
File originalFile = new File(ocFile.getStoragePath());
File expectedFile = null;
FileLock fileLock = null;
String privateKey = arbitraryDataProvider.getValue(account.name, EncryptionUtils.PRIVATE_KEY);
String publicKey = arbitraryDataProvider.getValue(account.name, EncryptionUtils.PUBLIC_KEY);
try {
/// check if the file continues existing before schedule the operation
if (!originalFile.exists()) {
Log_OC.d(TAG, ocFile.getStoragePath() + " not exists anymore");
throw new FileNotFoundException();
}
/// 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);
if (!result.isSuccess()) {
return result;
}
// TODO automatic rename? UploadFileOperation:365
/// set parent local id in uploading file
// OCFile parent = getStorageManager().getFileByPath(remoteParentPath);
// ocFile.setParentId(parent.getFileId());
// if (mCancellationRequested.get()) {
// throw new OperationCancelledException();
// }
// Get the last modification date of the file from the file system
Long timeStampLong = originalFile.lastModified() / 1000;
String timeStamp = timeStampLong.toString();
// Lock folder
LockFileOperation lockFileOperation = new LockFileOperation(parentFile.getLocalId());
RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client);
if (lockFileOperationResult.isSuccess()) {
token = (String) lockFileOperationResult.getData().get(0);
} else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
throw new Exception("Forbidden! Please try again later.)");
} else {
throw new Exception("Unknown error!");
}
// Update metadata
GetMetadataOperation getMetadataOperation = new GetMetadataOperation(parentFile.getLocalId());
RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client);
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.metadata = new DecryptedFolderMetadata.Metadata();
metadata.metadata.metadataKeys = new HashMap<>();
String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey);
metadata.metadata.metadataKeys.put(0, encryptedMetadataKey);
} else {
// TODO error
throw new Exception("something wrong");
}
// Key
byte[] key = null;
try {
// TODO change key if file has changed, e.g. when file is updated
key = encodeStringToBase64Bytes(metadata.files.get(ocFile.getFileName()).encrypted.key);
} catch (Exception e) {
// no key found
}
if (key == null || key.length == 0) {
key = EncryptionUtils.generateKey();
}
// IV
byte[] iv = null;
try {
iv = encodeStringToBase64Bytes(metadata.files.get(ocFile.getFileName()).initializationVector);
} catch (Exception e) {
// no iv found
}
if (iv == null || iv.length == 0) {
iv = EncryptionUtils.generateIV();
}
EncryptionUtils.EncryptedFile encryptedFile = EncryptionUtils.encryptFile(ocFile, key, iv);
// new random file name, check if it exists in metadata
String encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
while (metadata.files.get(encryptedFileName) != null) {
encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
}
ocFile.setEncryptedFileName(encryptedFileName);
File encryptedTempFile = File.createTempFile("encFile", encryptedFileName);
FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
fileOutputStream.write(encryptedFile.encryptedBytes);
fileOutputStream.close();
/// perform the upload
if (chunked &&
(new File(ocFile.getStoragePath())).length() >
ChunkedUploadRemoteFileOperation.CHUNK_SIZE) {
mUploadOperation = new ChunkedUploadRemoteFileOperation(context, encryptedTempFile.getAbsolutePath(),
ocFile.getParentRemotePath() + encryptedFileName, ocFile.getMimetype(),
ocFile.getEtagInConflict(), timeStamp);
} else {
mUploadOperation = new UploadRemoteFileOperation(encryptedTempFile.getAbsolutePath(),
ocFile.getParentRemotePath() + encryptedFileName, ocFile.getMimetype(),
ocFile.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();
// }
// }
result = mUploadOperation.execute(client);
/// move local temporal file or original file to its corresponding
// location in the ownCloud 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.filename = ocFile.getFileName();
data.mimetype = ocFile.getMimetype();
data.key = EncryptionUtils.encodeBytesToBase64String(key);
decryptedFile.encrypted = data;
decryptedFile.initializationVector = EncryptionUtils.encodeBytesToBase64String(iv);
decryptedFile.authenticationTag = encryptedFile.authenticationTag;
metadata.files.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);
} else {
// store metadata
StoreMetadataOperation storeMetadataOperation = new StoreMetadataOperation(parentFile.getLocalId(),
serializedFolderMetadata);
uploadMetadataOperationResult = storeMetadataOperation.execute(client);
}
if (!uploadMetadataOperationResult.isSuccess()) {
throw new Exception();
}
}
} catch (FileNotFoundException e) {
Log_OC.d(TAG, ocFile.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);
// unlock file
if (token != null) {
UnlockFileOperation unlockFileOperation = new UnlockFileOperation(parentFile.getLocalId(), token);
RemoteOperationResult unlockFileOperationResult = unlockFileOperation.execute(client);
if (!unlockFileOperationResult.isSuccess()) {
Log_OC.e(TAG, "Failed to unlock " + parentFile.getLocalId());
}
}
if (fileLock != null) {
try {
fileLock.release();
} catch (IOException e) {
Log_OC.e(TAG, "Failed to unlock file with path " + ocFile.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 " + ocFile.getStoragePath() + " to " + ocFile.getRemotePath() + ": " +
result.getLogMessage());
} else {
if (result.getException() != null) {
if (result.isCancelled()) {
Log_OC.w(TAG, "Upload of " + ocFile.getStoragePath() + " to " + ocFile.getRemotePath() +
": " + result.getLogMessage());
} else {
Log_OC.e(TAG, "Upload of " + ocFile.getStoragePath() + " to " + ocFile.getRemotePath() +
": " + result.getLogMessage(), result.getException());
}
} else {
Log_OC.e(TAG, "Upload of " + ocFile.getStoragePath() + " to " + ocFile.getRemotePath() +
": " + result.getLogMessage());
}
}
}
switch (localBehaviour) {
case FileUploader.LOCAL_BEHAVIOUR_FORGET:
String temporalPath = FileStorageUtils.getTemporalPath(account.name) + ocFile.getRemotePath();
if (originalStoragePath.equals(temporalPath)) {
// delete local file is was pre-copied in temporary folder (see .ui.helpers.UriUploader)
temporalFile = new File(temporalPath);
temporalFile.delete();
}
ocFile.setStoragePath("");
saveUploadedFile(client);
break;
case FileUploader.LOCAL_BEHAVIOUR_DELETE:
Log_OC.d(TAG, "Delete source file");
originalFile.delete();
getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
saveUploadedFile(client);
break;
case FileUploader.LOCAL_BEHAVIOUR_COPY:
if (temporalFile != null) {
try {
move(temporalFile, expectedFile);
} catch (IOException e) {
e.printStackTrace();
}
}
ocFile.setStoragePath(expectedFile.getAbsolutePath());
saveUploadedFile(client);
FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath());
break;
case FileUploader.LOCAL_BEHAVIOUR_MOVE:
String expectedPath = FileStorageUtils.getDefaultSavePathFor(account.name, ocFile);
expectedFile = new File(expectedPath);
try {
move(originalFile, expectedFile);
} catch (IOException e) {
e.printStackTrace();
}
getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
ocFile.setStoragePath(expectedFile.getAbsolutePath());
saveUploadedFile(client);
FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath());
break;
}
return result;
}
/**
* Checks the existence of the folder where the current file will be uploaded both
* in the remote server and in the local database.
* <p/>
* If the upload is set to enforce the creation of the folder, the method tries to
* create it both remote and locally.
*
* @param pathToGrant Full remote path whose existence will be granted.
* @return An {@link OCFile} instance corresponding to the folder where the file
* will be uploaded.
*/
private RemoteOperationResult grantFolderExistence(String pathToGrant, OwnCloudClient client) {
RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, context, false);
RemoteOperationResult result = operation.execute(client);
if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND && mRemoteFolderToBeCreated) {
SyncOperation syncOp = new CreateFolderOperation(pathToGrant, true);
result = syncOp.execute(client, getStorageManager());
}
if (result.isSuccess()) {
OCFile parentDir = getStorageManager().getFileByPath(pathToGrant);
if (parentDir == null) {
parentDir = createLocalFolder(pathToGrant);
}
if (parentDir != null) {
result = new RemoteOperationResult(ResultCode.OK);
} else {
result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR);
}
}
return result;
}
private OCFile createLocalFolder(String remotePath) {
String parentPath = new File(remotePath).getParent();
parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ?
parentPath : parentPath + OCFile.PATH_SEPARATOR;
OCFile parent = getStorageManager().getFileByPath(parentPath);
if (parent == null) {
parent = createLocalFolder(parentPath);
}
if (parent != null) {
OCFile createdFolder = new OCFile(remotePath);
createdFolder.setMimetype(MimeType.DIRECTORY);
createdFolder.setParentId(parent.getFileId());
getStorageManager().saveFile(createdFolder);
return createdFolder;
}
return null;
}
/**
* 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
*/
private String getAvailableRemotePath(OwnCloudClient wc, String remotePath) {
boolean check = existsFile(wc, remotePath);
if (!check) {
return remotePath;
}
int pos = remotePath.lastIndexOf('.');
String suffix = "";
String extension = "";
if (pos >= 0) {
extension = remotePath.substring(pos + 1);
remotePath = remotePath.substring(0, pos);
}
int count = 2;
do {
suffix = " (" + count + ")";
if (pos >= 0) {
check = existsFile(wc, remotePath + suffix + "." + extension);
} else {
check = existsFile(wc, remotePath + suffix);
}
count++;
} while (check);
if (pos >= 0) {
return remotePath + suffix + "." + extension;
} else {
return remotePath + suffix;
}
}
private boolean existsFile(OwnCloudClient client, String remotePath) {
ExistenceCheckRemoteOperation existsOperation =
new ExistenceCheckRemoteOperation(remotePath, context, false);
RemoteOperationResult result = existsOperation.execute(client);
return result.isSuccess();
}
/**
* Allows to cancel the actual upload operation. If actual upload operating
* is in progress it is cancelled, if upload preparation is being performed
* upload will not take place.
*/
public void cancel() {
if (mUploadOperation == null) {
if (mUploadStarted.get()) {
Log_OC.d(TAG, "Cancelling upload during upload preparations.");
mCancellationRequested.set(true);
} else {
Log_OC.e(TAG, "No upload in progress. This should not happen.");
}
} else {
Log_OC.d(TAG, "Cancelling upload during actual upload operation.");
mUploadOperation.cancel();
}
}
/**
* As soon as this method return true, upload can be cancel via cancel().
*/
public boolean isUploadInProgress() {
return mUploadStarted.get();
}
// /**
// * TODO rewrite with homogeneous fail handling, remove dependency on {@link RemoteOperationResult},
// * TODO use Exceptions instead
// *
// * @param sourceFile Source file to copy.
// * @param targetFile Target location to copy the file.
// * @return {@link RemoteOperationResult}
// * @throws IOException
// */
// private RemoteOperationResult copy(File sourceFile, File targetFile) throws IOException {
// Log_OC.d(TAG, "Copying local file");
//
// RemoteOperationResult result = null;
//
// if (FileStorageUtils.getUsableSpace(account.name) < sourceFile.length()) {
// result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_FULL);
// return result; // error condition when the file should be copied
//
// } else {
// Log_OC.d(TAG, "Creating temporal folder");
// File temporalParent = targetFile.getParentFile();
// temporalParent.mkdirs();
// if (!temporalParent.isDirectory()) {
// throw new IOException(
// "Unexpected error: parent directory could not be created");
// }
// Log_OC.d(TAG, "Creating temporal file");
// targetFile.createNewFile();
// if (!targetFile.isFile()) {
// throw new IOException(
// "Unexpected error: target file could not be created");
// }
//
// Log_OC.d(TAG, "Copying file contents");
// InputStream in = null;
// OutputStream out = null;
//
// try {
// if (!mOriginalStoragePath.equals(targetFile.getAbsolutePath())) {
// // In case document provider schema as 'content://'
// if (mOriginalStoragePath.startsWith(UriUtils.URI_CONTENT_SCHEME)) {
// Uri uri = Uri.parse(mOriginalStoragePath);
// in = context.getContentResolver().openInputStream(uri);
// } else {
// in = new FileInputStream(sourceFile);
// }
// out = new FileOutputStream(targetFile);
// int nRead;
// byte[] buf = new byte[4096];
// while (!mCancellationRequested.get() &&
// (nRead = in.read(buf)) > -1) {
// out.write(buf, 0, nRead);
// }
// out.flush();
//
// } // else: weird but possible situation, nothing to copy
//
// if (mCancellationRequested.get()) {
// result = new RemoteOperationResult(new OperationCancelledException());
// return result;
// }
//
// } catch (Exception e) {
// result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_NOT_COPIED);
// return result;
//
// } finally {
// try {
// if (in != null) {
// in.close();
// }
// } catch (Exception e) {
// Log_OC.d(TAG, "Weird exception while closing input stream for " +
// mOriginalStoragePath + " (ignoring)", e);
// }
// try {
// if (out != null) {
// out.close();
// }
// } catch (Exception e) {
// Log_OC.d(TAG, "Weird exception while closing output stream for " +
// targetFile.getAbsolutePath() + " (ignoring)", e);
// }
// }
// }
// return result;
// }
/**
* 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
*/
private void move(File sourceFile, File targetFile) throws IOException {
if (!targetFile.equals(sourceFile)) {
File expectedFolder = targetFile.getParentFile();
expectedFolder.mkdirs();
if (expectedFolder.isDirectory()) {
if (!sourceFile.renameTo(targetFile)) {
// try to copy and then delete
targetFile.createNewFile();
FileChannel inChannel = new FileInputStream(sourceFile).getChannel();
FileChannel outChannel = new FileOutputStream(targetFile).getChannel();
try {
inChannel.transferTo(0, inChannel.size(), outChannel);
sourceFile.delete();
} catch (Exception e) {
ocFile.setStoragePath(""); // forget the local file
// by now, treat this as a success; the file was uploaded
// the best option could be show a warning message
} finally {
if (inChannel != null) {
inChannel.close();
}
if (outChannel != null) {
outChannel.close();
}
}
}
} else {
ocFile.setStoragePath("");
}
}
}
/**
* Saves a OC File after a successful upload.
* <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 = ocFile;
if (file.fileExists()) {
file = getStorageManager().getFileById(file.getFileId());
}
long syncDate = System.currentTimeMillis();
file.setLastSyncDateForData(syncDate);
// new PROPFIND to keep data consistent with server
// 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);
if (result.isSuccess()) {
updateOCFile(file, (RemoteFile) result.getData().get(0));
file.setLastSyncDateForProperties(syncDate);
} else {
Log_OC.e(TAG, "Error reading properties of file after successful upload; this is gonna hurt...");
}
file.setEncrypted(true);
file.setStoragePath("");
file.setParentId(parentFile.getFileId());
getStorageManager().saveFile(file);
getStorageManager().saveConflict(file, null);
FileDataStorageManager.triggerMediaScan(file.getStoragePath());
}
private void updateOCFile(OCFile file, RemoteFile remoteFile) {
file.setCreationTimestamp(remoteFile.getCreationTimestamp());
file.setFileLength(remoteFile.getLength());
// file.setMimetype(file.getMimetype());
file.setModificationTimestamp(remoteFile.getModifiedTimestamp());
file.setModificationTimestampAtLastSyncForData(remoteFile.getModifiedTimestamp());
file.setEtag(remoteFile.getEtag());
file.setRemoteId(remoteFile.getRemoteId());
}
public interface OnRenameListener {
void onRenameUpload();
}
}

View File

@ -22,9 +22,15 @@ package com.owncloud.android.operations;
import android.accounts.Account;
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 +47,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,11 +80,15 @@ 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;
import static com.owncloud.android.utils.EncryptionUtils.encodeStringToBase64Bytes;
/**
* Operation performing the update in the ownCloud server
@ -125,6 +141,7 @@ public class UploadFileOperation extends SyncOperation {
protected RequestEntity mEntity = null;
private Account mAccount;
private UploadsStorageManager uploadsStorageManager;
public static OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType) {
@ -241,6 +258,10 @@ public class UploadFileOperation extends SyncOperation {
return mFile.getRemotePath();
}
public String getDecryptedRemotePath() {
return mFile.getDecryptedRemotePath();
}
public String getMimeType() {
return mFile.getMimetype();
}
@ -316,29 +337,74 @@ 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);
uploadsStorageManager = new UploadsStorageManager(mContext.getContentResolver(),
mContext);
for (OCUpload ocUpload : uploadsStorageManager.getAllStoredUploads()) {
if (ocUpload.getUploadId() == getOCUploadId()) {
ocUpload.setFileSize(0);
uploadsStorageManager.updateUpload(ocUpload);
break;
}
}
/// 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;
RemoteOperationResult result = grantFolderExistence(remoteParentPath, client);
if (!result.isSuccess()) {
return result;
}
OCFile parent = getStorageManager().getFileByPath(remoteParentPath);
mFile.setParentId(parent.getFileId());
if (parent.isEncrypted()) {
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);
}
}
@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;
UploadsStorageManager uploadsStorageManager = new UploadsStorageManager(mContext.getContentResolver(),
mContext);
long size = 0;
for (OCUpload ocUpload : uploadsStorageManager.getAllStoredUploads()) {
if (ocUpload.getUploadId() == getOCUploadId()) {
ocUpload.setFileSize(size);
uploadsStorageManager.updateUpload(ocUpload);
break;
}
}
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 {
@ -372,25 +438,382 @@ public class UploadFileOperation extends SyncOperation {
return new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND);
}
/// 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);
// Lock folder
LockFileOperation lockFileOperation = new LockFileOperation(parentFile.getLocalId());
RemoteOperationResult lockFileOperationResult = lockFileOperation.execute(client);
if (!result.isSuccess()) {
return result;
if (lockFileOperationResult.isSuccess()) {
token = (String) lockFileOperationResult.getData().get(0);
} else if (lockFileOperationResult.getHttpCode() == HttpStatus.SC_FORBIDDEN) {
throw new Exception("Forbidden! Please try again later.)");
} else {
throw new Exception("Unknown error!");
}
/// set parent local id in uploading file
OCFile parent = getStorageManager().getFileByPath(remoteParentPath);
mFile.setParentId(parent.getFileId());
// Update metadata
GetMetadataOperation getMetadataOperation = new GetMetadataOperation(parentFile.getLocalId());
RemoteOperationResult getMetadataOperationResult = getMetadataOperation.execute(client);
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.metadata = new DecryptedFolderMetadata.Metadata();
metadata.metadata.metadataKeys = new HashMap<>();
String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey());
String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey);
metadata.metadata.metadataKeys.put(0, encryptedMetadataKey);
} else {
// TODO error
throw new Exception("something wrong");
}
/// 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);
String remotePath = getAvailableRemotePath(client, mRemotePath, metadata, true);
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();
}
String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile);
expectedFile = new File(expectedPath);
/// copy the file locally before uploading
if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_COPY &&
!mOriginalStoragePath.equals(expectedPath)) {
String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath();
mFile.setStoragePath(temporalPath);
temporalFile = new File(temporalPath);
result = copy(originalFile, temporalFile);
if (result != null) {
return result;
}
}
if (mCancellationRequested.get()) {
throw new OperationCancelledException();
}
// Get the last modification date of the file from the file system
Long timeStampLong = originalFile.lastModified() / 1000;
String timeStamp = timeStampLong.toString();
// Key
byte[] key = null;
try {
// TODO change key if file has changed, e.g. when file is updated
key = encodeStringToBase64Bytes(metadata.files.get(mFile.getFileName()).encrypted.key);
} catch (Exception e) {
// no key found
}
if (key == null || key.length == 0) {
key = EncryptionUtils.generateKey();
}
// IV
byte[] iv = null;
try {
iv = encodeStringToBase64Bytes(metadata.files.get(mFile.getFileName()).initializationVector);
} catch (Exception e) {
// no iv found
}
if (iv == null || iv.length == 0) {
iv = EncryptionUtils.generateIV();
}
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.files.get(encryptedFileName) != null) {
encryptedFileName = UUID.randomUUID().toString().replaceAll("-", "");
}
mFile.setEncryptedFileName(encryptedFileName);
File encryptedTempFile = File.createTempFile("encFile", encryptedFileName);
FileOutputStream fileOutputStream = new FileOutputStream(encryptedTempFile);
fileOutputStream.write(encryptedFile.encryptedBytes);
fileOutputStream.close();
/// perform the upload
if (mChunked &&
(new File(mFile.getStoragePath())).length() >
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();
// }
// }
result = mUploadOperation.execute(client);
/// move local temporal file or original file to its corresponding
// location in the ownCloud 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.filename = mFile.getFileName();
data.mimetype = mFile.getMimetype();
data.key = EncryptionUtils.encodeBytesToBase64String(key);
decryptedFile.encrypted = data;
decryptedFile.initializationVector = EncryptionUtils.encodeBytesToBase64String(iv);
decryptedFile.authenticationTag = encryptedFile.authenticationTag;
metadata.files.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);
} else {
// store metadata
StoreMetadataOperation storeMetadataOperation = new StoreMetadataOperation(parentFile.getLocalId(),
serializedFolderMetadata);
uploadMetadataOperationResult = storeMetadataOperation.execute(client);
}
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);
// unlock file
if (token != null) {
UnlockFileOperation unlockFileOperation = new UnlockFileOperation(parentFile.getLocalId(), token);
RemoteOperationResult unlockFileOperationResult = unlockFileOperation.execute(client);
if (!unlockFileOperationResult.isSuccess()) {
Log_OC.e(TAG, "Failed to unlock " + parentFile.getLocalId());
}
}
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());
}
}
}
switch (mLocalBehaviour) {
case FileUploader.LOCAL_BEHAVIOUR_FORGET:
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)
temporalFile = new File(temporalPath);
temporalFile.delete();
}
mFile.setStoragePath("");
saveUploadedFile(client);
break;
case FileUploader.LOCAL_BEHAVIOUR_DELETE:
Log_OC.d(TAG, "Delete source file");
originalFile.delete();
getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
saveUploadedFile(client);
break;
case FileUploader.LOCAL_BEHAVIOUR_COPY:
if (temporalFile != null) {
try {
move(temporalFile, expectedFile);
} catch (IOException e) {
e.printStackTrace();
}
}
mFile.setStoragePath(expectedFile.getAbsolutePath());
saveUploadedFile(client);
FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath());
break;
case FileUploader.LOCAL_BEHAVIOUR_MOVE:
String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile);
expectedFile = new File(expectedPath);
try {
move(originalFile, expectedFile);
} catch (IOException e) {
e.printStackTrace();
}
getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
mFile.setStoragePath(expectedFile.getAbsolutePath());
saveUploadedFile(client);
FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath());
break;
}
return result;
}
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 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.isCharging(mContext)) {
Log_OC.d(TAG, "Upload delayed until the device is charging: " + getRemotePath());
return new RemoteOperationResult(ResultCode.DELAYED_FOR_CHARGING);
}
/// 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);
}
/// 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, null, false);
mWasRenamed = !remotePath.equals(mRemotePath);
if (mWasRenamed) {
createNewOCFile(remotePath);
@ -674,10 +1097,12 @@ public class UploadFileOperation extends SyncOperation {
*
* @param wc
* @param remotePath
* @param metadata
* @return
*/
private String getAvailableRemotePath(OwnCloudClient wc, String remotePath) {
boolean check = existsFile(wc, remotePath);
private String getAvailableRemotePath(OwnCloudClient wc, String remotePath, DecryptedFolderMetadata metadata,
boolean encrypted) {
boolean check = existsFile(wc, remotePath, metadata, encrypted);
if (!check) {
return remotePath;
}
@ -693,9 +1118,9 @@ public class UploadFileOperation extends SyncOperation {
do {
suffix = " (" + count + ")";
if (pos >= 0) {
check = existsFile(wc, remotePath + suffix + "." + extension);
check = existsFile(wc, remotePath + suffix + "." + extension, metadata, encrypted);
} else {
check = existsFile(wc, remotePath + suffix);
check = existsFile(wc, remotePath + suffix, metadata, encrypted);
}
count++;
} while (check);
@ -707,11 +1132,24 @@ public class UploadFileOperation extends SyncOperation {
}
}
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.files.values()) {
if (file.encrypted.filename.equalsIgnoreCase(fileName)) {
return true;
}
}
return false;
} else {
ExistenceCheckRemoteOperation existsOperation =
new ExistenceCheckRemoteOperation(remotePath, mContext, false);
RemoteOperationResult result = existsOperation.execute(client);
return result.isSuccess();
}
}
/**

View File

@ -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;
@ -1504,7 +1505,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 +
@ -1543,9 +1565,325 @@ public class FileContentProvider extends ContentProvider {
@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);
}
}
}
private boolean checkIfColumnExists(SQLiteDatabase database, String table, String column) {
Cursor cursor = database.rawQuery("SELECT * FROM " + table + " LIMIT 0", null);
boolean exists = cursor.getColumnIndex(column) != -1;
cursor.close();
return exists;
}
private void createFilesTable(SQLiteDatabase db) {
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
+ ProviderTableMeta.FILE_MODIFIED + INTEGER
+ ProviderTableMeta.FILE_CONTENT_TYPE + TEXT
+ ProviderTableMeta.FILE_CONTENT_LENGTH + INTEGER
+ ProviderTableMeta.FILE_STORAGE_PATH + TEXT
+ ProviderTableMeta.FILE_ACCOUNT_OWNER + TEXT
+ ProviderTableMeta.FILE_LAST_SYNC_DATE + INTEGER
+ ProviderTableMeta.FILE_KEEP_IN_SYNC + INTEGER
+ ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA + INTEGER
+ ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA + INTEGER
+ ProviderTableMeta.FILE_ETAG + TEXT
+ ProviderTableMeta.FILE_SHARED_VIA_LINK + INTEGER
+ ProviderTableMeta.FILE_PUBLIC_LINK + TEXT
+ ProviderTableMeta.FILE_PERMISSIONS + " TEXT null,"
+ ProviderTableMeta.FILE_REMOTE_ID + " TEXT null,"
+ 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);"
);
}
private void createOCSharesTable(SQLiteDatabase db) {
// Create OCShares table
db.execSQL("CREATE TABLE " + ProviderTableMeta.OCSHARES_TABLE_NAME + "("
+ ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "
+ ProviderTableMeta.OCSHARES_FILE_SOURCE + INTEGER
+ ProviderTableMeta.OCSHARES_ITEM_SOURCE + INTEGER
+ ProviderTableMeta.OCSHARES_SHARE_TYPE + INTEGER
+ ProviderTableMeta.OCSHARES_SHARE_WITH + TEXT
+ ProviderTableMeta.OCSHARES_PATH + TEXT
+ ProviderTableMeta.OCSHARES_PERMISSIONS + INTEGER
+ ProviderTableMeta.OCSHARES_SHARED_DATE + INTEGER
+ ProviderTableMeta.OCSHARES_EXPIRATION_DATE + INTEGER
+ ProviderTableMeta.OCSHARES_TOKEN + TEXT
+ ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME + TEXT
+ ProviderTableMeta.OCSHARES_IS_DIRECTORY + INTEGER // boolean
+ ProviderTableMeta.OCSHARES_USER_ID + INTEGER
+ ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED + INTEGER
+ ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + " TEXT );");
}
private void createCapabilitiesTable(SQLiteDatabase db) {
// Create capabilities table
db.execSQL("CREATE TABLE " + ProviderTableMeta.CAPABILITIES_TABLE_NAME + "("
+ ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "
+ ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME + TEXT
+ ProviderTableMeta.CAPABILITIES_VERSION_MAYOR + INTEGER
+ ProviderTableMeta.CAPABILITIES_VERSION_MINOR + INTEGER
+ ProviderTableMeta.CAPABILITIES_VERSION_MICRO + INTEGER
+ ProviderTableMeta.CAPABILITIES_VERSION_STRING + TEXT
+ ProviderTableMeta.CAPABILITIES_VERSION_EDITION + TEXT
+ ProviderTableMeta.CAPABILITIES_CORE_POLLINTERVAL + INTEGER
+ ProviderTableMeta.CAPABILITIES_SHARING_API_ENABLED + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ENABLED + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS + INTEGER
+ ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_SEND_MAIL + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_UPLOAD + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_USER_SEND_MAIL + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_RESHARING + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_OUTGOING + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_INCOMING + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_FILES_BIGFILECHUNKING + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_FILES_UNDELETE + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_FILES_VERSIONING + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_FILES_DROP + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_EXTERNAL_LINKS + INTEGER // boolean
+ ProviderTableMeta.CAPABILITIES_SERVER_NAME + TEXT
+ ProviderTableMeta.CAPABILITIES_SERVER_COLOR + TEXT
+ 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_END_TO_END_ENCRYPTION + " INTEGER );");
}
private void createUploadsTable(SQLiteDatabase db) {
// Create uploads table
db.execSQL("CREATE TABLE " + ProviderTableMeta.UPLOADS_TABLE_NAME + "("
+ ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "
+ ProviderTableMeta.UPLOADS_LOCAL_PATH + TEXT
+ ProviderTableMeta.UPLOADS_REMOTE_PATH + TEXT
+ ProviderTableMeta.UPLOADS_ACCOUNT_NAME + TEXT
+ ProviderTableMeta.UPLOADS_FILE_SIZE + " LONG, "
+ ProviderTableMeta.UPLOADS_STATUS + INTEGER // UploadStatus
+ ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR + INTEGER // Upload LocalBehaviour
+ ProviderTableMeta.UPLOADS_UPLOAD_TIME + INTEGER
+ ProviderTableMeta.UPLOADS_FORCE_OVERWRITE + INTEGER // boolean
+ ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER + INTEGER // boolean
+ ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP + INTEGER
+ 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
);
/* before:
// PRIMARY KEY should always imply NOT NULL. Unfortunately, due to a
// bug in some early versions, this is not the case in SQLite.
//db.execSQL("CREATE TABLE " + TABLE_UPLOAD + " (" + " path TEXT PRIMARY KEY NOT NULL UNIQUE,"
// + " uploadStatus INTEGER NOT NULL, uploadObject TEXT NOT NULL);");
// uploadStatus is used to easy filtering, it has precedence over
// uploadObject.getUploadStatus()
*/
}
private void createSyncedFoldersTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME + "("
+ ProviderTableMeta._ID + " INTEGER PRIMARY KEY, " // id
+ ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + " TEXT, " // local path
+ ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH + " TEXT, " // remote path
+ ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY + " INTEGER, " // wifi_only
+ ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY + " INTEGER, " // charging only
+ ProviderTableMeta.SYNCED_FOLDER_ENABLED + " INTEGER, " // enabled
+ ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE + " INTEGER, " // subfolder by date
+ ProviderTableMeta.SYNCED_FOLDER_ACCOUNT + " TEXT, " // account
+ ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION + " INTEGER, " // upload action
+ ProviderTableMeta.SYNCED_FOLDER_TYPE + " INTEGER );" // type
);
}
private void createExternalLinksTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME + "("
+ ProviderTableMeta._ID + " INTEGER PRIMARY KEY, " // id
+ ProviderTableMeta.EXTERNAL_LINKS_ICON_URL + " TEXT, " // icon url
+ ProviderTableMeta.EXTERNAL_LINKS_LANGUAGE + " TEXT, " // language
+ ProviderTableMeta.EXTERNAL_LINKS_TYPE + " INTEGER, " // type
+ ProviderTableMeta.EXTERNAL_LINKS_NAME + " TEXT, " // name
+ ProviderTableMeta.EXTERNAL_LINKS_URL + " TEXT );" // url
);
}
private void createArbitraryData(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + ProviderTableMeta.ARBITRARY_DATA_TABLE_NAME + "("
+ ProviderTableMeta._ID + " INTEGER PRIMARY KEY, " // id
+ ProviderTableMeta.ARBITRARY_DATA_CLOUD_ID + " TEXT, " // cloud id (account name + FQDN)
+ ProviderTableMeta.ARBITRARY_DATA_KEY + " TEXT, " // key
+ ProviderTableMeta.ARBITRARY_DATA_VALUE + " TEXT );" // value
);
}
private void createVirtualTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + ProviderTableMeta.VIRTUAL_TABLE_NAME + "("
+ ProviderTableMeta._ID + " INTEGER PRIMARY KEY, " // id
+ ProviderTableMeta.VIRTUAL_TYPE + " TEXT, " // type
+ ProviderTableMeta.VIRTUAL_OCFILE_ID + " INTEGER )" // file id
);
}
private void createFileSystemTable(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS " + ProviderTableMeta.FILESYSTEM_TABLE_NAME + "("
+ ProviderTableMeta._ID + " INTEGER PRIMARY KEY, " // id
+ ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " TEXT, "
+ ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER + " INTEGER, "
+ ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY + " LONG, "
+ ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD + " INTEGER, "
+ ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " STRING, "
+ ProviderTableMeta.FILESYSTEM_CRC32 + " STRING, "
+ ProviderTableMeta.FILESYSTEM_FILE_MODIFIED + " LONG );"
);
}
/**
* Version 10 of database does not modify its scheme. It coincides with the upgrade of the ownCloud account names
* structure to include in it the path to the server instance. Updating the account names and path to local files
* in the files table is a must to keep the existing account working and the database clean.
*
* @param db Database where table of files is included.
*/
private void updateAccountName(SQLiteDatabase db) {
Log_OC.d(SQL, "THREAD: " + Thread.currentThread().getName());
AccountManager ama = AccountManager.get(getContext());
try {
// get accounts from AccountManager ; we can't be sure if accounts in it are updated or not although
// we know the update was previously done in {link @FileActivity#onCreate} because the changes through
// AccountManager are not synchronous
Account[] accounts = AccountManager.get(getContext()).getAccountsByType(
MainApp.getAccountType());
String serverUrl;
String username;
String oldAccountName;
String newAccountName;
for (Account account : accounts) {
// build both old and new account name
serverUrl = ama.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL);
username = AccountUtils.getUsernameForAccount(account);
oldAccountName = AccountUtils.buildAccountNameOld(Uri.parse(serverUrl), username);
newAccountName = AccountUtils.buildAccountName(Uri.parse(serverUrl), username);
// update values in database
db.beginTransaction();
try {
ContentValues cv = new ContentValues();
cv.put(ProviderTableMeta.FILE_ACCOUNT_OWNER, newAccountName);
int num = db.update(ProviderTableMeta.FILE_TABLE_NAME,
cv,
ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?",
new String[]{oldAccountName});
Log_OC.d(SQL, "Updated account in database: old name == " + oldAccountName +
", new name == " + newAccountName + " (" + num + " rows updated )");
// update path for downloaded files
updateDownloadedFiles(db, newAccountName, oldAccountName);
db.setTransactionSuccessful();
} catch (SQLException e) {
Log_OC.e(TAG, "SQL Exception upgrading account names or paths in database", e);
} finally {
db.endTransaction();
}
}
} catch (Exception e) {
Log_OC.e(TAG, "Exception upgrading account names or paths in database", e);
}
}
/**
* Rename the local ownCloud folder of one account to match the a rename of the account itself. Updates the
* table of files in database so that the paths to the local files keep being the same.
*
* @param db Database where table of files is included.
* @param newAccountName New name for the target OC account.
* @param oldAccountName Old name of the target OC account.
*/
private void updateDownloadedFiles(SQLiteDatabase db, String newAccountName,
String oldAccountName) {
String whereClause = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=? AND " +
ProviderTableMeta.FILE_STORAGE_PATH + " IS NOT NULL";
Cursor c = db.query(ProviderTableMeta.FILE_TABLE_NAME,
null,
whereClause,
new String[]{newAccountName},
null, null, null);
try {
if (c.moveToFirst()) {
// create storage path
String oldAccountPath = FileStorageUtils.getSavePath(oldAccountName);
String newAccountPath = FileStorageUtils.getSavePath(newAccountName);
// move files
File oldAccountFolder = new File(oldAccountPath);
File newAccountFolder = new File(newAccountPath);
oldAccountFolder.renameTo(newAccountFolder);
// update database
do {
// Update database
String oldPath = c.getString(
c.getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH));
OCFile file = new OCFile(
c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PATH)));
String newPath = FileStorageUtils.getDefaultSavePathFor(newAccountName, file);
ContentValues cv = new ContentValues();
cv.put(ProviderTableMeta.FILE_STORAGE_PATH, newPath);
db.update(ProviderTableMeta.FILE_TABLE_NAME,
cv,
ProviderTableMeta.FILE_STORAGE_PATH + "=?",
new String[]{oldPath});
Log_OC.v(SQL, "Updated path of downloaded file: old file name == " + oldPath +
", new file name == " + newPath);
} while (c.moveToNext());
}
} finally {
c.close();
}
}
private boolean isCallerNotAllowed() {
String callingPackage;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
callingPackage = getCallingPackage();
} else {
callingPackage = mContext.getPackageManager().getNameForUid(Binder.getCallingUid());
}
return callingPackage == null || !callingPackage.contains(mContext.getPackageName());
}
}

View File

@ -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)

View File

@ -52,6 +52,7 @@ import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -105,6 +106,7 @@ import com.owncloud.android.ui.preview.PreviewMediaFragment;
import com.owncloud.android.ui.preview.PreviewTextFragment;
import com.owncloud.android.ui.preview.PreviewVideoActivity;
import com.owncloud.android.utils.DataHolderUtil;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.ErrorMessageAdapter;
import com.owncloud.android.utils.FileSortOrder;
@ -237,8 +239,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

View File

@ -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()));
}
}

View File

@ -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;

View File

@ -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) {

View File

@ -0,0 +1,352 @@
/*
* 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.security.KeyPair;
import java.security.PrivateKey;
import java.util.ArrayList;
/*
* 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 Account account;
private TextView textView;
private TextView passphraseTextView;
private ArbitraryDataProvider arbitraryDataProvider;
private Button positiveButton;
private TextInputLayout passwordLayout;
private DownloadKeysAsyncTask task;
private TextInputEditText passwordField;
private boolean keyCreated;
/**
* 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);
positiveButton.setVisibility(View.INVISIBLE);
}
@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 = (TextView) v.findViewById(R.id.encryption_status);
passphraseTextView = (TextView) v.findViewById(R.id.encryption_passphrase);
passwordLayout = (TextInputLayout) v.findViewById(R.id.encryption_passwordLayout);
passwordField = (TextInputEditText) 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)
.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) {
if (keyCreated) {
Log_OC.d(TAG, "New keys generated and stored.");
dialog.dismiss();
Intent intent = new Intent();
intent.putExtra(SUCCESS, true);
intent.putExtra(ARG_POSITION, getArguments().getInt(ARG_POSITION));
getTargetFragment().onActivityResult(getTargetRequestCode(), SETUP_ENCRYPTION_RESULT_CODE,
intent);
} else {
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());
arbitraryDataProvider.storeOrUpdateKeyValue(account.name, EncryptionUtils.PRIVATE_KEY,
decryptedPrivateKey);
dialog.dismiss();
Log_OC.d(TAG, "Private key successfully decrypted and stored");
Intent intent = new Intent();
intent.putExtra(SUCCESS, true);
intent.putExtra(ARG_POSITION, getArguments().getInt(ARG_POSITION));
getTargetFragment().onActivityResult(getTargetRequestCode(),
SETUP_ENCRYPTION_RESULT_CODE, intent);
} catch (Exception e) {
textView.setText(R.string.end_to_end_encryption_wrong_password);
Log_OC.d(TAG, "Error while decrypting private key: " + e.getMessage());
}
}
}
});
}
});
task = new DownloadKeysAsyncTask();
task.execute();
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);
}
@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());
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());
if (privateKeyResult.isSuccess()) {
Log_OC.d(TAG, "private key successful downloaded for " + account.name);
keyCreated = false;
return (String) privateKeyResult.getData().get(0);
} else {
return null;
}
}
@Override
protected void onPostExecute(String privateKey) {
super.onPostExecute(privateKey);
if (privateKey == null) {
// no public/private key available, generate new
GenerateNewKeysAsyncTask newKeysTask = new GenerateNewKeysAsyncTask();
newKeysTask.execute();
} 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> {
private ArrayList<String> keyWords;
@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;
keyCreated = true;
// Create public/private key pair
KeyPair keyPair = EncryptionUtils.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
// create CSR
String urlEncoded = CsrHelper.generateCsrPemEncodedString(keyPair);
SendCSROperation operation = new SendCSROperation(urlEncoded);
RemoteOperationResult result = operation.execute(account, getContext());
if (result.isSuccess()) {
Log_OC.d(TAG, "public key success");
publicKey = (String) result.getData().get(0);
} else {
throw new Exception("Public key not stored!");
}
keyWords = EncryptionUtils.getRandomWords(12, getContext());
StringBuilder stringBuilder = new StringBuilder();
for (String string: keyWords) {
stringBuilder.append(string);
}
String keyPhrase = stringBuilder.toString();
String privateKeyString = EncryptionUtils.encodeBytesToBase64String(privateKey.getEncoded());
String encryptedPrivateKey = EncryptionUtils.encryptPrivateKey(privateKeyString, keyPhrase);
// upload encryptedPrivateKey
StorePrivateKeyOperation storePrivateKeyOperation = new StorePrivateKeyOperation(encryptedPrivateKey);
RemoteOperationResult storePrivateKeyResult = storePrivateKeyOperation.execute(account, getContext());
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);
return (String) storePrivateKeyResult.getData().get(0);
}
} catch (Exception e) {
Log_OC.e(TAG, e.getMessage());
e.printStackTrace();
}
return "";
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
if (!s.isEmpty()) {
getDialog().setTitle(R.string.end_to_end_encryption_passphrase_title);
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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}
@ -1111,6 +1180,18 @@ public class OCFileListFragment extends ExtendedListFragment implements OCFileLi
}
mFile = directory;
// hide create new folder within encrypted folders for now
if (mFile.isEncrypted()) {
getFabMkdir().setVisibility(View.GONE);
} else {
getFabMkdir().setVisibility(View.VISIBLE);
if (miniFabClicked) {
((TextView) getFabMkdir().getTag(com.getbase.floatingactionbutton.R.id.fab_label))
.setVisibility(View.GONE);
}
}
updateLayout();
}
@ -1504,6 +1585,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);
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

View File

@ -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<>();

View File

@ -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) {

View File

@ -0,0 +1,65 @@
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 {
/**
* Create the certificate signing request (CSR) from private and public keys
*
* @param keyPair the KeyPair with private and public keys
* @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) throws IOException,
OperatorCreationException {
String principal = "CN=www.nextcloud.com, O=Nextcloud, L=Stuttgart, ST=Baden-Wuerttemberg, C=DE";
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);
}
public static String generateCsrPemEncodedString(KeyPair keyPair) throws IOException, OperatorCreationException {
PKCS10CertificationRequest csr = CsrHelper.generateCSR(keyPair);
byte[] derCSR = csr.getEncoded();
return "-----BEGIN CERTIFICATE REQUEST-----\n" + android.util.Base64.encodeToString(
derCSR, android.util.Base64.NO_PADDING | android.util.Base64.NO_WRAP)
+ "\n-----END CERTIFICATE REQUEST-----";
}
}

View File

@ -0,0 +1,621 @@
/*
* 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();
private static byte[] salt = "$4$YmBjm3hk$Qb74D5IUYwghUmzsMqeNFx5z0/8$".getBytes();
private static String ivDelimiter = "fA=="; // "|" base64 encoded
private static int iterationCount = 1024;
private static int keyStrength = 256;
public static String PUBLIC_KEY = "PUBLIC_KEY";
public static String PRIVATE_KEY = "PRIVATE_KEY";
/*
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.metadata,
files);
// Encrypt each file in "files"
for (Map.Entry<String, DecryptedFolderMetadata.DecryptedFile> entry : decryptedFolderMetadata.files.entrySet()) {
String key = entry.getKey();
DecryptedFolderMetadata.DecryptedFile decryptedFile = entry.getValue();
EncryptedFolderMetadata.EncryptedFile encryptedFile = new EncryptedFolderMetadata.EncryptedFile();
encryptedFile.initializationVector = decryptedFile.initializationVector;
encryptedFile.metadataKey = decryptedFile.metadataKey;
encryptedFile.authenticationTag = decryptedFile.authenticationTag;
byte[] decryptedMetadataKey = EncryptionUtils.decodeStringToBase64Bytes(EncryptionUtils.decryptStringAsymmetric(
decryptedFolderMetadata.metadata.metadataKeys.get(encryptedFile.metadataKey),
privateKey));
// encrypt
String dataJson = EncryptionUtils.serializeJSON(decryptedFile.encrypted);
encryptedFile.encrypted = 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.metadata,
files);
for (Map.Entry<String, EncryptedFolderMetadata.EncryptedFile> entry : encryptedFolderMetadata.files.entrySet()) {
String key = entry.getKey();
EncryptedFolderMetadata.EncryptedFile encryptedFile = entry.getValue();
DecryptedFolderMetadata.DecryptedFile decryptedFile = new DecryptedFolderMetadata.DecryptedFile();
decryptedFile.initializationVector = encryptedFile.initializationVector;
decryptedFile.metadataKey = encryptedFile.metadataKey;
decryptedFile.authenticationTag = encryptedFile.authenticationTag;
byte[] decryptedMetadataKey = EncryptionUtils.decodeStringToBase64Bytes(EncryptionUtils.decryptStringAsymmetric(
decryptedFolderMetadata.metadata.metadataKeys.get(encryptedFile.metadataKey),
privateKey));
// decrypt
String dataJson = EncryptionUtils.decryptStringSymmetric(encryptedFile.encrypted, decryptedMetadataKey);
decryptedFile.encrypted = 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);
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) {
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#generateIV()}
* @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#generateIV()}
* @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/GCM/NoPadding");
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/GCM/NoPadding");
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/ECB/OAEPWithSHA-256AndMGF1Padding");
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/ECB/OAEPWithSHA-256AndMGF1Padding");
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/GCM/NoPadding");
byte[] iv = generateIV();
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/GCM/NoPadding");
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/GCM/NoPadding");
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.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 encodedEncryptedBytes = encodeBytesToBase64String(encrypted);
return encodedEncryptedBytes + ivDelimiter + encodedIV;
}
/**
* 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 {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
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");
// handle private key
String[] strings = privateKey.split(ivDelimiter);
String realPrivateKey = strings[0];
String iv = strings[1];
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(decodeStringToBase64Bytes(iv)));
byte[] bytes = decodeStringToBase64Bytes(realPrivateKey);
byte[] decrypted = cipher.doFinal(bytes);
return decodeBase64BytesToString(decrypted);
}
/*
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 = (int) (random.nextDouble() * 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) {
e.printStackTrace();
}
return null;
}
public static byte[] generateIV() {
SecureRandom random = new SecureRandom();
final byte[] iv = new byte[16];
random.nextBytes(iv);
return iv;
}
}

View File

@ -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;
}

View File

@ -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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -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>

View File

@ -0,0 +1,59 @@
<?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: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>

View File

@ -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"

File diff suppressed because it is too large Load Diff

View File

@ -695,5 +695,19 @@
<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="dev_version_no_information_available">Keine Information verfügbar!</string>
<string name="dev_version_no_new_version_available">Keine neue Version verfügbar!</string>
<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>

View File

@ -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 -->

View File

@ -268,6 +268,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 +755,17 @@
<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>
</resources>

View File

@ -18,7 +18,9 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.owncloud.android">
package="com.owncloud.android"
android:versionCode="20000052"
android:versionName="2.0.0-e2e-02">
<application
android:name=".MainApp"

View File

@ -0,0 +1,152 @@
/*
* 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.os.Build;
import android.support.annotation.RequiresApi;
import com.google.gson.reflect.TypeToken;
import com.owncloud.android.datamodel.DecryptedFolderMetadata;
import org.junit.Test;
import java.util.HashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class EncryptionTest {
private String encryptedString = "{\"metadata\":{\"encrypted\":\"np1sIwoAFCb/vRqV/jWOIe1UtyVO02EJhvPoh3VwcZuiSbDjwQO5QHUWtKXpHLyX6wfbkRX6nr8mSG0+HhLRud1t126UMxQK5BNINu99WlzPMa5PaKhTXlpuRUf3tR6PTQ\\u003d\\u003d\",\"initializationVector\":" +
"\"kahzfT4u86Knc+e3\",\"sharing\":{\"recipient\":{\"blah@schiessle.org\":\"PUBLIC KEY\"," +
"\"bjoern@schiessle.org\":\"PUBLIC KEY\"},\"signature\":\"HMACOFRECIPIENTANDNEWESTMETADATAKEY\"}," +
"\"version\":1},\"files\":{\"ia7OEEEyXMoRa1QWQk8r\":{\"encrypted\":\"yl52TIccvo62LezCaFjQFJs7a1Q281pOuj59oNXMX7ti+7+h1SjK1AAk1HuwT+CI7BT64+R0ZLgyR/vBPjWvAQuxi9JWgsCjFMX91Mv2m2zI/bNQCarczOfnmf4FZ3Nv6yPLSjShmfQzemQ99Z3g7UHyrZ6pKT18m17IueJHF3V5kOhd9vcH\",\"metadataKey\":0," +
"\"initializationVector\":\"+mHu52HyZq+pAAIN\"}," +
"\"n9WXAIXO2wRY4R8nXwmo\":{\"encrypted\":\"Z9YTAgY/0YqKQlDwiqENcZRRupjgmJ1f0bTy0hOHP2/mHxFtoHCftT4STvt21OJMx8wF6V3cquQIGJ976WxkhA4SQxaQNpznhm1W9e8y+B5x8QnxSasYnOSFtZ/xVgQq6IZRjvYdPo7rvZk49hnqkwnUyvqtCj14aCE42qoxVZCd9M6XaZEBTA\\u003d\\u003d\",\"metadataKey\":0,\"initializationVector\":" +
"\"sOFd17hCKWIv0gyB\"}}}";
private String decryptedString = "{\"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\"}}}";
private String privateKey = "TUlJRXZRSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2N3Z2dTakFnRUFBb0lCQVFEV0FKMDNuNlBLVDNJWlZTT1paSzgyd3dqRTVWSW4vclZ5L0VLbmsyMVgzQ1dPbkdsb0ZrUXdiSWFsYjlkVWNRcmQzM250NVFoMmhJeXM5TkdsMjdMUmhreU1qemYzaFdzbnljSVhZQURVSExweGJZbWpJRzRjbWlNQnI4ckhjVFEvM0dUTlBKWG00ZzUwQlZEOXZlRThoMkQrTXdIQXlTSkxZcG5FaDBKWExES0pvVnpQYldxdVZVdHZNMmdmWVFHNFgwbS9kamR5VzJ4ZGN2UkpZeUtpWDZ3UmY3U3ViaWJkR1V1MHgxckhRdGhnM1dtaXdLd09NR2pQQ3oxRDAwMC9vWjZnckRHcW03TG5XMVZaN0VNTWtWWjVjVlc4NlpGQ2lrcXNtcDd1MVg1YjdCTzBiaThyV2Z0Zm8zZnJaOUptdmkvZHpmeXo2RFRFOWIrZU9CNzlBZ01CQUFFQ2dnRUJBSURYYzlCR1k5VnRDWFBwQjNyVjNJdXExcis4bFQ4UklldHRweSsvR2dqWXVSL29XYW5hSmduRmZUZGpZNUFxVXZHTUY1dTcxZUdOSWlrTGFLRmo2WUF1VEM0Z0dBRUZLYU9WM0M0NGxhY2UrTDFMeHA4WTZsSjhGbkZ3aGpTWG1tNk1ZWUFUWnVqUDF3WFJJWmJ6V0FVYU9MSXl3VzV4YWgxYTZ0c2cyRGNrZ2h3YVoxMURiRTVkNEpNNXl0Q1J6YmZzWTA3cElrUmU4WlFLcm1sMXU0THlsWXVHVHBDZGMwSnNXdUx3d1NWbm8vK056RURiVVNKZThIRmpnM1k3L2JkajgzNE9INzBtQWdBVUdEQTJqQ1BqY1hzNHJnN25udkhuQm5WcVFaemFvVWhHYzFOak9pZzMwNitUV25OL1JUNWc0TThucDNuMXZETWF2dVBiM0lFQ2dZRUE2NVpQV243UlYxZVVVS20zcVRUN1BSa3NuLzZDVWdFaFQ1WDloc0c3cXJXMUVlalhuODE2S1A0R3VsaDc1S0dhQ3hUVmdGZDhHdHF1QVU3MGdaKzB5MHVGd2hTdGtWN1A0K2xlUXlYSmxtNDNWNU9zT3Jxb25ocTZyOCthZ3lGbnNuOEhGd3lVcG1VM0FqR2tXVUNzYWFNdWM4SEt0ajU4RkdGRlh6K3ZlcDBDZ1lFQTZJdUZVZmljLytpUXpsVjczTDdWSS9kRjJNcFRPeHVKb1c4MmM1OE1tdEhGMFFQSktlYU5YVGJuYzluUXZ0aFBWM3c1eEZWc3FkN3pKOEM4ZkJ5S21Xc2ZLOU1abGQvY3RZTk9EMTNLWlM2WTNvWXpGVU5ZRzFPSEhpamNZT3RSWUsvY3k0cS9SRnExQ0pZVCtPaEVTOXlmeGFqZlZXdzFPdGxNZGNuNlYrRUNnWUFwNlN5bTBjYldQZnRodWorMU4zcTJyT0xXZDhXaFp4Z1ErNE1GMVROWXRFakpMZDRtVEx5OXpDdFFQV3VWQ2ZiSW4rVTNsdGk2UWtzUWFvWnZCUVY1NFM2amoyQXRhMnVhaFNyQzBWY2lqdXNEaG43dVY4U2xrK1hBWHpPQ3ZvK2ZIcUFaUnFDdlZYUkt6S0FMVE1rZlpldGVwb3cwamJzdk9QckpiaC8rdFFLQmdIQUFpd1R4RmtVWGNXOC9zdm1lSERCSGI2ZTd3eHlyNWIwUVFJeXRwVGVJSTV2SkZBR1BYclR2dGNpUnR6M0VGMnJPbFZBZnlNZUViMTdOTUxzaVVBc1draHZjZiswMHRpdmlneDFaa2hycnQ0c3QzYnEzQmQrYmVtK25SSVdWc1VzOVNMM3NKTFU2Yndra3A1ZngzcnNmRndEdmxpbWhoWDNEblZUNkpBNWhBb0dBZUtSZGRBVFAxZmVDL1pIZWE0VWh1MEVIencrY2xraDBrdjFDQkNHNnhjM002Y245SzFUNlIwMHRZYlFjS1JvZHJ1ZnBZNUNOL3V2Zyt0VWRucTUxRFhwTU5tbDVMdGpRYnV3MktuZ1F1ZG9NQ1NFMHMzT0dnaGJqMzZtbEVsRERjUGxLaE0rb1hyaVRYdXUvWmljclZqcGxTcWxuSlN2aDh6STJGNFBLaDlnPQ==";
private String publicKey = "-----BEGIN CERTIFICATE-----\n" +
"MIIDpzCCAo+gAwIBAgIBADANBgkqhkiG9w0BAQUFADBuMRowGAYDVQQDDBF3d3cu\n" +
"bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
"dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
"HhcNMTcwOTA0MTEwNTQyWhcNMzcwODMwMTEwNTQyWjBuMRowGAYDVQQDDBF3d3cu\n" +
"bmV4dGNsb3VkLmNvbTESMBAGA1UECgwJTmV4dGNsb3VkMRIwEAYDVQQHDAlTdHV0\n" +
"dGdhcnQxGzAZBgNVBAgMEkJhZGVuLVd1ZXJ0dGVtYmVyZzELMAkGA1UEBhMCREUw\n" +
"ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWAJ03n6PKT3IZVSOZZK82\n" +
"wwjE5VIn/rVy/EKnk21X3CWOnGloFkQwbIalb9dUcQrd33nt5Qh2hIys9NGl27LR\n" +
"hkyMjzf3hWsnycIXYADUHLpxbYmjIG4cmiMBr8rHcTQ/3GTNPJXm4g50BVD9veE8\n" +
"h2D+MwHAySJLYpnEh0JXLDKJoVzPbWquVUtvM2gfYQG4X0m/djdyW2xdcvRJYyKi\n" +
"X6wRf7SubibdGUu0x1rHQthg3WmiwKwOMGjPCz1D000/oZ6grDGqm7LnW1VZ7EMM\n" +
"kVZ5cVW86ZFCikqsmp7u1X5b7BO0bi8rWftfo3frZ9Jmvi/dzfyz6DTE9b+eOB79\n" +
"AgMBAAGjUDBOMB0GA1UdDgQWBBS3zNF86LEZFT/KDdscr4ZJEisXqDAfBgNVHSME\n" +
"GDAWgBS3zNF86LEZFT/KDdscr4ZJEisXqDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\n" +
"DQEBBQUAA4IBAQCPMu5AKNlh0gTr/k9Vc7RJ01uF07D+lTPGIErfW7+qXO21kXyw\n" +
"+w8sxw+e1B/gah/bxotfO7ZuOhs49d8QRUzPy/miBFaZXfjfiqs7UXSDQ6oUbX3a\n" +
"X9eTFMHDcsSUbyqhwn2cghmPJEhE10mtH2DJNPqDYvdpekJ6sEUVaqx63CD3nxcl\n" +
"7fXh0IfmvDQOrSBszRqPY8pvnZJIEwqaENPk9Vgbzs8oXVstKl6wCqM0B36tmhBl\n" +
"f6Dz/EhriF3Rq9w5RrWZOpS6XAWwRpyHPN+lKPa321dF6EEsnvhX8G3UbLbr0uEg\n" +
"dR8lPhuKejU/Ds0ARwQGmFXFzidFNZL5ymos\n" +
"-----END CERTIFICATE-----";
@Test
public void deserializeJSON() {
String file = "ia7OEEEyXMoRa1QWQk8r";
DecryptedFolderMetadata metadata = EncryptionUtils.deserializeJSON(decryptedString,
new TypeToken<DecryptedFolderMetadata>() {});
assertEquals("jtboLmgGR1OQf2uneqCVHpklQLlIwWL5TXAQ0keK", metadata.files.get(file).encrypted.key);
assertEquals("+mHu52HyZq+pAAIN", metadata.files.get(file).initializationVector);
}
@Test
public void serializeJSON() {
try {
HashMap<Integer, String> metadataKeys = new HashMap<>();
metadataKeys.put(0, EncryptionUtils.encryptStringAsymmetric("s4k4LPDpxoO53TKwem3Lo1", publicKey));
metadataKeys.put(1, EncryptionUtils.encryptStringAsymmetric("Q3ZobVJHbTlkK1VHT0g3ME", publicKey));
metadataKeys.put(2, EncryptionUtils.encryptStringAsymmetric("lkK1VHT0g3ME3TKwem3Lo1", publicKey));
DecryptedFolderMetadata.Encrypted encrypted = new DecryptedFolderMetadata.Encrypted();
encrypted.metadataKeys = metadataKeys;
DecryptedFolderMetadata.Metadata metadata1 = new DecryptedFolderMetadata.Metadata();
metadata1.metadataKeys = metadataKeys;
metadata1.version = 1;
DecryptedFolderMetadata.Sharing sharing = new DecryptedFolderMetadata.Sharing();
sharing.signature = "HMACOFRECIPIENTANDNEWESTMETADATAKEY";
HashMap<String, String> recipient = new HashMap<>();
recipient.put("blah@schiessle.org", "PUBLIC KEY");
recipient.put("bjoern@schiessle.org", "PUBLIC KEY");
sharing.recipient = recipient;
metadata1.sharing = sharing;
HashMap<String, DecryptedFolderMetadata.DecryptedFile> files = new HashMap<>();
DecryptedFolderMetadata.Data data1 = new DecryptedFolderMetadata.Data();
data1.key = "jtboLmgGR1OQf2uneqCVHpklQLlIwWL5TXAQ0keK";
data1.filename = "test.txt";
data1.version = 1;
DecryptedFolderMetadata.DecryptedFile file1 = new DecryptedFolderMetadata.DecryptedFile();
file1.initializationVector = "+mHu52HyZq+pAAIN";
file1.encrypted = data1;
file1.metadataKey = 0;
file1.authenticationTag = "HMAC of file";
files.put("ia7OEEEyXMoRa1QWQk8r", file1);
DecryptedFolderMetadata.Data data2 = new DecryptedFolderMetadata.Data();
data2.key = "s4k4LPDpxoO53TKwem3Lo1yJnbNUYH2KLrSFT8Ea";
data2.filename = "test2.txt";
data2.version = 1;
DecryptedFolderMetadata.DecryptedFile file2 = new DecryptedFolderMetadata.DecryptedFile();
file2.initializationVector = "sOFd17hCKWIv0gyB";
file2.encrypted = data2;
file2.metadataKey = 0;
file2.authenticationTag = "HMAC of file";
files.put("n9WXAIXO2wRY4R8nXwmo", file2);
DecryptedFolderMetadata metadata = new DecryptedFolderMetadata(metadata1, files);
// serialize
assertEquals(decryptedString, EncryptionUtils.serializeJSON(metadata));
} catch (Exception e) {
e.printStackTrace();
fail();
}
}
}

View File

@ -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
);