First release of E2E
|
@ -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'
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
1
lint.xml
|
@ -3,6 +3,7 @@
|
|||
<issue id="InvalidPackage">
|
||||
<ignore path="**/freemarker-2.3.23.jar"/>
|
||||
<ignore path="**/nnio-0.2.jar"/>
|
||||
<ignore path="**/pkix-1.54.0.0.jar"/>
|
||||
</issue>
|
||||
|
||||
<issue id="UnusedResources">
|
||||
|
|
|
@ -1 +1 @@
|
|||
include ':'
|
||||
include ':nextcloud-android-library'
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 59 KiB |
|
@ -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
|
|
@ -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));
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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() + ": " +
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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-----";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 5.4 KiB |
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -695,5 +695,19 @@
|
|||
<string name="screenshot_04_accounts">Mit verschiedenen Kontos verbinden</string>
|
||||
<string name="screenshot_05_autoUpload">Automatisches Hochladen von Bildern & Videos</string>
|
||||
<string name="screenshot_06_davdroid">Kalender & 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>
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|