From def35f55e319e3760591877776c43ef978009f3b Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 3 Jan 2018 14:58:14 +0100 Subject: 8773 add provider details for preseeded providers --- app/assets/calyx.net.json | 37 +++++++++++++++++++++++++++ app/assets/calyx.net.pem | 31 +++++++++++++++++++++++ app/assets/demo.bitmask.net.json | 42 +++++++++++++++++++++++++++++++ app/assets/demo.bitmask.net.pem | 32 +++++++++++++++++++++++ app/assets/riseup.net.json | 37 +++++++++++++++++++++++++++ app/assets/riseup.net.pem | 32 +++++++++++++++++++++++ app/assets/urls/bitmask demo.url | 3 --- app/assets/urls/calyx.net.url | 3 +++ app/assets/urls/calyx.url | 3 --- app/assets/urls/demo.bitmask.net.url | 3 +++ app/assets/urls/riseup.net.url | 3 +++ app/assets/urls/riseup.url | 3 --- app/src/main/assets/urls/bitmask demo.url | 3 --- 13 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 app/assets/calyx.net.json create mode 100644 app/assets/calyx.net.pem create mode 100644 app/assets/demo.bitmask.net.json create mode 100644 app/assets/demo.bitmask.net.pem create mode 100644 app/assets/riseup.net.json create mode 100644 app/assets/riseup.net.pem delete mode 100644 app/assets/urls/bitmask demo.url create mode 100644 app/assets/urls/calyx.net.url delete mode 100644 app/assets/urls/calyx.url create mode 100644 app/assets/urls/demo.bitmask.net.url create mode 100644 app/assets/urls/riseup.net.url delete mode 100644 app/assets/urls/riseup.url delete mode 100644 app/src/main/assets/urls/bitmask demo.url diff --git a/app/assets/calyx.net.json b/app/assets/calyx.net.json new file mode 100644 index 00000000..30ab43c5 --- /dev/null +++ b/app/assets/calyx.net.json @@ -0,0 +1,37 @@ +{ + "api_uri": "https://calyx.net:4430", + "api_version": "1", + "ca_cert_fingerprint": "SHA256: 43683c9ba3862c5384a8c1885072fcac40b5d2d4dd67331443f13a3077fa2e69", + "ca_cert_uri": "https://calyx.net/ca.crt", + "default_language": "en", + "description": { + "en": "Calyx Institute privacy focused ISP testbed" + }, + "domain": "calyx.net", + "enrollment_policy": "open", + "languages": [ + "en" + ], + "name": { + "en": "calyx" + }, + "service": { + "allow_anonymous": false, + "allow_free": true, + "allow_limited_bandwidth": false, + "allow_paid": false, + "allow_registration": true, + "allow_unlimited_bandwidth": true, + "bandwidth_limit": 102400, + "default_service_level": 1, + "levels": { + "1": { + "description": "Please donate.", + "name": "free" + } + } + }, + "services": [ + "openvpn" + ] +} \ No newline at end of file diff --git a/app/assets/calyx.net.pem b/app/assets/calyx.net.pem new file mode 100644 index 00000000..cedb2e38 --- /dev/null +++ b/app/assets/calyx.net.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQ0FADBEMQ4wDAYDVQQKDAVjYWx5 +eDEaMBgGA1UECwwRaHR0cHM6Ly9jYWx5eC5uZXQxFjAUBgNVBAMMDWNhbHl4IFJv +b3QgQ0EwHhcNMTMwNzAyMDAwMDAwWhcNMjMwNzAyMDAwMDAwWjBEMQ4wDAYDVQQK +DAVjYWx5eDEaMBgGA1UECwwRaHR0cHM6Ly9jYWx5eC5uZXQxFjAUBgNVBAMMDWNh +bHl4IFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDupdnx +Bgat537XOqrZOulE/RvjoXB1S07sy9/MMtksXFoQuWJZRCSTp1Jaqg3H/e9o1nct +LQO91+izfJe07TUyajFl7CfllYgMeyKTYcT85dFwNX4pcIHZr8UpmO0MpGBoR4W1 +8cPa3vxAG0CsyUmrASJVyhRouk4qazRosM5RwBxTdMzCK7L3SwqPQoxlY9YmRJlD +XYZlK5VMJd0dj9XxhMeFs5n43R0bsDENryrExSbuxoNfnUoQg3wffKk+Z0gW7YgW +ivPsbObqOgXUuBEU0xr9xMNBpU33ffLIsccrHq1EKp8zGfCOcww6v7+zEadUkVLo +6j/rRhYYgRw9lijZG1rMuV/mTGnUqbjHsdoz5mzkFFWeTSqo44lvhveUyCcwRNmi +2sjS77l0fCTzfreufffFoOEcRVMRfsnJdu/xPeARoXILEx8nQ421mSn6spOZlDQr +Tt0T0BAWt+VNc+m0IGSW3SwS7r5MUyQ/M5GrbQBGi5W2SzPriKZ79YTOwPVmXKLZ +vJoEuKRDkEPJLBAhcD5oSQljOm/Wp/hjmRH4HnI1y4XMshWlDsyRDB1Au5yrsfwN +noFVSskEcbXlZfNgml4lktLBqz+qwsw+voq6Ak7ROKbc0ii5s8+iNMbAtIK7GcFF +kuKKIyRmmGlDim/SDhlNdWo7Ah4Akde7zfWufwIDAQABo2AwXjAdBgNVHQ4EFgQU +AY8+K4ZupAQ+L9ttFJG3vaLBq5gwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMB +Af8wHwYDVR0jBBgwFoAUAY8+K4ZupAQ+L9ttFJG3vaLBq5gwDQYJKoZIhvcNAQEN +BQADggIBAOpXi5o3g/2o2rPa53iG7Zgcy8RpePGgZk6xknGYWeLamEqSh+XWQZ2w +2kQP54bf8HfPj3ugJBWsVtYAs/ltJwzeBfYDrwEJd1N8tw2IRuGlQOWiTAVVLBj4 +Zs+dikSuMoA399f/7BlUIEpVLUiV/emTtbkjFnDeKEV9zql6ypR0BtR8Knf8ALvL +YfMsWLvTe4rXeypzxIaE2pn8ttcXLYAX0ml2MofTi5xcDhMn1vznKIvs82xhncQx +I1MJMWqPHNHgJUJpA+y1IFh5LPbpag9PKQ0yQ9sM+/dyGumF2jElsMw71flh/Txr +2dEv8+FNV1pPK26XJZBK24rNWFs30eAFfH9EQCwVla174I4PDoWqsIR7vtQMObDt +Bq34R3TjjJJIt2sCSlYLooWwiK7Q+d/SgYqA+MSDmmwhzm86ToK6cwbCsvuw1AxR +X6VIs4U8wOotgljzX/CSpKqlxcqZjhnAuelZ1+KiN8RHKPj7AzSLYOv/YwTjLTIq +EOxquoNR58uDa5pBG22a7xWbSaKosn/mEl8SrUr6klzzc8Vh09IMoxrw74uLdAg2 +1jnrhm7qg91Ttb0aXiqbV+Kg/qQzojdewnnoBFnv4jaQ3y8zDCfMhsBtWlWz4Knb +Zqga1WyRm3Gj1j6IV0oOincYMrw5YA7bgXpwop/Lo/mmliMA14ps +-----END CERTIFICATE----- \ No newline at end of file diff --git a/app/assets/demo.bitmask.net.json b/app/assets/demo.bitmask.net.json new file mode 100644 index 00000000..e7fe6099 --- /dev/null +++ b/app/assets/demo.bitmask.net.json @@ -0,0 +1,42 @@ +{ + "api_uri": "https://api.demo.bitmask.net:4430", + "api_version": "1", + "ca_cert_fingerprint": "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e", + "ca_cert_uri": "https://demo.bitmask.net/ca.crt", + "default_language": "en", + "description": { + "el": "demo.bitmask.net allows you to test the Bitmask application. User accounts may be periodically deleted.", + "en": "demo.bitmask.net allows you to test the Bitmask application. User accounts may be periodically deleted.", + "es": "demo.bitmask.net allows you to test the Bitmask application. User accounts may be periodically deleted." + }, + "domain": "demo.bitmask.net", + "enrollment_policy": "open", + "languages": [ + "de", + "en", + "es", + "pt" + ], + "name": { + "en": "Bitmask" + }, + "service": { + "allow_anonymous": true, + "allow_free": true, + "allow_limited_bandwidth": false, + "allow_paid": false, + "allow_registration": true, + "allow_unlimited_bandwidth": true, + "bandwidth_limit": 102400, + "default_service_level": 1, + "levels": { + "1": { + "description": "Please donate.", + "name": "free" + } + } + }, + "services": [ + "openvpn" + ] +} \ No newline at end of file diff --git a/app/assets/demo.bitmask.net.pem b/app/assets/demo.bitmask.net.pem new file mode 100644 index 00000000..9a422161 --- /dev/null +++ b/app/assets/demo.bitmask.net.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt +YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v +Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw +FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV +BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai +dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB +7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84 +CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+ +znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4 +MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4 +lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0 +bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl +DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB +lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy +YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw +XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE +MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w +DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl +cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY +k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj +RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG +htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX +EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J +aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l +mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK +G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co +Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d +69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e +yV8e +-----END CERTIFICATE----- \ No newline at end of file diff --git a/app/assets/riseup.net.json b/app/assets/riseup.net.json new file mode 100644 index 00000000..9a5ec79e --- /dev/null +++ b/app/assets/riseup.net.json @@ -0,0 +1,37 @@ +{ + "api_uri": "https://api.black.riseup.net:443", + "api_version": "1", + "ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494", + "ca_cert_uri": "https://black.riseup.net/ca.crt", + "default_language": "en", + "description": { + "en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change." + }, + "domain": "riseup.net", + "enrollment_policy": "open", + "languages": [ + "en" + ], + "name": { + "en": "Riseup Networks" + }, + "service": { + "allow_anonymous": false, + "allow_free": true, + "allow_limited_bandwidth": false, + "allow_paid": false, + "allow_registration": true, + "allow_unlimited_bandwidth": true, + "bandwidth_limit": 102400, + "default_service_level": 1, + "levels": { + "1": { + "description": "Please donate.", + "name": "free" + } + } + }, + "services": [ + "openvpn" + ] +} \ No newline at end of file diff --git a/app/assets/riseup.net.pem b/app/assets/riseup.net.pem new file mode 100644 index 00000000..c890aff4 --- /dev/null +++ b/app/assets/riseup.net.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl +dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE +AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw +NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM +Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv +b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m +TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a +7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE +LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY +iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK +5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx +HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58 +m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF +PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q +hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj +shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k +ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu +f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD +VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB +AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v +qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/ +3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ +4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7 +3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch +Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf +Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg +tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF +tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ +UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp +0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO +-----END CERTIFICATE----- \ No newline at end of file diff --git a/app/assets/urls/bitmask demo.url b/app/assets/urls/bitmask demo.url deleted file mode 100644 index 1a412055..00000000 --- a/app/assets/urls/bitmask demo.url +++ /dev/null @@ -1,3 +0,0 @@ -{ - "main_url" : "https://demo.bitmask.net/" -} diff --git a/app/assets/urls/calyx.net.url b/app/assets/urls/calyx.net.url new file mode 100644 index 00000000..8de04fe9 --- /dev/null +++ b/app/assets/urls/calyx.net.url @@ -0,0 +1,3 @@ +{ + "main_url" : "https://calyx.net/" +} diff --git a/app/assets/urls/calyx.url b/app/assets/urls/calyx.url deleted file mode 100644 index 8de04fe9..00000000 --- a/app/assets/urls/calyx.url +++ /dev/null @@ -1,3 +0,0 @@ -{ - "main_url" : "https://calyx.net/" -} diff --git a/app/assets/urls/demo.bitmask.net.url b/app/assets/urls/demo.bitmask.net.url new file mode 100644 index 00000000..1a412055 --- /dev/null +++ b/app/assets/urls/demo.bitmask.net.url @@ -0,0 +1,3 @@ +{ + "main_url" : "https://demo.bitmask.net/" +} diff --git a/app/assets/urls/riseup.net.url b/app/assets/urls/riseup.net.url new file mode 100644 index 00000000..4548b433 --- /dev/null +++ b/app/assets/urls/riseup.net.url @@ -0,0 +1,3 @@ +{ + "main_url" : "https://riseup.net/" +} diff --git a/app/assets/urls/riseup.url b/app/assets/urls/riseup.url deleted file mode 100644 index 4548b433..00000000 --- a/app/assets/urls/riseup.url +++ /dev/null @@ -1,3 +0,0 @@ -{ - "main_url" : "https://riseup.net/" -} diff --git a/app/src/main/assets/urls/bitmask demo.url b/app/src/main/assets/urls/bitmask demo.url deleted file mode 100644 index 1a412055..00000000 --- a/app/src/main/assets/urls/bitmask demo.url +++ /dev/null @@ -1,3 +0,0 @@ -{ - "main_url" : "https://demo.bitmask.net/" -} -- cgit v1.2.3 From 319589d126dd5e5fa20ee146f52268c99559f04c Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 3 Jan 2018 15:59:30 +0100 Subject: 8773 preseeded providers implementation for production flavor --- .../bitmaskclient/BaseConfigurationWizard.java | 26 +- .../java/se/leap/bitmaskclient/ConfigHelper.java | 49 +++- .../main/java/se/leap/bitmaskclient/Dashboard.java | 37 ++- .../java/se/leap/bitmaskclient/DefaultedURL.java | 1 + .../leap/bitmaskclient/DownloadFailedDialog.java | 86 +++++- .../main/java/se/leap/bitmaskclient/Provider.java | 136 ++++++--- .../se/leap/bitmaskclient/ProviderApiBase.java | 303 +++++++++++++++++---- .../se/leap/bitmaskclient/ProviderManager.java | 53 ++-- app/src/main/res/values/strings.xml | 5 + .../se/leap/bitmaskclient/ConfigurationWizard.java | 35 ++- .../java/se/leap/bitmaskclient/ProviderAPI.java | 187 +++++++++---- 11 files changed, 717 insertions(+), 201 deletions(-) diff --git a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java index 21520dc4..1d675499 100644 --- a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java +++ b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java @@ -56,6 +56,7 @@ import se.leap.bitmaskclient.userstatus.SessionDialog; import static android.view.View.GONE; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; +import static se.leap.bitmaskclient.ProviderApiBase.ERRORS; /** * abstract base Activity that builds and shows the list of known available providers. @@ -184,10 +185,10 @@ public abstract class BaseConfigurationWizard extends Activity // by the height of mProgressbar (and the height of the first list item) mProgressBar.setVisibility(INVISIBLE); progressbar_description.setVisibility(INVISIBLE); - + mProgressBar.setProgress(0); } - private void showProgressBar() { + protected void showProgressBar() { mProgressBar.setVisibility(VISIBLE); progressbar_description.setVisibility(VISIBLE); } @@ -231,12 +232,11 @@ public abstract class BaseConfigurationWizard extends Activity } } else if (resultCode == ProviderAPI.PROVIDER_NOK) { mConfigState.setAction(PROVIDER_NOT_SET); - hideProgressBar(); preferences.edit().remove(Provider.KEY).apply(); setResult(RESULT_CANCELED, mConfigState); - String reason_to_fail = resultData.getString(ProviderAPI.ERRORS); + String reason_to_fail = resultData.getString(ERRORS); showDownloadFailedDialog(reason_to_fail); } else if (resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) { mProgressBar.incrementProgressBy(1); @@ -293,7 +293,9 @@ public abstract class BaseConfigurationWizard extends Activity cancelSettingUpProvider(); } + @Override public void cancelSettingUpProvider() { + hideProgressBar(); mConfigState.setAction(PROVIDER_NOT_SET); adapter.showAllProviders(); preferences.edit().remove(Provider.KEY).remove(Constants.PROVIDER_ALLOW_ANONYMOUS).remove(Constants.PROVIDER_KEY).apply(); @@ -369,18 +371,24 @@ public abstract class BaseConfigurationWizard extends Activity /** * Shows an error dialog, if configuring of a provider failed. * - * @param reason_to_fail + * @param reasonToFail */ - public void showDownloadFailedDialog(String reason_to_fail) { + public void showDownloadFailedDialog(String reasonToFail) { try { FragmentTransaction fragment_transaction = fragment_manager.removePreviousFragment(DownloadFailedDialog.TAG); - - DialogFragment newFragment = DownloadFailedDialog.newInstance(reason_to_fail); + DialogFragment newFragment; + try { + JSONObject errorJson = new JSONObject(reasonToFail); + newFragment = DownloadFailedDialog.newInstance(errorJson); + } catch (JSONException e) { + e.printStackTrace(); + newFragment = DownloadFailedDialog.newInstance(reasonToFail); + } newFragment.show(fragment_transaction, DownloadFailedDialog.TAG); } catch (IllegalStateException e) { e.printStackTrace(); mConfigState.setAction(PENDING_SHOW_FAILED_DIALOG); - mConfigState.putExtra(REASON_TO_FAIL, reason_to_fail); + mConfigState.putExtra(REASON_TO_FAIL, reasonToFail); } } diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java index fd1e2080..ed527a54 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java +++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java @@ -27,6 +27,8 @@ import java.security.cert.*; import java.security.interfaces.*; import java.security.spec.*; +import static android.R.attr.name; + /** * Stores constants, and implements auxiliary methods used across all LEAP Android classes. * @@ -34,6 +36,7 @@ import java.security.spec.*; * @author MeanderingCode */ public class ConfigHelper { + private static final String TAG = ConfigHelper.class.getName(); private static KeyStore keystore_trusted; final public static String NG_1024 = @@ -42,7 +45,7 @@ public class ConfigHelper { public static boolean checkErroneousDownload(String downloaded_string) { try { - if (new JSONObject(downloaded_string).has(ProviderAPI.ERRORS) || downloaded_string.isEmpty()) { + if (downloaded_string == null || downloaded_string.isEmpty() || new JSONObject(downloaded_string).has(ProviderAPI.ERRORS)) { return true; } else { return false; @@ -99,6 +102,38 @@ public class ConfigHelper { return (X509Certificate) certificate; } + + public static String loadInputStreamAsString(InputStream inputStream) { + BufferedReader in = null; + try { + StringBuilder buf = new StringBuilder(); + in = new BufferedReader(new InputStreamReader(inputStream)); + + String str; + boolean isFirst = true; + while ( (str = in.readLine()) != null ) { + if (isFirst) + isFirst = false; + else + buf.append('\n'); + buf.append(str); + } + return buf.toString(); + } catch (IOException e) { + Log.e(TAG, "Error opening asset " + name); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + Log.e(TAG, "Error closing asset " + name); + } + } + } + + return null; + } + protected static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) { RSAPrivateKey key = null; try { @@ -126,6 +161,18 @@ public class ConfigHelper { return key; } + public static String base64toHex(String base64_input) { + byte[] byteArray = Base64.decode(base64_input, Base64.DEFAULT); + int readBytes = byteArray.length; + StringBuffer hexData = new StringBuffer(); + int onebyte; + for (int i = 0; i < readBytes; i++) { + onebyte = ((0x000000ff & byteArray[i]) | 0xffffff00); + hexData.append(Integer.toHexString(onebyte).substring(6)); + } + return hexData.toString(); + } + /** * Adds a new X509 certificate given its input stream and its provider name * diff --git a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java index f1e7b3bd..861ce801 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java +++ b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java @@ -38,9 +38,13 @@ import org.json.JSONObject; import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import butterknife.ButterKnife; import butterknife.InjectView; +import de.blinkt.openvpn.core.VpnStatus; import se.leap.bitmaskclient.userstatus.SessionDialog; import se.leap.bitmaskclient.userstatus.User; import se.leap.bitmaskclient.userstatus.UserStatusFragment; @@ -104,6 +108,11 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec handleVersion(); } + // initialize app necessities + ProviderAPICommand.initialize(this); + VpnStatus.initLogCache(getApplicationContext().getCacheDir()); + User.init(getString(R.string.default_username)); + prepareEIP(savedInstanceState); } @@ -147,6 +156,7 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec try { provider.setUrl(new URL(preferences.getString(Provider.MAIN_URL, ""))); provider.define(new JSONObject(preferences.getString(Provider.KEY, ""))); + provider.setCACert(preferences.getString(Provider.CA_CERT, "")); } catch (MalformedURLException | JSONException e) { e.printStackTrace(); } @@ -246,10 +256,9 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec } @SuppressLint("CommitPrefEdits") private void providerToPreferences(Provider provider) { - //FIXME: figure out why .commit() is used and try refactor that cause, currently runs on UI thread - preferences.edit().putBoolean(Constants.PROVIDER_CONFIGURED, true).commit(); - preferences.edit().putString(Provider.MAIN_URL, provider.mainUrl().toString()).apply(); - preferences.edit().putString(Provider.KEY, provider.definition().toString()).apply(); + preferences.edit().putBoolean(Constants.PROVIDER_CONFIGURED, true). + putString(Provider.MAIN_URL, provider.getMainUrl().toString()). + putString(Provider.KEY, provider.getDefinition().toString()).apply(); } private void configErrorDialog() { @@ -399,7 +408,25 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec private void switchProvider() { if (provider.hasEIP()) eip_fragment.stopEipIfPossible(); - preferences.edit().clear().apply(); + Map allEntries = preferences.getAll(); + List lastProvidersKeys = new ArrayList<>(); + for (Map.Entry entry : allEntries.entrySet()) { + //sort out all preferences that don't belong to the last provider + if (entry.getKey().startsWith(Provider.KEY + ".") || + entry.getKey().startsWith(Provider.CA_CERT + ".") || + entry.getKey().startsWith(Provider.CA_CERT_FINGERPRINT + ".") + ) { + continue; + } + lastProvidersKeys.add(entry.getKey()); + } + + SharedPreferences.Editor preferenceEditor = preferences.edit(); + for (String key : lastProvidersKeys) { + preferenceEditor.remove(key); + } + preferenceEditor.apply(); + switching_provider = false; startActivityForResult(new Intent(this, ConfigurationWizard.class), SWITCH_PROVIDER); } diff --git a/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java b/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java index 8daa7d8c..52c797a4 100644 --- a/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java +++ b/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java @@ -31,6 +31,7 @@ public class DefaultedURL { return url; } + @Override public String toString() { return url.toString(); } diff --git a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java index da32dbd4..6f6a14de 100644 --- a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java +++ b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java @@ -20,6 +20,16 @@ import android.app.*; import android.content.*; import android.os.*; +import org.json.JSONException; +import org.json.JSONObject; + +import se.leap.bitmaskclient.userstatus.SessionDialog; + +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.DEFAULT; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.valueOf; +import static se.leap.bitmaskclient.ProviderApiBase.ERRORID; +import static se.leap.bitmaskclient.ProviderApiBase.ERRORS; + /** * Implements a dialog to show why a download failed. * @@ -29,6 +39,13 @@ public class DownloadFailedDialog extends DialogFragment { public static String TAG = "downloaded_failed_dialog"; private String reason_to_fail; + private DOWNLOAD_ERRORS downloadError = DEFAULT; + public enum DOWNLOAD_ERRORS { + DEFAULT, + ERROR_CORRUPTED_PROVIDER_JSON, + ERROR_INVALID_CERTIFICATE, + ERROR_CERTIFICATE_PINNING + } /** * @return a new instance of this DialogFragment. @@ -39,32 +56,79 @@ public class DownloadFailedDialog extends DialogFragment { return dialog_fragment; } + /** + * @return a new instance of this DialogFragment. + */ + public static DialogFragment newInstance(JSONObject errorJson) { + DownloadFailedDialog dialog_fragment = new DownloadFailedDialog(); + try { + if (errorJson.has(ERRORS)) { + dialog_fragment.reason_to_fail = errorJson.getString(ERRORS); + } else { + //default error msg + dialog_fragment.reason_to_fail = dialog_fragment.getString(R.string.error_io_exception_user_message); + } + + if (errorJson.has(ERRORID)) { + dialog_fragment.downloadError = valueOf(errorJson.getString(ERRORID)); + } + } catch (Exception e) { + e.printStackTrace(); + dialog_fragment.reason_to_fail = dialog_fragment.getString(R.string.error_io_exception_user_message); + } + return dialog_fragment; + } + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setMessage(reason_to_fail) - .setPositiveButton(R.string.retry, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + interface_with_ConfigurationWizard.cancelSettingUpProvider(); + dialog.dismiss(); + } + }); + switch (downloadError) { + case ERROR_CORRUPTED_PROVIDER_JSON: + builder.setPositiveButton(R.string.update_provider_details, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { dismiss(); - interface_with_ConfigurationWizard.retrySetUpProvider(); + interface_with_ConfigurationWizard.updateProviderDetails(); } - }) - .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - interface_with_ConfigurationWizard.cancelSettingUpProvider(); - dialog.dismiss(); + }); + break; + case ERROR_CERTIFICATE_PINNING: + case ERROR_INVALID_CERTIFICATE: + builder.setPositiveButton(R.string.update_certificate, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + interface_with_ConfigurationWizard.updateProviderDetails(); } }); + break; + default: + builder.setPositiveButton(R.string.retry, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dismiss(); + interface_with_ConfigurationWizard.retrySetUpProvider(); + } + }); + break; + } // Create the AlertDialog object and return it return builder.create(); } public interface DownloadFailedDialogInterface { - public void retrySetUpProvider(); + void retrySetUpProvider(); + + void cancelSettingUpProvider(); - public void cancelSettingUpProvider(); + void updateProviderDetails(); } DownloadFailedDialogInterface interface_with_ConfigurationWizard; diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java index 559b47d1..71a0e149 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Provider.java +++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java @@ -18,9 +18,11 @@ package se.leap.bitmaskclient; import android.os.*; +import com.google.gson.Gson; + import org.json.*; -import java.io.*; +import java.io.Serializable; import java.net.*; import java.util.*; @@ -31,8 +33,11 @@ import java.util.*; public final class Provider implements Parcelable { private JSONObject definition = new JSONObject(); // Represents our Provider's provider.json - private DefaultedURL main_url = new DefaultedURL(); - private String certificate_pin = ""; + private DefaultedURL mainUrl = new DefaultedURL(); + private DefaultedURL apiUrl = new DefaultedURL(); + private String certificatePin = ""; + private String certificatePinEncoding = ""; + private String caCert = ""; final public static String API_URL = "api_uri", @@ -61,13 +66,20 @@ public final class Provider implements Parcelable { public Provider() { } - public Provider(URL main_url) { - this.main_url.setUrl(main_url); + public Provider(URL mainUrl) { + this.mainUrl.setUrl(mainUrl); } - public Provider(URL main_url, String certificate_pin) { - this.main_url.setUrl(main_url); - this.certificate_pin = certificate_pin; + public Provider(URL mainUrl, String caCert, /*String certificatePin,*/ String definition) { + this.mainUrl.setUrl(mainUrl); + this.caCert = caCert; + try { + this.definition = new JSONObject(definition); + parseDefinition(this.definition); + } catch (JSONException e) { + e.printStackTrace(); + } + } public static final Parcelable.Creator CREATOR @@ -81,42 +93,57 @@ public final class Provider implements Parcelable { } }; - private Provider(Parcel in) { - try { - main_url.setUrl(new URL(in.readString())); - String definition_string = in.readString(); - if (!definition_string.isEmpty()) - definition = new JSONObject((definition_string)); - } catch (MalformedURLException | JSONException e) { - e.printStackTrace(); - } - } - public boolean isConfigured() { - return !main_url.isDefault() && definition.length() > 0; + return !mainUrl.isDefault() && + definition.length() > 0 && + !apiUrl.isDefault() && + caCert != null && + !caCert.isEmpty(); } protected void setUrl(URL url) { - main_url.setUrl(url); + mainUrl.setUrl(url); } protected void define(JSONObject provider_json) { definition = provider_json; + parseDefinition(definition); } - protected JSONObject definition() { + protected JSONObject getDefinition() { return definition; } protected String getDomain() { - return main_url.getDomain(); + return mainUrl.getDomain(); + } + + protected DefaultedURL getMainUrl() { + return mainUrl; + } + + protected DefaultedURL getApiUrl() { + return apiUrl; + } + + protected String certificatePin() { return certificatePin; } + + protected boolean hasCertificatePin() { + return certificatePin != null && !certificatePin.isEmpty(); + } + + boolean hasCaCert() { + return caCert != null && !caCert.isEmpty(); } - protected DefaultedURL mainUrl() { - return main_url; + public boolean hasDefinition() { + return definition != null && definition.length() > 0; } - protected String certificatePin() { return certificate_pin; } + + public String getCaCert() { + return caCert; + } public String getName() { // Should we pass the locale in, or query the system here? @@ -127,8 +154,8 @@ public final class Provider implements Parcelable { name = definition.getJSONObject(API_TERM_NAME).getString(lang); else throw new JSONException("Provider not defined"); } catch (JSONException e) { - if (main_url != null) { - String host = main_url.getDomain(); + if (mainUrl != null) { + String host = mainUrl.getDomain(); name = host.substring(0, host.indexOf(".")); } } @@ -191,24 +218,29 @@ public final class Provider implements Parcelable { @Override public void writeToParcel(Parcel parcel, int i) { - if(main_url != null) - parcel.writeString(main_url.toString()); + if(mainUrl != null) + parcel.writeString(mainUrl.toString()); if (definition != null) parcel.writeString(definition.toString()); + if (caCert != null) + parcel.writeString(caCert); } @Override public boolean equals(Object o) { if (o instanceof Provider) { Provider p = (Provider) o; - return p.mainUrl().getDomain().equals(mainUrl().getDomain()); + return p.getMainUrl().getDomain().equals(getMainUrl().getDomain()); } else return false; } public JSONObject toJson() { JSONObject json = new JSONObject(); try { - json.put(Provider.MAIN_URL, main_url); + json.put(Provider.MAIN_URL, mainUrl); + //TODO: add other fields here? + //this is used to save custom providers as json. I guess this doesn't work correctly + //TODO 2: verify that } catch (JSONException e) { e.printStackTrace(); } @@ -217,6 +249,44 @@ public final class Provider implements Parcelable { @Override public int hashCode() { - return mainUrl().getDomain().hashCode(); + return getMainUrl().getDomain().hashCode(); + } + + @Override + public String toString() { + return new Gson().toJson(this); + } + + private Provider(Parcel in) { + try { + mainUrl.setUrl(new URL(in.readString())); + String definitionString = in.readString(); + if (!definitionString.isEmpty()) { + definition = new JSONObject((definitionString)); + parseDefinition(definition); + } + String caCert = in.readString(); + if (!caCert.isEmpty()) { + this.caCert = caCert; + } + } catch (MalformedURLException | JSONException e) { + e.printStackTrace(); + } } + + private void parseDefinition(JSONObject definition) { + try { + String pin = definition.getString(CA_CERT_FINGERPRINT); + this.certificatePin = pin.split(":")[1].trim(); + this.certificatePinEncoding = pin.split(":")[0].trim(); + this.apiUrl.setUrl(new URL(definition.getString(API_URL))); + } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) { + e.printStackTrace(); + } + } + + public void setCACert(String cert) { + this.caCert = cert; + } + } diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java index 6e3b8b08..dfc48bee 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java @@ -45,6 +45,8 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.util.ArrayList; @@ -71,6 +73,12 @@ import se.leap.bitmaskclient.userstatus.SessionDialog; import se.leap.bitmaskclient.userstatus.User; import se.leap.bitmaskclient.userstatus.UserStatus; +import static android.text.TextUtils.isEmpty; +import static se.leap.bitmaskclient.ConfigHelper.base64toHex; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE; +import static se.leap.bitmaskclient.Provider.MAIN_URL; import static se.leap.bitmaskclient.R.string.certificate_error; import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; import static se.leap.bitmaskclient.R.string.error_json_exception_user_message; @@ -96,6 +104,7 @@ public abstract class ProviderApiBase extends IntentService { final public static String TAG = ProviderAPI.class.getSimpleName(), SET_UP_PROVIDER = "setUpProvider", + UPDATE_PROVIDER_DETAILS = "updateProviderDetails", DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON", SIGN_UP = "srpRegister", LOG_IN = "srpAuth", @@ -105,6 +114,7 @@ public abstract class ProviderApiBase extends IntentService { RESULT_KEY = "result", RECEIVER_KEY = "receiver", ERRORS = "errors", + ERRORID = "errorId", UPDATE_PROGRESSBAR = "update_progressbar", CURRENT_PROGRESS = "current_progress", DOWNLOAD_EIP_SERVICE = TAG + ".DOWNLOAD_EIP_SERVICE"; @@ -123,16 +133,18 @@ public abstract class ProviderApiBase extends IntentService { CORRECTLY_DOWNLOADED_EIP_SERVICE = 13, INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14; - public static boolean + protected static boolean CA_CERT_DOWNLOADED = false, PROVIDER_JSON_DOWNLOADED = false, EIP_SERVICE_JSON_DOWNLOADED = false; - protected static String last_provider_main_url; + protected static String lastProviderMainUrl; protected static boolean go_ahead = true; protected static SharedPreferences preferences; - protected static String provider_api_url; - protected static String provider_ca_cert_fingerprint; + protected static String providerApiUrl; + protected static String providerCaCertFingerprint; + protected static String providerCaCert; + protected static JSONObject providerDefinition; protected Resources resources; public static void stop() { @@ -155,7 +167,7 @@ public abstract class ProviderApiBase extends IntentService { } public static String lastProviderMainUrl() { - return last_provider_main_url; + return lastProviderMainUrl; } @Override @@ -164,17 +176,26 @@ public abstract class ProviderApiBase extends IntentService { String action = command.getAction(); Bundle parameters = command.getBundleExtra(PARAMETERS); - if (provider_api_url == null && preferences.contains(Provider.KEY)) { + if (providerApiUrl == null && preferences.contains(Provider.KEY)) { try { JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - provider_api_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION); + providerApiUrl = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION); go_ahead = true; } catch (JSONException e) { go_ahead = false; } } - - if (action.equalsIgnoreCase(SET_UP_PROVIDER)) { + if (action.equals(UPDATE_PROVIDER_DETAILS)) { + resetProviderDetails(); + Bundle task = new Bundle(); + task.putString(MAIN_URL, lastProviderMainUrl); + Bundle result = setUpProvider(task); + if (result.getBoolean(RESULT_KEY)) { + receiver.send(PROVIDER_OK, result); + } else { + receiver.send(PROVIDER_NOK, result); + } + } else if (action.equalsIgnoreCase(SET_UP_PROVIDER)) { Bundle result = setUpProvider(parameters); if (go_ahead) { if (result.getBoolean(RESULT_KEY)) { @@ -226,6 +247,13 @@ public abstract class ProviderApiBase extends IntentService { } } + protected void resetProviderDetails() { + CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = false; + deleteProviderDetailsFromPreferences(providerDefinition); + providerCaCert = ""; + providerDefinition = new JSONObject(); + } + protected String formatErrorMessage(final int toastStringId) { return formatErrorMessage(getResources().getString(toastStringId)); } @@ -243,22 +271,31 @@ public abstract class ProviderApiBase extends IntentService { } } - private JSONObject getErrorMessageAsJson(String message) { + protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) { try { - return new JSONObject(formatErrorMessage(message)); + jsonObject.put(ERRORS, errorMessage); } catch (JSONException e) { e.printStackTrace(); - return new JSONObject(); } } - private OkHttpClient initHttpClient(JSONObject initError, boolean isSelfSigned) { + + protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) { + try { + jsonObject.put(ERRORS, errorMessage); + jsonObject.put(ERRORID, errorId); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + private OkHttpClient initHttpClient(JSONObject initError, String certificate) { try { TLSCompatSocketFactory sslCompatFactory; ConnectionSpec spec = getConnectionSpec(); OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); - if (isSelfSigned) { - sslCompatFactory = new TLSCompatSocketFactory(preferences.getString(Provider.CA_CERT, "")); + if (!isEmpty(certificate)) { + sslCompatFactory = new TLSCompatSocketFactory(certificate); } else { sslCompatFactory = new TLSCompatSocketFactory(); } @@ -266,40 +303,39 @@ public abstract class ProviderApiBase extends IntentService { clientBuilder.cookieJar(getCookieJar()) .connectionSpecs(Collections.singletonList(spec)); return clientBuilder.build(); - } catch (IllegalStateException e) { - e.printStackTrace(); - initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage())); - } catch (KeyStoreException e) { + } catch (IllegalArgumentException e) { e.printStackTrace(); - initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage())); - } catch (KeyManagementException e) { + addErrorMessageToJson(initError, resources.getString(R.string.certificate_error)); + } catch (IllegalStateException | KeyManagementException | KeyStoreException e) { e.printStackTrace(); - initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage())); - } catch (NoSuchAlgorithmException e) { + addErrorMessageToJson(initError, String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage())); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { e.printStackTrace(); - initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message)); + addErrorMessageToJson(initError, resources.getString(error_no_such_algorithm_exception_user_message)); } catch (CertificateException e) { e.printStackTrace(); - initError = getErrorMessageAsJson(resources.getString(certificate_error)); + addErrorMessageToJson(initError, resources.getString(certificate_error)); } catch (UnknownHostException e) { e.printStackTrace(); - initError = getErrorMessageAsJson(resources.getString(server_unreachable_message)); + addErrorMessageToJson(initError, resources.getString(server_unreachable_message)); } catch (IOException e) { e.printStackTrace(); - initError = getErrorMessageAsJson(resources.getString(error_io_exception_user_message)); - } catch (NoSuchProviderException e) { - e.printStackTrace(); - initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message)); + addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message)); } return null; } protected OkHttpClient initCommercialCAHttpClient(JSONObject initError) { - return initHttpClient(initError, false); + return initHttpClient(initError, null); } protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) { - return initHttpClient(initError, true); + String certificate = preferences.getString(Provider.CA_CERT, ""); + return initHttpClient(initError, certificate); + } + + protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError, String certificate) { + return initHttpClient(initError, certificate); } @NonNull @@ -376,7 +412,7 @@ public abstract class ProviderApiBase extends IntentService { BigInteger password_verifier = client.calculateV(username, password, salt); - JSONObject api_result = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); + JSONObject api_result = sendNewUserDataToSRPServer(providerApiUrl, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); Bundle result = new Bundle(); if (api_result.has(ERRORS)) @@ -431,13 +467,13 @@ public abstract class ProviderApiBase extends IntentService { LeapSRPSession client = new LeapSRPSession(username, password); byte[] A = client.exponential(); - JSONObject step_result = sendAToSRPServer(provider_api_url, username, new BigInteger(1, A).toString(16), okHttpClient); + JSONObject step_result = sendAToSRPServer(providerApiUrl, username, new BigInteger(1, A).toString(16), okHttpClient); try { String salt = step_result.getString(LeapSRPSession.SALT); byte[] Bbytes = new BigInteger(step_result.getString("B"), 16).toByteArray(); byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes); if (M1 != null) { - step_result = sendM1ToSRPServer(provider_api_url, username, M1, okHttpClient); + step_result = sendM1ToSRPServer(providerApiUrl, username, M1, okHttpClient); setTokenIfAvailable(step_result); byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray(); if (client.verify(M2)) { @@ -629,6 +665,9 @@ public abstract class ProviderApiBase extends IntentService { try { response = okHttpClient.newCall(request).execute(); + if (!response.isSuccessful()){ + return formatErrorMessage(error_json_exception_user_message); + } InputStream inputStream = response.body().byteStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); @@ -638,12 +677,10 @@ public abstract class ProviderApiBase extends IntentService { } catch (NullPointerException npe) { plainResponseBody = formatErrorMessage(error_json_exception_user_message); - } catch (UnknownHostException e) { + } catch (UnknownHostException | SocketTimeoutException e) { plainResponseBody = formatErrorMessage(server_unreachable_message); } catch (MalformedURLException e) { plainResponseBody = formatErrorMessage(malformed_url); - } catch (SocketTimeoutException e) { - plainResponseBody = formatErrorMessage(server_unreachable_message); } catch (SSLHandshakeException e) { plainResponseBody = formatErrorMessage(certificate_error); } catch (ConnectException e) { @@ -693,6 +730,7 @@ public abstract class ProviderApiBase extends IntentService { } catch(JSONException e) { return false; } catch(NullPointerException e) { + e.printStackTrace(); return false; } } @@ -714,11 +752,7 @@ public abstract class ProviderApiBase extends IntentService { result = real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim()); } else result = false; - } catch (JSONException e) { - result = false; - } catch (NoSuchAlgorithmException e) { - result = false; - } catch (CertificateEncodingException e) { + } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) { result = false; } } @@ -726,16 +760,179 @@ public abstract class ProviderApiBase extends IntentService { return result; } - private String base64toHex(String base64_input) { - byte[] byteArray = Base64.decode(base64_input, Base64.DEFAULT); - int readBytes = byteArray.length; - StringBuffer hexData = new StringBuffer(); - int onebyte; - for (int i = 0; i < readBytes; i++) { - onebyte = ((0x000000ff & byteArray[i]) | 0xffffff00); - hexData.append(Integer.toHexString(onebyte).substring(6)); + protected Bundle validateCertificateForProvider(String cert_string, JSONObject providerDefinition, String mainUrl) { + Bundle result = new Bundle(); + result.putBoolean(RESULT_KEY, false); + + if (ConfigHelper.checkErroneousDownload(cert_string)) { + return result; + } + + X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string); + if (certificate == null) { + return setErrorResult(result, getString(R.string.warning_corrupted_provider_cert), ERROR_INVALID_CERTIFICATE.toString()); + } + try { + certificate.checkValidity(); + String fingerprint = getCaCertFingerprint(providerDefinition); + String encoding = fingerprint.split(":")[0]; + String expected_fingerprint = fingerprint.split(":")[1]; + String real_fingerprint = base64toHex(Base64.encodeToString( + MessageDigest.getInstance(encoding).digest(certificate.getEncoded()), + Base64.DEFAULT)); + if (!real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim())) { + return setErrorResult(result, getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); + } + + if (!hasApiUrlExpectedDomain(providerDefinition, mainUrl)){ + return setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + } + + if (!canConnect(cert_string, providerDefinition, result)) { + return result; + } + } catch (NoSuchAlgorithmException e ) { + return setErrorResult(result, resources.getString(error_no_such_algorithm_exception_user_message), null); + } catch (ArrayIndexOutOfBoundsException e) { + return setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) { + return setErrorResult(result, getString(R.string.warning_expired_provider_cert), ERROR_INVALID_CERTIFICATE.toString()); + } + + result.putBoolean(RESULT_KEY, true); + return result; + } + + protected Bundle setErrorResult(Bundle result, String errorMessage, String errorId) { + JSONObject errorJson = new JSONObject(); + if (errorId != null) { + addErrorMessageToJson(errorJson, errorMessage, errorId); + } else { + addErrorMessageToJson(errorJson, errorMessage); + } + result.putString(ERRORS, errorJson.toString()); + return result; + } + + /** + * This method aims to prevent attacks where the provider.json file got manipulated by a third party. + * The main url is visible to the provider when setting up a new provider. + * The user is responsible to check that this is the provider main url he intends to connect to. + * + * @param providerDefinition + * @param mainUrlString + * @return + */ + private boolean hasApiUrlExpectedDomain(JSONObject providerDefinition, String mainUrlString) { + // fix against "api_uri": "https://calyx.net.malicious.url.net:4430", + String apiUrlString = getApiUrl(providerDefinition); + String providerDomain = getProviderDomain(providerDefinition); + if (mainUrlString.contains(providerDomain) && apiUrlString.contains(providerDomain + ":")) { + return true; + } + return false; + } + + private boolean canConnect(String caCert, JSONObject providerDefinition, Bundle result) { + JSONObject errorJson = new JSONObject(); + String baseUrl = getApiUrl(providerDefinition); + + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert); + if (okHttpClient == null) { + result.putString(ERRORS, errorJson.toString()); + return false; } - return hexData.toString(); + + List> headerArgs = getAuthorizationHeader(); + String plain_response = requestStringFromServer(baseUrl, "GET", null, headerArgs, okHttpClient); + + try { + if (new JSONObject(plain_response).has(ERRORS)) { + result.putString(ERRORS, plain_response); + return false; + } + } catch (JSONException e) { + //eat me + } + + return true; + } + + protected String getCaCertFingerprint(JSONObject providerDefinition) { + try { + return providerDefinition.getString(Provider.CA_CERT_FINGERPRINT); + } catch (JSONException e) { + e.printStackTrace(); + } + return ""; + } + + protected String getApiUrl(JSONObject providerDefinition) { + try { + return providerDefinition.getString(Provider.API_URL); + } catch (JSONException e) { + e.printStackTrace(); + } + return ""; + } + + protected String getApiUrlWithVersion(JSONObject providerDefinition) { + try { + return providerDefinition.getString(Provider.API_URL) + "/" + providerDefinition.getString(Provider.API_VERSION); + } catch (JSONException e) { + e.printStackTrace(); + } + return ""; + } + + protected void deleteProviderDetailsFromPreferences(JSONObject providerDefinition) { + String providerDomain = getProviderDomain(providerDefinition); + + if (preferences.contains(Provider.KEY + "." + providerDomain)) { + preferences.edit().remove(Provider.KEY + "." + providerDomain).apply(); + } + if (preferences.contains(Provider.CA_CERT + "." + providerDomain)) { + preferences.edit().remove(Provider.CA_CERT + "." + providerDomain).apply(); + } + if (preferences.contains(Provider.CA_CERT_FINGERPRINT + "." + providerDomain)) { + preferences.edit().remove(Provider.CA_CERT_FINGERPRINT + "." + providerDomain).apply(); + } + } + + protected String getPersistedCaCertFingerprint(String providerDomain) { + try { + return getPersistedProviderDefinition(providerDomain).getString(Provider.CA_CERT_FINGERPRINT); + } catch (JSONException e) { + e.printStackTrace(); + } + return ""; + } + + protected JSONObject getPersistedProviderDefinition(String providerDomain) { + try { + return new JSONObject(preferences.getString(Provider.KEY + "." + providerDomain, "")); + } catch (JSONException e) { + e.printStackTrace(); + return new JSONObject(); + } + } + + protected String getPersistedProviderCA(String providerDomain) { + return preferences.getString(Provider.CA_CERT + "." + providerDomain, ""); + } + + protected String getProviderDomain(JSONObject providerDefinition) { + try { + return providerDefinition.getString(Provider.DOMAIN); + } catch (JSONException e) { + e.printStackTrace(); + } + + return ""; + } + + protected boolean hasUpdatedProviderDetails(String providerDomain) { + return preferences.contains(Provider.KEY + "." + providerDomain) && preferences.contains(Provider.CA_CERT + "." + providerDomain); } /** @@ -774,7 +971,7 @@ public abstract class ProviderApiBase extends IntentService { return false; } - String deleteUrl = provider_api_url + "/logout"; + String deleteUrl = providerApiUrl + "/logout"; int progress = 0; Request.Builder requestBuilder = new Request.Builder() diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java index abbdeb66..cf703631 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java @@ -1,20 +1,31 @@ package se.leap.bitmaskclient; -import android.content.res.*; - -import com.pedrogomez.renderers.*; - -import org.json.*; - -import java.io.*; -import java.net.*; -import java.util.*; +import android.content.res.AssetManager; + +import com.pedrogomez.renderers.AdapteeCollection; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; /** * Created by parmegv on 4/12/14. */ public class ProviderManager implements AdapteeCollection { + private static final String TAG = ProviderManager.class.getName(); private AssetManager assets_manager; private File external_files_dir; private Set default_providers; @@ -49,13 +60,13 @@ public class ProviderManager implements AdapteeCollection { Set providers = new HashSet(); try { for (String file : relative_file_paths) { + + String provider = file.substring(0, file.length() - ".url".length()); InputStream provider_file = assets_manager.open(directory + "/" + file); - String main_url = extractMainUrlFromInputStream(provider_file); - String certificate_pin = extractCertificatePinFromInputStream(provider_file); - if(certificate_pin.isEmpty()) - providers.add(new Provider(new URL(main_url))); - else - providers.add(new Provider(new URL(main_url), certificate_pin)); + String mainUrl = extractMainUrlFromInputStream(provider_file); + String certificate = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".pem")); + String providerDefinition = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".json")); + providers.add(new Provider(new URL(mainUrl), certificate, providerDefinition)); } } catch (IOException e) { e.printStackTrace(); @@ -89,21 +100,11 @@ public class ProviderManager implements AdapteeCollection { String main_url = ""; JSONObject file_contents = inputStreamToJson(input_stream); - if(file_contents != null) + if (file_contents != null) main_url = file_contents.optString(Provider.MAIN_URL); return main_url; } - private String extractCertificatePinFromInputStream(InputStream input_stream) { - String certificate_pin = ""; - - JSONObject file_contents = inputStreamToJson(input_stream); - if(file_contents != null) - certificate_pin = file_contents.optString(Provider.CA_CERT_FINGERPRINT); - - return certificate_pin; - } - private JSONObject inputStreamToJson(InputStream input_stream) { JSONObject json = null; try { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 70f6f4ab..680f92e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,4 +82,9 @@ Failed to establish blocking VPN. Stopped blocking all outgoing internet traffic. Blocking traffic + Update provider details + Update certificate + Stored provider details are corrupted. You can either update Bitmask (recommended) or update the provider details using a commercial CA certificate. + Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate. + Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate. diff --git a/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java b/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java index fc2569b2..363fa66c 100644 --- a/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java +++ b/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java @@ -63,8 +63,16 @@ public class ConfigurationWizard extends BaseConfigurationWizard { mConfigState.setAction(SETTING_UP_PROVIDER); Intent provider_API_command = new Intent(this, ProviderAPI.class); Bundle parameters = new Bundle(); - parameters.putString(Provider.MAIN_URL, selected_provider.mainUrl().toString()); - parameters.putString(Provider.CA_CERT_FINGERPRINT, selected_provider.certificatePin()); + parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString()); + if (selected_provider.hasCertificatePin()){ + parameters.putString(Provider.CA_CERT_FINGERPRINT, selected_provider.certificatePin()); + } + if (selected_provider.hasCaCert()) { + parameters.putString(Provider.CA_CERT, selected_provider.getCaCert()); + } + if (selected_provider.hasDefinition()) { + parameters.putString(Provider.KEY, selected_provider.getDefinition().toString()); + } provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); @@ -73,18 +81,39 @@ public class ConfigurationWizard extends BaseConfigurationWizard { startService(provider_API_command); } + @Override public void retrySetUpProvider() { cancelSettingUpProvider(); if (!ProviderAPI.caCertDownloaded()) { addAndSelectNewProvider(ProviderAPI.lastProviderMainUrl()); } else { - Intent provider_API_command = new Intent(this, ProviderAPI.class); + showProgressBar(); + adapter.hideAllBut(adapter.indexOf(selected_provider)); + + Intent provider_API_command = new Intent(this, ProviderAPI.class); provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver); + Bundle parameters = new Bundle(); + parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString()); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); startService(provider_API_command); } } + @Override + public void updateProviderDetails() { + mConfigState.setAction(SETTING_UP_PROVIDER); + Intent provider_API_command = new Intent(this, ProviderAPI.class); + + provider_API_command.setAction(ProviderAPI.UPDATE_PROVIDER_DETAILS); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver); + Bundle parameters = new Bundle(); + parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString()); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + + startService(provider_API_command); + } + } diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java index fadb03c3..b27c3dca 100644 --- a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java @@ -22,20 +22,15 @@ import android.util.Pair; import org.json.JSONException; import org.json.JSONObject; -import org.thoughtcrime.ssl.pinning.util.PinningHelper; import java.io.IOException; import java.net.URL; import java.util.List; -import java.util.Scanner; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLHandshakeException; import okhttp3.OkHttpClient; import se.leap.bitmaskclient.eip.EIP; -import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON; import static se.leap.bitmaskclient.R.string.malformed_url; /** @@ -60,72 +55,139 @@ public class ProviderAPI extends ProviderApiBase { @Override protected Bundle setUpProvider(Bundle task) { int progress = 0; - Bundle current_download = new Bundle(); + Bundle currentDownload = new Bundle(); - if (task != null && task.containsKey(Provider.MAIN_URL)) { - last_provider_main_url = task.containsKey(Provider.MAIN_URL) ? + if (task != null) { + //FIXME: this should be refactored in order to avoid static variables all over here + lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ? task.getString(Provider.MAIN_URL) : ""; - provider_ca_cert_fingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ? + providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ? task.getString(Provider.CA_CERT_FINGERPRINT) : ""; + providerCaCert = task.containsKey(Provider.CA_CERT) ? + task.getString(Provider.CA_CERT) : + ""; - CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = EIP_SERVICE_JSON_DOWNLOADED = false; + try { + providerDefinition = task.containsKey(Provider.KEY) ? + new JSONObject(task.getString(Provider.KEY)) : + new JSONObject(); + } catch (JSONException e) { + e.printStackTrace(); + providerDefinition = new JSONObject(); + } + providerApiUrl = getApiUrlWithVersion(providerDefinition); + + checkPersistedProviderUpdates(); + currentDownload = validateProviderDetails(); + + //provider details invalid + if (currentDownload.containsKey(ERRORS)) { + return currentDownload; + } + + //no provider certificate available + if (currentDownload.containsKey(RESULT_KEY) && !currentDownload.getBoolean(RESULT_KEY)) { + resetProviderDetails(); + } + + EIP_SERVICE_JSON_DOWNLOADED = false; go_ahead = true; } if (!PROVIDER_JSON_DOWNLOADED) - current_download = getAndSetProviderJson(last_provider_main_url, provider_ca_cert_fingerprint); - if (PROVIDER_JSON_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) { + currentDownload = getAndSetProviderJson(lastProviderMainUrl, providerCaCert, providerDefinition); + if (PROVIDER_JSON_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { broadcastProgress(progress++); PROVIDER_JSON_DOWNLOADED = true; if (!CA_CERT_DOWNLOADED) - current_download = downloadCACert(); - if (CA_CERT_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) { + currentDownload = downloadCACert(); + if (CA_CERT_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { broadcastProgress(progress++); CA_CERT_DOWNLOADED = true; - current_download = getAndSetEipServiceJson(); - if (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY)) { + currentDownload = getAndSetEipServiceJson(); + if (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY)) { broadcastProgress(progress++); EIP_SERVICE_JSON_DOWNLOADED = true; } } } - return current_download; + return currentDownload; } - private Bundle getAndSetProviderJson(String provider_main_url, String provider_ca_cert_fingerprint) { + + private Bundle validateProviderDetails() { + Bundle result = validateCertificateForProvider(providerCaCert, providerDefinition, lastProviderMainUrl); + + //invalid certificate or no certificate + if (result.containsKey(ERRORS) || (result.containsKey(RESULT_KEY) && !result.getBoolean(RESULT_KEY)) ) { + return result; + } + + //valid certificate: skip download, save loaded provider CA cert and provider definition directly + try { + preferences.edit().putString(Provider.KEY, providerDefinition.toString()). + putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). + putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). + putString(Provider.CA_CERT, providerCaCert).commit(); + CA_CERT_DOWNLOADED = true; + PROVIDER_JSON_DOWNLOADED = true; + result.putBoolean(RESULT_KEY, true); + } catch (JSONException e) { + e.printStackTrace(); + result.putBoolean(RESULT_KEY, false); + result = setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + } + + return result; + } + + private void checkPersistedProviderUpdates() { + String providerDomain = getProviderDomain(providerDefinition); + if (hasUpdatedProviderDetails(providerDomain)) { + providerCaCert = getPersistedProviderCA(providerDomain); + providerDefinition = getPersistedProviderDefinition(providerDomain); + providerCaCertFingerprint = getPersistedCaCertFingerprint(providerDomain); + providerApiUrl = getApiUrlWithVersion(providerDefinition); + } + } + + + private Bundle getAndSetProviderJson(String providerMainUrl, String caCert, JSONObject providerDefinition) { Bundle result = new Bundle(); if (go_ahead) { - String provider_dot_json_string; - if(provider_ca_cert_fingerprint.isEmpty()) - provider_dot_json_string = downloadWithCommercialCA(provider_main_url + "/provider.json"); - else - provider_dot_json_string = downloadWithCommercialCA(provider_main_url + "/provider.json", provider_ca_cert_fingerprint); + String providerDotJsonString; + if(providerDefinition.length() == 0 || caCert.isEmpty()) + providerDotJsonString = downloadWithCommercialCA(providerMainUrl + "/provider.json"); + else { + providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition); + } - if (!isValidJson(provider_dot_json_string)) { + if (!isValidJson(providerDotJsonString)) { result.putString(ERRORS, getString(malformed_url)); result.putBoolean(RESULT_KEY, false); return result; } try { - JSONObject provider_json = new JSONObject(provider_dot_json_string); - provider_api_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION); - String name = provider_json.getString(Provider.NAME); + JSONObject providerJson = new JSONObject(providerDotJsonString); + String providerDomain = providerJson.getString(Provider.DOMAIN); + providerApiUrl = getApiUrlWithVersion(providerJson); + String name = providerJson.getString(Provider.NAME); //TODO setProviderName(name); - preferences.edit().putString(Provider.KEY, provider_json.toString()).commit(); - preferences.edit().putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, provider_json.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)).commit(); - preferences.edit().putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, provider_json.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)).commit(); - + preferences.edit().putString(Provider.KEY, providerJson.toString()). + putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). + putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). + putString(Provider.KEY + "." + providerDomain, providerJson.toString()).commit(); result.putBoolean(RESULT_KEY, true); } catch (JSONException e) { //TODO Error message should be contained in that provider_dot_json_string - String reason_to_fail = pickErrorMessage(provider_dot_json_string); + String reason_to_fail = pickErrorMessage(providerDotJsonString); result.putString(ERRORS, reason_to_fail); result.putBoolean(RESULT_KEY, false); } @@ -176,7 +238,7 @@ public class ProviderAPI extends ProviderApiBase { String cert_string = downloadWithProviderCA(new_cert_string_url.toString()); - if (cert_string == null || cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string)) + if (ConfigHelper.checkErroneousDownload(cert_string)) return false; else return loadCertificate(cert_string); @@ -194,13 +256,16 @@ public class ProviderAPI extends ProviderApiBase { private Bundle downloadCACert() { Bundle result = new Bundle(); try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - String ca_cert_url = provider_json.getString(Provider.CA_CERT_URI); - String cert_string = downloadWithCommercialCA(ca_cert_url); + JSONObject providerJson = new JSONObject(preferences.getString(Provider.KEY, "")); + String caCertUrl = providerJson.getString(Provider.CA_CERT_URI); + String providerDomain = providerJson.getString(Provider.DOMAIN); + + String cert_string = downloadWithCommercialCA(caCertUrl); result.putBoolean(RESULT_KEY, true); if (validCertificate(cert_string) && go_ahead) { preferences.edit().putString(Provider.CA_CERT, cert_string).commit(); + preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, cert_string).commit(); result.putBoolean(RESULT_KEY, true); } else { String reason_to_fail = pickErrorMessage(cert_string); @@ -216,29 +281,6 @@ public class ProviderAPI extends ProviderApiBase { return result; } - //TODO: refactor with ticket #8773 - private String downloadWithCommercialCA(String url_string, String ca_cert_fingerprint) { - String result = ""; - - int seconds_of_timeout = 2; - String[] pins = new String[] {ca_cert_fingerprint}; - try { - URL url = new URL(url_string); - HttpsURLConnection connection = PinningHelper.getPinnedHttpsURLConnection(Dashboard.getContext(), pins, url); - connection.setConnectTimeout(seconds_of_timeout * 1000); - if (!LeapSRPSession.getToken().isEmpty()) - connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); - result = new Scanner(connection.getInputStream()).useDelimiter("\\A").next(); - } catch (IOException e) { - if(e instanceof SSLHandshakeException) - result = formatErrorMessage(R.string.error_security_pinnedcertificate); - else - result = formatErrorMessage(error_io_exception_user_message); - } - - return result; - } - /** * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider. * @@ -273,6 +315,31 @@ public class ProviderAPI extends ProviderApiBase { return responseString; } + + /** + * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. + * + * @return an empty string if it fails, the response body if not. + */ + protected String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition) { + String responseString; + JSONObject errorJson = new JSONObject(); + String baseUrl = getApiUrl(providerDefinition); + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert); + if (okHttpClient == null) { + return errorJson.toString(); + } + + String urlString = baseUrl + path; + List> headerArgs = getAuthorizationHeader(); + responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); + + return responseString; + + } + + + /** * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. * -- cgit v1.2.3 From 67c375afcd7d2e62cdf761f4934860938ae29235 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 3 Jan 2018 16:00:10 +0100 Subject: fix minor bugs --- app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java | 2 +- app/src/main/java/se/leap/bitmaskclient/StartActivity.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java index c5a6e809..fc763c7d 100644 --- a/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java +++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java @@ -37,7 +37,7 @@ public class TestEIP extends ServiceTestCase { super(activityClass); context = getSystemContext(); intent = new Intent(context, EIP.class); - preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, Context.MODE_PRIVATE); + preferences = context.getSharedPreferences(Constants.SHARED_PREFERENCES, Context.MODE_PRIVATE); } @Override diff --git a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java index dd2be212..2bfe650a 100644 --- a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java +++ b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java @@ -62,7 +62,7 @@ public class StartActivity extends Activity { } // initialize app necessities - ProviderAPICommand.initialize(this); + ProviderAPICommand.initialize(getApplicationContext()); VpnStatus.initLogCache(getApplicationContext().getCacheDir()); User.init(getString(R.string.default_username)); -- cgit v1.2.3 From 81a732702f7b3125ac543f92d8a5ec33cce972fe Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 4 Jan 2018 13:23:58 +0100 Subject: 8773 preseeded providers implementation for insecure flavor --- .../se/leap/bitmaskclient/ConfigurationWizard.java | 19 ++- .../java/se/leap/bitmaskclient/ProviderAPI.java | 163 +++++++++++++-------- .../bitmaskclient/BaseConfigurationWizard.java | 16 ++ .../main/java/se/leap/bitmaskclient/Provider.java | 18 ++- .../se/leap/bitmaskclient/ProviderApiBase.java | 39 ++++- .../se/leap/bitmaskclient/ProviderManager.java | 30 ++-- .../se/leap/bitmaskclient/ConfigurationWizard.java | 14 -- .../java/se/leap/bitmaskclient/ProviderAPI.java | 42 +----- 8 files changed, 199 insertions(+), 142 deletions(-) diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ConfigurationWizard.java b/app/src/insecure/java/se/leap/bitmaskclient/ConfigurationWizard.java index df1a59ff..766b6c60 100644 --- a/app/src/insecure/java/se/leap/bitmaskclient/ConfigurationWizard.java +++ b/app/src/insecure/java/se/leap/bitmaskclient/ConfigurationWizard.java @@ -89,9 +89,17 @@ public class ConfigurationWizard extends BaseConfigurationWizard { mConfigState.setAction(SETTING_UP_PROVIDER); Intent provider_API_command = new Intent(this, ProviderAPI.class); Bundle parameters = new Bundle(); - parameters.putString(Provider.MAIN_URL, selected_provider.mainUrl().getUrl().toString()); + parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString()); parameters.putBoolean(ProviderItem.DANGER_ON, danger_on); - parameters.putString(Provider.CA_CERT_FINGERPRINT, selected_provider.certificatePin()); + if (selected_provider.hasCertificatePin()){ + parameters.putString(Provider.CA_CERT_FINGERPRINT, selected_provider.certificatePin()); + } + if (selected_provider.hasCaCert()) { + parameters.putString(Provider.CA_CERT, selected_provider.getCaCert()); + } + if (selected_provider.hasDefinition()) { + parameters.putString(Provider.KEY, selected_provider.getDefinition().toString()); + } provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); @@ -103,15 +111,22 @@ public class ConfigurationWizard extends BaseConfigurationWizard { /** * Retrys setup of last used provider, allows bypassing ca certificate validation. */ + @Override public void retrySetUpProvider() { cancelSettingUpProvider(); if (!ProviderAPI.caCertDownloaded()) { addAndSelectNewProvider(ProviderAPI.lastProviderMainUrl(), ProviderAPI.lastDangerOn()); } else { + showProgressBar(); + adapter.hideAllBut(adapter.indexOf(selected_provider)); + Intent provider_API_command = new Intent(this, ProviderAPI.class); provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver); + Bundle parameters = new Bundle(); + parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString()); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); startService(provider_API_command); } diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java index 7689c343..5cb06115 100644 --- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java @@ -21,7 +21,6 @@ import android.util.Pair; import org.json.JSONException; import org.json.JSONObject; -import org.thoughtcrime.ssl.pinning.util.PinningHelper; import java.io.FileNotFoundException; import java.io.IOException; @@ -38,7 +37,6 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; @@ -48,7 +46,6 @@ import se.leap.bitmaskclient.ProviderListContent.ProviderItem; import se.leap.bitmaskclient.eip.EIP; import static se.leap.bitmaskclient.R.string.certificate_error; -import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; import static se.leap.bitmaskclient.R.string.malformed_url; /** @@ -64,10 +61,10 @@ import static se.leap.bitmaskclient.R.string.malformed_url; */ public class ProviderAPI extends ProviderApiBase { - private static boolean last_danger_on = true; + private static boolean lastDangerOn = true; public static boolean lastDangerOn() { - return last_danger_on; + return lastDangerOn; } /** @@ -79,71 +76,99 @@ public class ProviderAPI extends ProviderApiBase { @Override protected Bundle setUpProvider(Bundle task) { int progress = 0; - Bundle current_download = new Bundle(); + Bundle currentDownload = new Bundle(); if (task != null) { - last_danger_on = task.containsKey(ProviderItem.DANGER_ON) && task.getBoolean(ProviderItem.DANGER_ON); - last_provider_main_url = task.containsKey(Provider.MAIN_URL) ? + lastDangerOn = task.containsKey(ProviderItem.DANGER_ON) && task.getBoolean(ProviderItem.DANGER_ON); + lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ? task.getString(Provider.MAIN_URL) : ""; - provider_ca_cert_fingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ? + providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ? task.getString(Provider.CA_CERT_FINGERPRINT) : ""; - CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = EIP_SERVICE_JSON_DOWNLOADED = false; + providerCaCert = task.containsKey(Provider.CA_CERT) ? + task.getString(Provider.CA_CERT) : + ""; + + try { + providerDefinition = task.containsKey(Provider.KEY) ? + new JSONObject(task.getString(Provider.KEY)) : + new JSONObject(); + } catch (JSONException e) { + e.printStackTrace(); + providerDefinition = new JSONObject(); + } + providerApiUrl = getApiUrlWithVersion(providerDefinition); + + checkPersistedProviderUpdates(); + currentDownload = validateProviderDetails(); + + //provider details invalid + if (currentDownload.containsKey(ERRORS)) { + return currentDownload; + } + + //no provider certificate available + if (currentDownload.containsKey(RESULT_KEY) && !currentDownload.getBoolean(RESULT_KEY)) { + resetProviderDetails(); + } + + EIP_SERVICE_JSON_DOWNLOADED = false; go_ahead = true; } if (!PROVIDER_JSON_DOWNLOADED) - current_download = getAndSetProviderJson(last_provider_main_url, last_danger_on, provider_ca_cert_fingerprint); - if (PROVIDER_JSON_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) { + currentDownload = getAndSetProviderJson(lastProviderMainUrl, lastDangerOn, providerCaCert, providerDefinition); + if (PROVIDER_JSON_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { broadcastProgress(progress++); PROVIDER_JSON_DOWNLOADED = true; - current_download = downloadCACert(last_danger_on); - if (CA_CERT_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) { + if (!CA_CERT_DOWNLOADED) + currentDownload = downloadCACert(lastDangerOn); + if (CA_CERT_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { broadcastProgress(progress++); CA_CERT_DOWNLOADED = true; - current_download = getAndSetEipServiceJson(); - if (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY)) { + currentDownload = getAndSetEipServiceJson(); + if (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY)) { broadcastProgress(progress++); EIP_SERVICE_JSON_DOWNLOADED = true; } } } - return current_download; + return currentDownload; } - private Bundle getAndSetProviderJson(String provider_main_url, boolean danger_on, String provider_ca_cert_fingerprint) { + private Bundle getAndSetProviderJson(String providerMainUrl, boolean dangerOn, String caCert, JSONObject providerDefinition) { Bundle result = new Bundle(); if (go_ahead) { - String provider_dot_json_string; - if(provider_ca_cert_fingerprint.isEmpty()) - provider_dot_json_string = downloadWithCommercialCA(provider_main_url + "/provider.json", danger_on); + String providerDotJsonString; + if(providerDefinition.length() == 0 || caCert.isEmpty()) + providerDotJsonString = downloadWithCommercialCA(providerMainUrl + "/provider.json", dangerOn); else - provider_dot_json_string = downloadWithCommercialCA(provider_main_url + "/provider.json", danger_on, provider_ca_cert_fingerprint); + providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition, dangerOn); - if (!isValidJson(provider_dot_json_string)) { + if (!isValidJson(providerDotJsonString)) { result.putString(ERRORS, getString(malformed_url)); result.putBoolean(RESULT_KEY, false); return result; } try { - JSONObject provider_json = new JSONObject(provider_dot_json_string); - provider_api_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION); - String name = provider_json.getString(Provider.NAME); + JSONObject providerJson = new JSONObject(providerDotJsonString); + String providerDomain = providerJson.getString(Provider.DOMAIN); + providerApiUrl = getApiUrlWithVersion(providerJson); + String name = providerJson.getString(Provider.NAME); //TODO setProviderName(name); - preferences.edit().putString(Provider.KEY, provider_json.toString()).commit(); - preferences.edit().putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, provider_json.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)).commit(); - preferences.edit().putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, provider_json.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)).commit(); - + preferences.edit().putString(Provider.KEY, providerJson.toString()). + putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). + putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). + putString(Provider.KEY + "." + providerDomain, providerJson.toString()).commit(); result.putBoolean(RESULT_KEY, true); } catch (JSONException e) { - //TODO Error message should be contained in that provider_dot_json_string - String reason_to_fail = pickErrorMessage(provider_dot_json_string); + String reason_to_fail = pickErrorMessage(providerDotJsonString); result.putString(ERRORS, reason_to_fail); result.putBoolean(RESULT_KEY, false); } @@ -163,7 +188,7 @@ public class ProviderAPI extends ProviderApiBase { try { JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH; - eip_service_json_string = downloadWithProviderCA(eip_service_url, last_danger_on); + eip_service_json_string = downloadWithProviderCA(eip_service_url, lastDangerOn); JSONObject eip_service_json = new JSONObject(eip_service_json_string); eip_service_json.getInt(Provider.API_RETURN_SERIAL); @@ -192,7 +217,7 @@ public class ProviderAPI extends ProviderApiBase { String provider_main_url = provider_json.getString(Provider.API_URL); URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.PROVIDER_VPN_CERTIFICATE); - String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), last_danger_on); + String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), lastDangerOn); if (cert_string == null || cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string)) return false; @@ -210,18 +235,21 @@ public class ProviderAPI extends ProviderApiBase { } - private Bundle downloadCACert(boolean danger_on) { + private Bundle downloadCACert(boolean dangerOn) { Bundle result = new Bundle(); try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - String ca_cert_url = provider_json.getString(Provider.CA_CERT_URI); - String cert_string = downloadWithCommercialCA(ca_cert_url, danger_on); + JSONObject providerJson = new JSONObject(preferences.getString(Provider.KEY, "")); + String caCertUrl = providerJson.getString(Provider.CA_CERT_URI); + String providerDomain = providerJson.getString(Provider.DOMAIN); + + String certString = downloadWithCommercialCA(caCertUrl, dangerOn); - if (validCertificate(cert_string) && go_ahead) { - preferences.edit().putString(Provider.CA_CERT, cert_string).commit(); + if (validCertificate(certString) && go_ahead) { + preferences.edit().putString(Provider.CA_CERT, certString).commit(); + preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, certString).commit(); result.putBoolean(RESULT_KEY, true); } else { - String reason_to_fail = pickErrorMessage(cert_string); + String reason_to_fail = pickErrorMessage(certString); result.putString(ERRORS, reason_to_fail); result.putBoolean(RESULT_KEY, false); } @@ -234,29 +262,6 @@ public class ProviderAPI extends ProviderApiBase { return result; } - //TODO: refactor with ticket #8773 - private String downloadWithCommercialCA(String urlString, boolean dangerOn, String caCertFingerprint) { - String result = ""; - int seconds_of_timeout = 2; - String[] pins = new String[] {caCertFingerprint}; - try { - URL url = new URL(urlString); - HttpsURLConnection connection = PinningHelper.getPinnedHttpsURLConnection(getApplicationContext(), pins, url); - connection.setConnectTimeout(seconds_of_timeout * 1000); - if (!LeapSRPSession.getToken().isEmpty()) - connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); - result = new Scanner(connection.getInputStream()).useDelimiter("\\A").next(); - } catch (IOException e) { - if(e instanceof SSLHandshakeException) { - result = dangerOn ? downloadWithoutCA(urlString) : - formatErrorMessage(R.string.error_security_pinnedcertificate); - } else - result = formatErrorMessage(error_io_exception_user_message); - } - - return result; - } - /** * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider. *

@@ -284,7 +289,35 @@ public class ProviderAPI extends ProviderApiBase { // try to download with provider CA on certificate error JSONObject responseErrorJson = new JSONObject(responseString); if (danger_on && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) { - responseString = downloadWithProviderCA(string_url, danger_on); + responseString = downloadWithoutCA(string_url); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + return responseString; + } + + private String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition, boolean dangerOn) { + String responseString; + JSONObject errorJson = new JSONObject(); + String baseUrl = getApiUrl(providerDefinition); + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert); + if (okHttpClient == null) { + return errorJson.toString(); + } + + String urlString = baseUrl + path; + List> headerArgs = getAuthorizationHeader(); + responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); + + if (responseString != null && responseString.contains(ERRORS)) { + try { + // try to download with provider CA on certificate error + JSONObject responseErrorJson = new JSONObject(responseString); + if (dangerOn && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) { + responseString = downloadWithCommercialCA(urlString, dangerOn); } } catch (JSONException e) { e.printStackTrace(); diff --git a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java index 1d675499..2c169e3d 100644 --- a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java +++ b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java @@ -216,6 +216,8 @@ public abstract class BaseConfigurationWizard extends Activity String provider_json_string = preferences.getString(Provider.KEY, ""); if (!provider_json_string.isEmpty()) selected_provider.define(new JSONObject(provider_json_string)); + String caCert = preferences.getString(Provider.CA_CERT, ""); + selected_provider.setCACert(caCert); } catch (JSONException e) { e.printStackTrace(); } @@ -301,6 +303,20 @@ public abstract class BaseConfigurationWizard extends Activity preferences.edit().remove(Provider.KEY).remove(Constants.PROVIDER_ALLOW_ANONYMOUS).remove(Constants.PROVIDER_KEY).apply(); } + @Override + public void updateProviderDetails() { + mConfigState.setAction(SETTING_UP_PROVIDER); + Intent provider_API_command = new Intent(this, ProviderAPI.class); + + provider_API_command.setAction(ProviderAPI.UPDATE_PROVIDER_DETAILS); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver); + Bundle parameters = new Bundle(); + parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString()); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + + startService(provider_API_command); + } + private void askDashboardToQuitApp() { Intent ask_quit = new Intent(); ask_quit.putExtra(Dashboard.ACTION_QUIT, Dashboard.ACTION_QUIT); diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java index 71a0e149..ae07bc25 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Provider.java +++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java @@ -70,14 +70,18 @@ public final class Provider implements Parcelable { this.mainUrl.setUrl(mainUrl); } - public Provider(URL mainUrl, String caCert, /*String certificatePin,*/ String definition) { + public Provider(URL mainUrl, String caCert, String definition) { this.mainUrl.setUrl(mainUrl); - this.caCert = caCert; - try { - this.definition = new JSONObject(definition); - parseDefinition(this.definition); - } catch (JSONException e) { - e.printStackTrace(); + if (caCert != null) { + this.caCert = caCert; + } + if (definition != null) { + try { + this.definition = new JSONObject(definition); + parseDefinition(this.definition); + } catch (JSONException | NullPointerException e) { + e.printStackTrace(); + } } } diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java index dfc48bee..0013d2c2 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java @@ -665,9 +665,6 @@ public abstract class ProviderApiBase extends IntentService { try { response = okHttpClient.newCall(request).execute(); - if (!response.isSuccessful()){ - return formatErrorMessage(error_json_exception_user_message); - } InputStream inputStream = response.body().byteStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); @@ -760,6 +757,42 @@ public abstract class ProviderApiBase extends IntentService { return result; } + protected void checkPersistedProviderUpdates() { + String providerDomain = getProviderDomain(providerDefinition); + if (hasUpdatedProviderDetails(providerDomain)) { + providerCaCert = getPersistedProviderCA(providerDomain); + providerDefinition = getPersistedProviderDefinition(providerDomain); + providerCaCertFingerprint = getPersistedCaCertFingerprint(providerDomain); + providerApiUrl = getApiUrlWithVersion(providerDefinition); + } + } + + protected Bundle validateProviderDetails() { + Bundle result = validateCertificateForProvider(providerCaCert, providerDefinition, lastProviderMainUrl); + + //invalid certificate or no certificate + if (result.containsKey(ERRORS) || (result.containsKey(RESULT_KEY) && !result.getBoolean(RESULT_KEY)) ) { + return result; + } + + //valid certificate: skip download, save loaded provider CA cert and provider definition directly + try { + preferences.edit().putString(Provider.KEY, providerDefinition.toString()). + putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). + putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). + putString(Provider.CA_CERT, providerCaCert).commit(); + CA_CERT_DOWNLOADED = true; + PROVIDER_JSON_DOWNLOADED = true; + result.putBoolean(RESULT_KEY, true); + } catch (JSONException e) { + e.printStackTrace(); + result.putBoolean(RESULT_KEY, false); + result = setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + } + + return result; + } + protected Bundle validateCertificateForProvider(String cert_string, JSONObject providerDefinition, String mainUrl) { Bundle result = new Bundle(); result.putBoolean(RESULT_KEY, false); diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java index cf703631..92d5da9f 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java @@ -58,19 +58,27 @@ public class ProviderManager implements AdapteeCollection { private Set providersFromAssets(String directory, String[] relative_file_paths) { Set providers = new HashSet(); - try { - for (String file : relative_file_paths) { - String provider = file.substring(0, file.length() - ".url".length()); - InputStream provider_file = assets_manager.open(directory + "/" + file); - String mainUrl = extractMainUrlFromInputStream(provider_file); - String certificate = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".pem")); - String providerDefinition = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".json")); - providers.add(new Provider(new URL(mainUrl), certificate, providerDefinition)); + for (String file : relative_file_paths) { + String mainUrl = null; + String certificate = null; + String providerDefinition = null; + try { + String provider = file.substring(0, file.length() - ".url".length()); + InputStream provider_file = assets_manager.open(directory + "/" + file); + mainUrl = extractMainUrlFromInputStream(provider_file); + certificate = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".pem")); + providerDefinition = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".json")); + } catch (IOException e) { + e.printStackTrace(); + } + try { + providers.add(new Provider(new URL(mainUrl), certificate, providerDefinition)); + } catch (MalformedURLException e) { + e.printStackTrace(); + } } - } catch (IOException e) { - e.printStackTrace(); - } + return providers; } diff --git a/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java b/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java index 363fa66c..3f05b0a2 100644 --- a/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java +++ b/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java @@ -102,18 +102,4 @@ public class ConfigurationWizard extends BaseConfigurationWizard { } } - @Override - public void updateProviderDetails() { - mConfigState.setAction(SETTING_UP_PROVIDER); - Intent provider_API_command = new Intent(this, ProviderAPI.class); - - provider_API_command.setAction(ProviderAPI.UPDATE_PROVIDER_DETAILS); - provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver); - Bundle parameters = new Bundle(); - parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString()); - provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); - - startService(provider_API_command); - } - } diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java index b27c3dca..39651a43 100644 --- a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java @@ -62,6 +62,7 @@ public class ProviderAPI extends ProviderApiBase { lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ? task.getString(Provider.MAIN_URL) : ""; + //TODO: remove that providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ? task.getString(Provider.CA_CERT_FINGERPRINT) : ""; @@ -119,43 +120,6 @@ public class ProviderAPI extends ProviderApiBase { } - private Bundle validateProviderDetails() { - Bundle result = validateCertificateForProvider(providerCaCert, providerDefinition, lastProviderMainUrl); - - //invalid certificate or no certificate - if (result.containsKey(ERRORS) || (result.containsKey(RESULT_KEY) && !result.getBoolean(RESULT_KEY)) ) { - return result; - } - - //valid certificate: skip download, save loaded provider CA cert and provider definition directly - try { - preferences.edit().putString(Provider.KEY, providerDefinition.toString()). - putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). - putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). - putString(Provider.CA_CERT, providerCaCert).commit(); - CA_CERT_DOWNLOADED = true; - PROVIDER_JSON_DOWNLOADED = true; - result.putBoolean(RESULT_KEY, true); - } catch (JSONException e) { - e.printStackTrace(); - result.putBoolean(RESULT_KEY, false); - result = setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); - } - - return result; - } - - private void checkPersistedProviderUpdates() { - String providerDomain = getProviderDomain(providerDefinition); - if (hasUpdatedProviderDetails(providerDomain)) { - providerCaCert = getPersistedProviderCA(providerDomain); - providerDefinition = getPersistedProviderDefinition(providerDomain); - providerCaCertFingerprint = getPersistedCaCertFingerprint(providerDomain); - providerApiUrl = getApiUrlWithVersion(providerDefinition); - } - } - - private Bundle getAndSetProviderJson(String providerMainUrl, String caCert, JSONObject providerDefinition) { Bundle result = new Bundle(); @@ -186,7 +150,6 @@ public class ProviderAPI extends ProviderApiBase { putString(Provider.KEY + "." + providerDomain, providerJson.toString()).commit(); result.putBoolean(RESULT_KEY, true); } catch (JSONException e) { - //TODO Error message should be contained in that provider_dot_json_string String reason_to_fail = pickErrorMessage(providerDotJsonString); result.putString(ERRORS, reason_to_fail); result.putBoolean(RESULT_KEY, false); @@ -261,7 +224,6 @@ public class ProviderAPI extends ProviderApiBase { String providerDomain = providerJson.getString(Provider.DOMAIN); String cert_string = downloadWithCommercialCA(caCertUrl); - result.putBoolean(RESULT_KEY, true); if (validCertificate(cert_string) && go_ahead) { preferences.edit().putString(Provider.CA_CERT, cert_string).commit(); @@ -321,7 +283,7 @@ public class ProviderAPI extends ProviderApiBase { * * @return an empty string if it fails, the response body if not. */ - protected String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition) { + private String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition) { String responseString; JSONObject errorJson = new JSONObject(); String baseUrl = getApiUrl(providerDefinition); -- cgit v1.2.3 From 0b647ea5e7ff67747080b2ffcebc948da0fbecb5 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 9 Jan 2018 20:55:10 +0100 Subject: 8773 refactoring ProviderAPI for testability, setting up basic unit test framework --- app/build.gradle | 14 +- .../java/se/leap/bitmaskclient/ProviderAPI.java | 422 -------- .../se/leap/bitmaskclient/ProviderApiManager.java | 430 ++++++++ app/src/main/AndroidManifest.xml | 2 +- .../bitmaskclient/BaseConfigurationWizard.java | 4 +- .../java/se/leap/bitmaskclient/ConfigHelper.java | 60 +- .../leap/bitmaskclient/DownloadFailedDialog.java | 16 +- .../leap/bitmaskclient/OkHttpClientGenerator.java | 166 +++ .../java/se/leap/bitmaskclient/ProviderAPI.java | 124 +++ .../bitmaskclient/ProviderAPIResultReceiver.java | 4 +- .../se/leap/bitmaskclient/ProviderApiBase.java | 1061 -------------------- .../leap/bitmaskclient/ProviderApiConnector.java | 102 ++ .../leap/bitmaskclient/ProviderApiManagerBase.java | 984 ++++++++++++++++++ .../bitmaskclient/userstatus/SessionDialog.java | 2 - .../java/se/leap/bitmaskclient/ProviderAPI.java | 327 ------ .../se/leap/bitmaskclient/ProviderApiManager.java | 340 +++++++ .../test/java/se/leap/bitmaskclient/TestUtils.java | 26 - .../bitmaskclient/eip/GatewaysManagerTest.java | 10 +- .../bitmaskclient/eip/ProviderApiManagerTest.java | 249 +++++ .../bitmaskclient/eip/VpnConfigGeneratorTest.java | 10 +- .../testutils/MockSharedPreferences.java | 165 +++ .../bitmaskclient/testutils/TestSetupHelper.java | 443 ++++++++ .../testutils/answers/BackendAnswerFabric.java | 82 ++ .../testutils/answers/NoErrorAnswer.java | 58 ++ .../testutils/matchers/BundleMatcher.java | 192 ++++ app/src/test/resources/error_messages.json | 16 + app/src/test/resources/riseup.net.json | 37 + app/src/test/resources/riseup.net.pem | 32 + app/src/test/resources/riseup.service.json | 93 ++ app/src/test/resources/secrets.json | 2 +- 30 files changed, 3589 insertions(+), 1884 deletions(-) delete mode 100644 app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java create mode 100644 app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java delete mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java delete mode 100644 app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java create mode 100644 app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java delete mode 100644 app/src/test/java/se/leap/bitmaskclient/TestUtils.java create mode 100644 app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java create mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java create mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java create mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java create mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java create mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java create mode 100644 app/src/test/resources/error_messages.json create mode 100644 app/src/test/resources/riseup.net.json create mode 100644 app/src/test/resources/riseup.net.pem create mode 100644 app/src/test/resources/riseup.service.json diff --git a/app/build.gradle b/app/build.gradle index 048eb597..8b956944 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,7 +64,16 @@ android { } dependencies { - testCompile 'org.mockito:mockito-core:2.6.3' + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:2.8.0' + testCompile 'org.powermock:powermock-api-mockito2:1.7.3' + testCompile 'org.powermock:powermock-module-junit4:1.7.3' + testCompile 'org.powermock:powermock-core:1.7.3' + testCompile 'org.powermock:powermock-module-junit4-rule:1.7.3' + testCompile 'com.madgag.spongycastle:bctls-jdk15on:1.58.0.0' + testCompile 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' + testCompile 'com.madgag.spongycastle:bcpg-jdk15on:1.58.0.0' + androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.6.3' testCompile 'junit:junit:4.12' testCompile 'org.json:json:20170516' @@ -72,8 +81,7 @@ dependencies { provided 'com.squareup.dagger:dagger-compiler:1.2.2' compile 'com.github.pedrovgs:renderers:1.5' compile 'com.intellij:annotations:12.0' - compile 'com.google.code.gson:gson:2.4' - compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0' + compile 'com.google.code.gson:gson:2.7' compile 'com.squareup.okhttp3:okhttp:3.9.0' compile 'mbanje.kurt:fabbutton:1.1.4' compile "com.android.support:support-core-utils:26.1.0" diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java deleted file mode 100644 index 5cb06115..00000000 --- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java +++ /dev/null @@ -1,422 +0,0 @@ -/** - * Copyright (c) 2013 LEAP Encryption Access Project and contributers - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 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 . - */ -package se.leap.bitmaskclient; - -import android.os.Bundle; -import android.util.Pair; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.URL; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Scanner; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManager; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import okhttp3.OkHttpClient; -import se.leap.bitmaskclient.ProviderListContent.ProviderItem; -import se.leap.bitmaskclient.eip.EIP; - -import static se.leap.bitmaskclient.R.string.certificate_error; -import static se.leap.bitmaskclient.R.string.malformed_url; - -/** - * Implements HTTP api methods used to manage communications with the provider server. - * It extends the abstract ProviderApiBase and implements the diverging method calls between the different flavors - * of ProviderAPI. - *

- * It extends an IntentService because it downloads data from the Internet, so it operates in the background. - * - * @author parmegv - * @author MeanderingCode - * @author cyberta - */ -public class ProviderAPI extends ProviderApiBase { - - private static boolean lastDangerOn = true; - - public static boolean lastDangerOn() { - return lastDangerOn; - } - - /** - * Downloads a provider.json from a given URL, adding a new provider using the given name. - * - * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. - * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. - */ - @Override - protected Bundle setUpProvider(Bundle task) { - int progress = 0; - Bundle currentDownload = new Bundle(); - - if (task != null) { - lastDangerOn = task.containsKey(ProviderItem.DANGER_ON) && task.getBoolean(ProviderItem.DANGER_ON); - lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ? - task.getString(Provider.MAIN_URL) : - ""; - providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ? - task.getString(Provider.CA_CERT_FINGERPRINT) : - ""; - providerCaCert = task.containsKey(Provider.CA_CERT) ? - task.getString(Provider.CA_CERT) : - ""; - - try { - providerDefinition = task.containsKey(Provider.KEY) ? - new JSONObject(task.getString(Provider.KEY)) : - new JSONObject(); - } catch (JSONException e) { - e.printStackTrace(); - providerDefinition = new JSONObject(); - } - providerApiUrl = getApiUrlWithVersion(providerDefinition); - - checkPersistedProviderUpdates(); - currentDownload = validateProviderDetails(); - - //provider details invalid - if (currentDownload.containsKey(ERRORS)) { - return currentDownload; - } - - //no provider certificate available - if (currentDownload.containsKey(RESULT_KEY) && !currentDownload.getBoolean(RESULT_KEY)) { - resetProviderDetails(); - } - - EIP_SERVICE_JSON_DOWNLOADED = false; - go_ahead = true; - } - - if (!PROVIDER_JSON_DOWNLOADED) - currentDownload = getAndSetProviderJson(lastProviderMainUrl, lastDangerOn, providerCaCert, providerDefinition); - if (PROVIDER_JSON_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { - broadcastProgress(progress++); - PROVIDER_JSON_DOWNLOADED = true; - - if (!CA_CERT_DOWNLOADED) - currentDownload = downloadCACert(lastDangerOn); - if (CA_CERT_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { - broadcastProgress(progress++); - CA_CERT_DOWNLOADED = true; - currentDownload = getAndSetEipServiceJson(); - if (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY)) { - broadcastProgress(progress++); - EIP_SERVICE_JSON_DOWNLOADED = true; - } - } - } - - return currentDownload; - } - - private Bundle getAndSetProviderJson(String providerMainUrl, boolean dangerOn, String caCert, JSONObject providerDefinition) { - Bundle result = new Bundle(); - - if (go_ahead) { - String providerDotJsonString; - if(providerDefinition.length() == 0 || caCert.isEmpty()) - providerDotJsonString = downloadWithCommercialCA(providerMainUrl + "/provider.json", dangerOn); - else - providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition, dangerOn); - - if (!isValidJson(providerDotJsonString)) { - result.putString(ERRORS, getString(malformed_url)); - result.putBoolean(RESULT_KEY, false); - return result; - } - - try { - JSONObject providerJson = new JSONObject(providerDotJsonString); - String providerDomain = providerJson.getString(Provider.DOMAIN); - providerApiUrl = getApiUrlWithVersion(providerJson); - String name = providerJson.getString(Provider.NAME); - //TODO setProviderName(name); - - preferences.edit().putString(Provider.KEY, providerJson.toString()). - putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). - putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). - putString(Provider.KEY + "." + providerDomain, providerJson.toString()).commit(); - result.putBoolean(RESULT_KEY, true); - } catch (JSONException e) { - String reason_to_fail = pickErrorMessage(providerDotJsonString); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - } - return result; - } - - /** - * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways - * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the download was successful. - */ - @Override - protected Bundle getAndSetEipServiceJson() { - Bundle result = new Bundle(); - String eip_service_json_string = ""; - if (go_ahead) { - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH; - eip_service_json_string = downloadWithProviderCA(eip_service_url, lastDangerOn); - JSONObject eip_service_json = new JSONObject(eip_service_json_string); - eip_service_json.getInt(Provider.API_RETURN_SERIAL); - - preferences.edit().putString(Constants.PROVIDER_KEY, eip_service_json.toString()).commit(); - - result.putBoolean(RESULT_KEY, true); - } catch (NullPointerException | JSONException e) { - String reason_to_fail = pickErrorMessage(eip_service_json_string); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - } - return result; - } - - /** - * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. - * - * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. - */ - @Override - protected boolean updateVpnCertificate() { - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - - String provider_main_url = provider_json.getString(Provider.API_URL); - URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.PROVIDER_VPN_CERTIFICATE); - - String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), lastDangerOn); - - if (cert_string == null || cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string)) - return false; - else - return loadCertificate(cert_string); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } - } - - - private Bundle downloadCACert(boolean dangerOn) { - Bundle result = new Bundle(); - try { - JSONObject providerJson = new JSONObject(preferences.getString(Provider.KEY, "")); - String caCertUrl = providerJson.getString(Provider.CA_CERT_URI); - String providerDomain = providerJson.getString(Provider.DOMAIN); - - String certString = downloadWithCommercialCA(caCertUrl, dangerOn); - - if (validCertificate(certString) && go_ahead) { - preferences.edit().putString(Provider.CA_CERT, certString).commit(); - preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, certString).commit(); - result.putBoolean(RESULT_KEY, true); - } else { - String reason_to_fail = pickErrorMessage(certString); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - } catch (JSONException e) { - String reason_to_fail = formatErrorMessage(malformed_url); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - - return result; - } - - /** - * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider. - *

- * If danger_on flag is true, SSL exceptions will be managed by futher methods that will try to use some bypass methods. - * - * @param string_url - * @param danger_on if the user completely trusts this provider - * @return - */ - private String downloadWithCommercialCA(String string_url, boolean danger_on) { - String responseString; - JSONObject errorJson = new JSONObject(); - - OkHttpClient okHttpClient = initCommercialCAHttpClient(errorJson); - if (okHttpClient == null) { - return errorJson.toString(); - } - - List> headerArgs = getAuthorizationHeader(); - - responseString = sendGetStringToServer(string_url, headerArgs, okHttpClient); - - if (responseString != null && responseString.contains(ERRORS)) { - try { - // try to download with provider CA on certificate error - JSONObject responseErrorJson = new JSONObject(responseString); - if (danger_on && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) { - responseString = downloadWithoutCA(string_url); - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - - return responseString; - } - - private String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition, boolean dangerOn) { - String responseString; - JSONObject errorJson = new JSONObject(); - String baseUrl = getApiUrl(providerDefinition); - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert); - if (okHttpClient == null) { - return errorJson.toString(); - } - - String urlString = baseUrl + path; - List> headerArgs = getAuthorizationHeader(); - responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); - - if (responseString != null && responseString.contains(ERRORS)) { - try { - // try to download with provider CA on certificate error - JSONObject responseErrorJson = new JSONObject(responseString); - if (dangerOn && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) { - responseString = downloadWithCommercialCA(urlString, dangerOn); - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - - return responseString; - } - - /** - * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. - * - * @param urlString as a string - * @param dangerOn true to download CA certificate in case it has not been downloaded. - * @return an empty string if it fails, the url content if not. - */ - private String downloadWithProviderCA(String urlString, boolean dangerOn) { - JSONObject initError = new JSONObject(); - String responseString; - - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(initError); - if (okHttpClient == null) { - return initError.toString(); - } - - List> headerArgs = getAuthorizationHeader(); - - responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); - - if (responseString.contains(ERRORS)) { - try { - // danger danger: try to download without CA on certificate error - JSONObject responseErrorJson = new JSONObject(responseString); - if (dangerOn && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) { - responseString = downloadWithoutCA(urlString); - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - - return responseString; - } - - /** - * Downloads the string that's in the url with any certificate. - */ - // This method is totally insecure anyways. So no need to refactor that in order to use okHttpClient, force modern TLS etc.. DO NOT USE IN PRODUCTION! - private String downloadWithoutCA(String url_string) { - String string = ""; - try { - - HostnameVerifier hostnameVerifier = new HostnameVerifier() { - @Override - public boolean verify(String hostname, SSLSession session) { - return true; - } - }; - - class DefaultTrustManager implements X509TrustManager { - - @Override - public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { - } - - @Override - public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return null; - } - } - - SSLContext context = SSLContext.getInstance("TLS"); - context.init(new KeyManager[0], new TrustManager[]{new DefaultTrustManager()}, new SecureRandom()); - - URL url = new URL(url_string); - HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); - urlConnection.setSSLSocketFactory(context.getSocketFactory()); - urlConnection.setHostnameVerifier(hostnameVerifier); - string = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next(); - System.out.println("String ignoring certificate = " + string); - } catch (FileNotFoundException e) { - e.printStackTrace(); - string = formatErrorMessage(malformed_url); - } catch (IOException e) { - // The downloaded certificate doesn't validate our https connection. - e.printStackTrace(); - string = formatErrorMessage(certificate_error); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (KeyManagementException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return string; - } - -} diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java new file mode 100644 index 00000000..9c27fd1f --- /dev/null +++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java @@ -0,0 +1,430 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ + +package se.leap.bitmaskclient; + +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Bundle; +import android.util.Pair; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Scanner; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import okhttp3.OkHttpClient; +import se.leap.bitmaskclient.eip.EIP; + +import static android.text.TextUtils.isEmpty; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON; +import static se.leap.bitmaskclient.ProviderAPI.ERRORS; +import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY; +import static se.leap.bitmaskclient.R.string.certificate_error; +import static se.leap.bitmaskclient.R.string.malformed_url; + +/** + * Created by cyberta on 04.01.18. + */ + +public class ProviderApiManager extends ProviderApiManagerBase { + protected static boolean lastDangerOn = true; + + + public ProviderApiManager(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) { + super(preferences, resources, clientGenerator, callback); + } + + public static boolean lastDangerOn() { + return lastDangerOn; + } + + /** + * Downloads a provider.json from a given URL, adding a new provider using the given name. + * + * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. + */ + @Override + protected Bundle setUpProvider(Bundle task) { + int progress = 0; + Bundle currentDownload = new Bundle(); + + if (task != null) { + lastDangerOn = task.containsKey(ProviderListContent.ProviderItem.DANGER_ON) && task.getBoolean(ProviderListContent.ProviderItem.DANGER_ON); + lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ? + task.getString(Provider.MAIN_URL) : + ""; + + if (isEmpty(lastProviderMainUrl)) { + currentDownload.putBoolean(RESULT_KEY, false); + setErrorResult(currentDownload, resources.getString(R.string.malformed_url), null); + return currentDownload; + } + + providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ? + task.getString(Provider.CA_CERT_FINGERPRINT) : + ""; + providerCaCert = task.containsKey(Provider.CA_CERT) ? + task.getString(Provider.CA_CERT) : + ""; + + try { + providerDefinition = task.containsKey(Provider.KEY) ? + new JSONObject(task.getString(Provider.KEY)) : + new JSONObject(); + } catch (JSONException e) { + e.printStackTrace(); + providerDefinition = new JSONObject(); + } + providerApiUrl = getApiUrlWithVersion(providerDefinition); + + checkPersistedProviderUpdates(); + currentDownload = validateProviderDetails(); + + //provider details invalid + if (currentDownload.containsKey(ERRORS)) { + return currentDownload; + } + + //no provider certificate available + if (currentDownload.containsKey(RESULT_KEY) && !currentDownload.getBoolean(RESULT_KEY)) { + resetProviderDetails(); + } + + EIP_SERVICE_JSON_DOWNLOADED = false; + go_ahead = true; + } + + if (!PROVIDER_JSON_DOWNLOADED) + currentDownload = getAndSetProviderJson(lastProviderMainUrl, lastDangerOn, providerCaCert, providerDefinition); + if (PROVIDER_JSON_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { + broadcastProgress(progress++); + PROVIDER_JSON_DOWNLOADED = true; + + if (!CA_CERT_DOWNLOADED) + currentDownload = downloadCACert(lastDangerOn); + if (CA_CERT_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { + broadcastProgress(progress++); + CA_CERT_DOWNLOADED = true; + currentDownload = getAndSetEipServiceJson(); + if (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY)) { + broadcastProgress(progress++); + EIP_SERVICE_JSON_DOWNLOADED = true; + } + } + } + + return currentDownload; + } + + private Bundle getAndSetProviderJson(String providerMainUrl, boolean dangerOn, String caCert, JSONObject providerDefinition) { + Bundle result = new Bundle(); + + if (go_ahead) { + String providerDotJsonString; + if(providerDefinition.length() == 0 || caCert.isEmpty()) + providerDotJsonString = downloadWithCommercialCA(providerMainUrl + "/provider.json", dangerOn); + else + providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition, dangerOn); + + if (!isValidJson(providerDotJsonString)) { + result.putString(ERRORS, resources.getString(malformed_url)); + result.putBoolean(RESULT_KEY, false); + return result; + } + + try { + JSONObject providerJson = new JSONObject(providerDotJsonString); + String providerDomain = getDomainFromMainURL(lastProviderMainUrl); + providerApiUrl = getApiUrlWithVersion(providerJson); + //String name = providerJson.getString(Provider.NAME); + //TODO setProviderName(name); + + preferences.edit().putString(Provider.KEY, providerJson.toString()). + putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). + putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). + putString(Provider.KEY + "." + providerDomain, providerJson.toString()).commit(); + result.putBoolean(RESULT_KEY, true); + } catch (JSONException e) { + String reason_to_fail = pickErrorMessage(providerDotJsonString); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + } + return result; + } + + /** + * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the download was successful. + */ + @Override + protected Bundle getAndSetEipServiceJson() { + Bundle result = new Bundle(); + String eip_service_json_string = ""; + if (go_ahead) { + try { + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH; + eip_service_json_string = downloadWithProviderCA(eip_service_url, lastDangerOn); + JSONObject eip_service_json = new JSONObject(eip_service_json_string); + eip_service_json.getInt(Provider.API_RETURN_SERIAL); + + preferences.edit().putString(Constants.PROVIDER_KEY, eip_service_json.toString()).commit(); + + result.putBoolean(RESULT_KEY, true); + } catch (NullPointerException | JSONException e) { + String reason_to_fail = pickErrorMessage(eip_service_json_string); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + } + return result; + } + + /** + * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. + * + * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. + */ + @Override + protected boolean updateVpnCertificate() { + try { + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + + String provider_main_url = provider_json.getString(Provider.API_URL); + URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.PROVIDER_VPN_CERTIFICATE); + + String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), lastDangerOn); + + if (cert_string == null || cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string)) + return false; + else + return loadCertificate(cert_string); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } + } + + + private Bundle downloadCACert(boolean dangerOn) { + Bundle result = new Bundle(); + try { + JSONObject providerJson = new JSONObject(preferences.getString(Provider.KEY, "")); + String caCertUrl = providerJson.getString(Provider.CA_CERT_URI); + String providerDomain = providerJson.getString(Provider.DOMAIN); + + String certString = downloadWithCommercialCA(caCertUrl, dangerOn); + + if (validCertificate(certString) && go_ahead) { + preferences.edit().putString(Provider.CA_CERT, certString).commit(); + preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, certString).commit(); + result.putBoolean(RESULT_KEY, true); + } else { + setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); + result.putBoolean(RESULT_KEY, false); + } + } catch (JSONException e) { + setErrorResult(result, resources.getString(malformed_url), null); + result.putBoolean(RESULT_KEY, false); + } + + return result; + } + + /** + * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider. + *

+ * If danger_on flag is true, SSL exceptions will be managed by futher methods that will try to use some bypass methods. + * + * @param string_url + * @param danger_on if the user completely trusts this provider + * @return + */ + private String downloadWithCommercialCA(String string_url, boolean danger_on) { + String responseString; + JSONObject errorJson = new JSONObject(); + + OkHttpClient okHttpClient = clientGenerator.initCommercialCAHttpClient(errorJson); + if (okHttpClient == null) { + return errorJson.toString(); + } + + List> headerArgs = getAuthorizationHeader(); + + responseString = sendGetStringToServer(string_url, headerArgs, okHttpClient); + + if (responseString != null && responseString.contains(ERRORS)) { + try { + // try to download with provider CA on certificate error + JSONObject responseErrorJson = new JSONObject(responseString); + if (danger_on && responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) { + responseString = downloadWithoutCA(string_url); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + return responseString; + } + + private String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition, boolean dangerOn) { + String responseString; + JSONObject errorJson = new JSONObject(); + String baseUrl = getApiUrl(providerDefinition); + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert); + if (okHttpClient == null) { + return errorJson.toString(); + } + + String urlString = baseUrl + path; + List> headerArgs = getAuthorizationHeader(); + responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); + + if (responseString != null && responseString.contains(ERRORS)) { + try { + // try to download with provider CA on certificate error + JSONObject responseErrorJson = new JSONObject(responseString); + if (dangerOn && responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) { + responseString = downloadWithCommercialCA(urlString, dangerOn); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + return responseString; + } + + /** + * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. + * + * @param urlString as a string + * @param dangerOn true to download CA certificate in case it has not been downloaded. + * @return an empty string if it fails, the url content if not. + */ + private String downloadWithProviderCA(String urlString, boolean dangerOn) { + JSONObject initError = new JSONObject(); + String responseString; + + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(initError); + if (okHttpClient == null) { + return initError.toString(); + } + + List> headerArgs = getAuthorizationHeader(); + + responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); + + if (responseString.contains(ERRORS)) { + try { + // danger danger: try to download without CA on certificate error + JSONObject responseErrorJson = new JSONObject(responseString); + if (dangerOn && responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) { + responseString = downloadWithoutCA(urlString); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + return responseString; + } + + /** + * Downloads the string that's in the url with any certificate. + */ + // This method is totally insecure anyways. So no need to refactor that in order to use okHttpClient, force modern TLS etc.. DO NOT USE IN PRODUCTION! + private String downloadWithoutCA(String url_string) { + String string = ""; + try { + + HostnameVerifier hostnameVerifier = new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }; + + class DefaultTrustManager implements X509TrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + + SSLContext context = SSLContext.getInstance("TLS"); + context.init(new KeyManager[0], new TrustManager[]{new DefaultTrustManager()}, new SecureRandom()); + + URL url = new URL(url_string); + HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); + urlConnection.setSSLSocketFactory(context.getSocketFactory()); + urlConnection.setHostnameVerifier(hostnameVerifier); + string = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next(); + System.out.println("String ignoring certificate = " + string); + } catch (FileNotFoundException e) { + e.printStackTrace(); + string = formatErrorMessage(malformed_url); + } catch (IOException e) { + // The downloaded certificate doesn't validate our https connection. + e.printStackTrace(); + string = formatErrorMessage(certificate_error); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (KeyManagementException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return string; + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4dd11143..c0e2d90c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,7 +54,7 @@ - + . + */ + +package se.leap.bitmaskclient; + +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Build; +import android.support.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import okhttp3.CipherSuite; +import okhttp3.ConnectionSpec; +import okhttp3.Cookie; +import okhttp3.CookieJar; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.TlsVersion; + +import static android.text.TextUtils.isEmpty; +import static se.leap.bitmaskclient.ProviderAPI.ERRORS; +import static se.leap.bitmaskclient.R.string.certificate_error; +import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; +import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message; +import static se.leap.bitmaskclient.R.string.keyChainAccessError; +import static se.leap.bitmaskclient.R.string.server_unreachable_message; + +/** + * Created by cyberta on 08.01.18. + */ + +public class OkHttpClientGenerator { + + SharedPreferences preferences; + Resources resources; + + public OkHttpClientGenerator(SharedPreferences preferences, Resources resources) { + this.preferences = preferences; + this.resources = resources; + } + + public OkHttpClient initCommercialCAHttpClient(JSONObject initError) { + return initHttpClient(initError, null); + } + + public OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) { + String certificate = preferences.getString(Provider.CA_CERT, ""); + return initHttpClient(initError, certificate); + } + + public OkHttpClient initSelfSignedCAHttpClient(JSONObject initError, String certificate) { + return initHttpClient(initError, certificate); + } + + + private OkHttpClient initHttpClient(JSONObject initError, String certificate) { + try { + TLSCompatSocketFactory sslCompatFactory; + ConnectionSpec spec = getConnectionSpec(); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + + if (!isEmpty(certificate)) { + sslCompatFactory = new TLSCompatSocketFactory(certificate); + } else { + sslCompatFactory = new TLSCompatSocketFactory(); + } + sslCompatFactory.initSSLSocketFactory(clientBuilder); + clientBuilder.cookieJar(getCookieJar()) + .connectionSpecs(Collections.singletonList(spec)); + return clientBuilder.build(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, resources.getString(R.string.certificate_error)); + } catch (IllegalStateException | KeyManagementException | KeyStoreException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage())); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, resources.getString(error_no_such_algorithm_exception_user_message)); + } catch (CertificateException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, resources.getString(certificate_error)); + } catch (UnknownHostException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, resources.getString(server_unreachable_message)); + } catch (IOException e) { + e.printStackTrace(); + addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message)); + } + return null; + } + + + + @NonNull + private ConnectionSpec getConnectionSpec() { + ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3); + //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels + //figure out how to use bcjsse for that purpose + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) + connectionSpecbuilder.cipherSuites( + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + ); + return connectionSpecbuilder.build(); + } + + @NonNull + private CookieJar getCookieJar() { + return new CookieJar() { + private final HashMap> cookieStore = new HashMap<>(); + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + cookieStore.put(url.host(), cookies); + } + + @Override + public List loadForRequest(HttpUrl url) { + List cookies = cookieStore.get(url.host()); + return cookies != null ? cookies : new ArrayList(); + } + }; + } + + private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) { + try { + jsonObject.put(ERRORS, errorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java new file mode 100644 index 00000000..5a6aabc0 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2017 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ +package se.leap.bitmaskclient; + +import android.annotation.SuppressLint; +import android.app.IntentService; +import android.content.Intent; +import android.content.SharedPreferences; + +import de.blinkt.openvpn.core.Preferences; + +/** + * Implements HTTP api methods (encapsulated in {{@link ProviderApiManager}}) + * used to manage communications with the provider server. + *

+ * It's an IntentService because it downloads data from the Internet, so it operates in the background. + * + * @author parmegv + * @author MeanderingCode + * @author cyberta + */ + +public class ProviderAPI extends IntentService implements ProviderApiManagerBase.ProviderApiServiceCallback { + + final public static String + TAG = ProviderAPI.class.getSimpleName(), + SET_UP_PROVIDER = "setUpProvider", + UPDATE_PROVIDER_DETAILS = "updateProviderDetails", + DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON", + SIGN_UP = "srpRegister", + LOG_IN = "srpAuth", + LOG_OUT = "logOut", + DOWNLOAD_CERTIFICATE = "downloadUserAuthedCertificate", + PARAMETERS = "parameters", + RESULT_KEY = "result", + RECEIVER_KEY = "receiver", + ERRORS = "errors", + ERRORID = "errorId", + UPDATE_PROGRESSBAR = "update_progressbar", + CURRENT_PROGRESS = "current_progress", + DOWNLOAD_EIP_SERVICE = TAG + ".DOWNLOAD_EIP_SERVICE"; + + final public static int + SUCCESSFUL_LOGIN = 3, + FAILED_LOGIN = 4, + SUCCESSFUL_SIGNUP = 5, + FAILED_SIGNUP = 6, + SUCCESSFUL_LOGOUT = 7, + LOGOUT_FAILED = 8, + CORRECTLY_DOWNLOADED_CERTIFICATE = 9, + INCORRECTLY_DOWNLOADED_CERTIFICATE = 10, + PROVIDER_OK = 11, + PROVIDER_NOK = 12, + CORRECTLY_DOWNLOADED_EIP_SERVICE = 13, + INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14; + + ProviderApiManager providerApiManager; + + + + public ProviderAPI() { + super(TAG); + } + + //TODO: refactor me, please! + public static void stop() { + ProviderApiManager.stop(); + } + + //TODO: refactor me, please! + public static boolean caCertDownloaded() { + return ProviderApiManager.caCertDownloaded(); + } + + //TODO: refactor me, please! + public static String lastProviderMainUrl() { + return ProviderApiManager.lastProviderMainUrl(); + } + + //TODO: refactor me, please! + //used in insecure flavor only + @SuppressLint("unused") + public static boolean lastDangerOn() { + return ProviderApiManager.lastDangerOn(); + } + + + @Override + public void onCreate() { + super.onCreate(); + providerApiManager = initApiManager(); + } + + @Override + public void broadcastProgress(Intent intent) { + sendBroadcast(intent); + } + + @Override + protected void onHandleIntent(Intent command) { + providerApiManager.handleIntent(command); + } + + + private ProviderApiManager initApiManager() { + SharedPreferences preferences = getSharedPreferences(Constants.SHARED_PREFERENCES, MODE_PRIVATE); + OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(preferences, getResources()); + return new ProviderApiManager(preferences, getResources(), clientGenerator, this); + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java index 9b880f89..9b777e5a 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java @@ -16,7 +16,9 @@ */ package se.leap.bitmaskclient; -import android.os.*; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; /** * Implements the ResultReceiver needed by Activities using ProviderAPI to receive the results of its operations. diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java deleted file mode 100644 index 0013d2c2..00000000 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java +++ /dev/null @@ -1,1061 +0,0 @@ -/** - * Copyright (c) 2017 LEAP Encryption Access Project and contributers - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 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 . - */ -package se.leap.bitmaskclient; - -import android.app.IntentService; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.os.Build; -import android.os.Bundle; -import android.os.ResultReceiver; -import android.support.annotation.NonNull; -import android.util.Base64; -import android.util.Pair; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.io.InputStream; -import java.math.BigInteger; -import java.net.ConnectException; -import java.net.MalformedURLException; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; -import java.net.UnknownServiceException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.X509Certificate; -import java.security.interfaces.RSAPrivateKey; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Scanner; - -import javax.net.ssl.SSLHandshakeException; - -import okhttp3.CipherSuite; -import okhttp3.ConnectionSpec; -import okhttp3.Cookie; -import okhttp3.CookieJar; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.TlsVersion; -import se.leap.bitmaskclient.userstatus.SessionDialog; -import se.leap.bitmaskclient.userstatus.User; -import se.leap.bitmaskclient.userstatus.UserStatus; - -import static android.text.TextUtils.isEmpty; -import static se.leap.bitmaskclient.ConfigHelper.base64toHex; -import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING; -import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON; -import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE; -import static se.leap.bitmaskclient.Provider.MAIN_URL; -import static se.leap.bitmaskclient.R.string.certificate_error; -import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; -import static se.leap.bitmaskclient.R.string.error_json_exception_user_message; -import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message; -import static se.leap.bitmaskclient.R.string.keyChainAccessError; -import static se.leap.bitmaskclient.R.string.malformed_url; -import static se.leap.bitmaskclient.R.string.server_unreachable_message; -import static se.leap.bitmaskclient.R.string.service_is_down_error; - -/** - * Implements HTTP api methods used to manage communications with the provider server. - * The implemented methods are commonly used by insecure's and production's flavor of ProviderAPI. - *

- * It's an IntentService because it downloads data from the Internet, so it operates in the background. - * - * @author parmegv - * @author MeanderingCode - * @author cyberta - */ - -public abstract class ProviderApiBase extends IntentService { - - final public static String - TAG = ProviderAPI.class.getSimpleName(), - SET_UP_PROVIDER = "setUpProvider", - UPDATE_PROVIDER_DETAILS = "updateProviderDetails", - DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON", - SIGN_UP = "srpRegister", - LOG_IN = "srpAuth", - LOG_OUT = "logOut", - DOWNLOAD_CERTIFICATE = "downloadUserAuthedCertificate", - PARAMETERS = "parameters", - RESULT_KEY = "result", - RECEIVER_KEY = "receiver", - ERRORS = "errors", - ERRORID = "errorId", - UPDATE_PROGRESSBAR = "update_progressbar", - CURRENT_PROGRESS = "current_progress", - DOWNLOAD_EIP_SERVICE = TAG + ".DOWNLOAD_EIP_SERVICE"; - - final public static int - SUCCESSFUL_LOGIN = 3, - FAILED_LOGIN = 4, - SUCCESSFUL_SIGNUP = 5, - FAILED_SIGNUP = 6, - SUCCESSFUL_LOGOUT = 7, - LOGOUT_FAILED = 8, - CORRECTLY_DOWNLOADED_CERTIFICATE = 9, - INCORRECTLY_DOWNLOADED_CERTIFICATE = 10, - PROVIDER_OK = 11, - PROVIDER_NOK = 12, - CORRECTLY_DOWNLOADED_EIP_SERVICE = 13, - INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14; - - protected static boolean - CA_CERT_DOWNLOADED = false, - PROVIDER_JSON_DOWNLOADED = false, - EIP_SERVICE_JSON_DOWNLOADED = false; - - protected static String lastProviderMainUrl; - protected static boolean go_ahead = true; - protected static SharedPreferences preferences; - protected static String providerApiUrl; - protected static String providerCaCertFingerprint; - protected static String providerCaCert; - protected static JSONObject providerDefinition; - protected Resources resources; - - public static void stop() { - go_ahead = false; - } - - private final MediaType JSON - = MediaType.parse("application/json; charset=utf-8"); - - public ProviderApiBase() { - super(TAG); - } - - @Override - public void onCreate() { - super.onCreate(); - - preferences = getSharedPreferences(Constants.SHARED_PREFERENCES, MODE_PRIVATE); - resources = getResources(); - } - - public static String lastProviderMainUrl() { - return lastProviderMainUrl; - } - - @Override - protected void onHandleIntent(Intent command) { - final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY); - String action = command.getAction(); - Bundle parameters = command.getBundleExtra(PARAMETERS); - - if (providerApiUrl == null && preferences.contains(Provider.KEY)) { - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - providerApiUrl = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION); - go_ahead = true; - } catch (JSONException e) { - go_ahead = false; - } - } - if (action.equals(UPDATE_PROVIDER_DETAILS)) { - resetProviderDetails(); - Bundle task = new Bundle(); - task.putString(MAIN_URL, lastProviderMainUrl); - Bundle result = setUpProvider(task); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(PROVIDER_OK, result); - } else { - receiver.send(PROVIDER_NOK, result); - } - } else if (action.equalsIgnoreCase(SET_UP_PROVIDER)) { - Bundle result = setUpProvider(parameters); - if (go_ahead) { - if (result.getBoolean(RESULT_KEY)) { - receiver.send(PROVIDER_OK, result); - } else { - receiver.send(PROVIDER_NOK, result); - } - } - } else if (action.equalsIgnoreCase(SIGN_UP)) { - UserStatus.updateStatus(UserStatus.SessionStatus.SIGNING_UP, resources); - Bundle result = tryToRegister(parameters); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(SUCCESSFUL_SIGNUP, result); - } else { - receiver.send(FAILED_SIGNUP, result); - } - } else if (action.equalsIgnoreCase(LOG_IN)) { - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_IN, resources); - Bundle result = tryToAuthenticate(parameters); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(SUCCESSFUL_LOGIN, result); - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_IN, resources); - } else { - receiver.send(FAILED_LOGIN, result); - UserStatus.updateStatus(UserStatus.SessionStatus.NOT_LOGGED_IN, resources); - } - } else if (action.equalsIgnoreCase(LOG_OUT)) { - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_OUT, resources); - if (logOut()) { - receiver.send(SUCCESSFUL_LOGOUT, Bundle.EMPTY); - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_OUT, resources); - } else { - receiver.send(LOGOUT_FAILED, Bundle.EMPTY); - UserStatus.updateStatus(UserStatus.SessionStatus.DIDNT_LOG_OUT, resources); - } - } else if (action.equalsIgnoreCase(DOWNLOAD_CERTIFICATE)) { - if (updateVpnCertificate()) { - receiver.send(CORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); - } else { - receiver.send(INCORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); - } - } else if (action.equalsIgnoreCase(DOWNLOAD_EIP_SERVICE)) { - Bundle result = getAndSetEipServiceJson(); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(CORRECTLY_DOWNLOADED_EIP_SERVICE, result); - } else { - receiver.send(INCORRECTLY_DOWNLOADED_EIP_SERVICE, result); - } - } - } - - protected void resetProviderDetails() { - CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = false; - deleteProviderDetailsFromPreferences(providerDefinition); - providerCaCert = ""; - providerDefinition = new JSONObject(); - } - - protected String formatErrorMessage(final int toastStringId) { - return formatErrorMessage(getResources().getString(toastStringId)); - } - - private String formatErrorMessage(String errorMessage) { - return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }"; - } - - private JSONObject getErrorMessageAsJson(final int toastStringId) { - try { - return new JSONObject(formatErrorMessage(toastStringId)); - } catch (JSONException e) { - e.printStackTrace(); - return new JSONObject(); - } - } - - protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) { - try { - jsonObject.put(ERRORS, errorMessage); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) { - try { - jsonObject.put(ERRORS, errorMessage); - jsonObject.put(ERRORID, errorId); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - private OkHttpClient initHttpClient(JSONObject initError, String certificate) { - try { - TLSCompatSocketFactory sslCompatFactory; - ConnectionSpec spec = getConnectionSpec(); - OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); - - if (!isEmpty(certificate)) { - sslCompatFactory = new TLSCompatSocketFactory(certificate); - } else { - sslCompatFactory = new TLSCompatSocketFactory(); - } - sslCompatFactory.initSSLSocketFactory(clientBuilder); - clientBuilder.cookieJar(getCookieJar()) - .connectionSpecs(Collections.singletonList(spec)); - return clientBuilder.build(); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, resources.getString(R.string.certificate_error)); - } catch (IllegalStateException | KeyManagementException | KeyStoreException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage())); - } catch (NoSuchAlgorithmException | NoSuchProviderException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, resources.getString(error_no_such_algorithm_exception_user_message)); - } catch (CertificateException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, resources.getString(certificate_error)); - } catch (UnknownHostException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, resources.getString(server_unreachable_message)); - } catch (IOException e) { - e.printStackTrace(); - addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message)); - } - return null; - } - - protected OkHttpClient initCommercialCAHttpClient(JSONObject initError) { - return initHttpClient(initError, null); - } - - protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) { - String certificate = preferences.getString(Provider.CA_CERT, ""); - return initHttpClient(initError, certificate); - } - - protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError, String certificate) { - return initHttpClient(initError, certificate); - } - - @NonNull - private ConnectionSpec getConnectionSpec() { - ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3); - //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels - //figure out how to use bcjsse for that purpose - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) - connectionSpecbuilder.cipherSuites( - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 - ); - return connectionSpecbuilder.build(); - } - - @NonNull - private CookieJar getCookieJar() { - return new CookieJar() { - private final HashMap> cookieStore = new HashMap<>(); - - @Override - public void saveFromResponse(HttpUrl url, List cookies) { - cookieStore.put(url.host(), cookies); - } - - @Override - public List loadForRequest(HttpUrl url) { - List cookies = cookieStore.get(url.host()); - return cookies != null ? cookies : new ArrayList(); - } - }; - } - - - private Bundle tryToRegister(Bundle task) { - Bundle result = new Bundle(); - int progress = 0; - - String username = User.userName(); - String password = task.getString(SessionDialog.PASSWORD); - - if (validUserLoginData(username, password)) { - result = register(username, password); - broadcastProgress(progress++); - } else { - if (!wellFormedPassword(password)) { - result.putBoolean(RESULT_KEY, false); - result.putString(SessionDialog.USERNAME, username); - result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); - } - if (!validUsername(username)) { - result.putBoolean(RESULT_KEY, false); - result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); - } - } - - return result; - } - - private Bundle register(String username, String password) { - JSONObject stepResult = null; - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); - if (okHttpClient == null) { - return authFailedNotification(stepResult, username); - } - - LeapSRPSession client = new LeapSRPSession(username, password); - byte[] salt = client.calculateNewSalt(); - - BigInteger password_verifier = client.calculateV(username, password, salt); - - JSONObject api_result = sendNewUserDataToSRPServer(providerApiUrl, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); - - Bundle result = new Bundle(); - if (api_result.has(ERRORS)) - result = authFailedNotification(api_result, username); - else { - result.putString(SessionDialog.USERNAME, username); - result.putString(SessionDialog.PASSWORD, password); - result.putBoolean(RESULT_KEY, true); - } - - return result; - } - - /** - * Starts the authentication process using SRP protocol. - * - * @param task containing: username, password and api url. - * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if authentication was successful. - */ - private Bundle tryToAuthenticate(Bundle task) { - Bundle result = new Bundle(); - int progress = 0; - - String username = User.userName(); - String password = task.getString(SessionDialog.PASSWORD); - if (validUserLoginData(username, password)) { - result = authenticate(username, password); - broadcastProgress(progress++); - } else { - if (!wellFormedPassword(password)) { - result.putBoolean(RESULT_KEY, false); - result.putString(SessionDialog.USERNAME, username); - result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); - } - if (!validUsername(username)) { - result.putBoolean(RESULT_KEY, false); - result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); - } - } - - return result; - } - - private Bundle authenticate(String username, String password) { - Bundle result = new Bundle(); - JSONObject stepResult = new JSONObject(); - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); - if (okHttpClient == null) { - return authFailedNotification(stepResult, username); - } - - LeapSRPSession client = new LeapSRPSession(username, password); - byte[] A = client.exponential(); - - JSONObject step_result = sendAToSRPServer(providerApiUrl, username, new BigInteger(1, A).toString(16), okHttpClient); - try { - String salt = step_result.getString(LeapSRPSession.SALT); - byte[] Bbytes = new BigInteger(step_result.getString("B"), 16).toByteArray(); - byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes); - if (M1 != null) { - step_result = sendM1ToSRPServer(providerApiUrl, username, M1, okHttpClient); - setTokenIfAvailable(step_result); - byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray(); - if (client.verify(M2)) { - result.putBoolean(RESULT_KEY, true); - } else { - authFailedNotification(step_result, username); - } - } else { - result.putBoolean(RESULT_KEY, false); - result.putString(SessionDialog.USERNAME, username); - result.putString(resources.getString(R.string.user_message), resources.getString(R.string.error_srp_math_error_user_message)); - } - } catch (JSONException e) { - result = authFailedNotification(step_result, username); - e.printStackTrace(); - } - - return result; - } - - private boolean setTokenIfAvailable(JSONObject authentication_step_result) { - try { - LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); - } catch (JSONException e) { // - return false; - } - return true; - } - - private Bundle authFailedNotification(JSONObject result, String username) { - Bundle userNotificationBundle = new Bundle(); - Object baseErrorMessage = result.opt(ERRORS); - if (baseErrorMessage != null) { - if (baseErrorMessage instanceof JSONObject) { - try { - JSONObject errorMessage = result.getJSONObject(ERRORS); - String errorType = errorMessage.keys().next().toString(); - String message = errorMessage.get(errorType).toString(); - userNotificationBundle.putString(resources.getString(R.string.user_message), message); - } catch (JSONException e) { - e.printStackTrace(); - } - } else if (baseErrorMessage instanceof String) { - try { - String errorMessage = result.getString(ERRORS); - userNotificationBundle.putString(resources.getString(R.string.user_message), errorMessage); - } catch (JSONException e) { - e.printStackTrace(); - } - } - } - - if (!username.isEmpty()) - userNotificationBundle.putString(SessionDialog.USERNAME, username); - userNotificationBundle.putBoolean(RESULT_KEY, false); - - return userNotificationBundle; - } - - /** - * Sets up an intent with the progress value passed as a parameter - * and sends it as a broadcast. - * - * @param progress - */ - protected void broadcastProgress(int progress) { - Intent intentUpdate = new Intent(); - intentUpdate.setAction(UPDATE_PROGRESSBAR); - intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); - intentUpdate.putExtra(CURRENT_PROGRESS, progress); - sendBroadcast(intentUpdate); - } - - /** - * Validates parameters entered by the user to log in - * - * @param username - * @param password - * @return true if both parameters are present and the entered password length is greater or equal to eight (8). - */ - private boolean validUserLoginData(String username, String password) { - return validUsername(username) && wellFormedPassword(password); - } - - private boolean validUsername(String username) { - return username != null && !username.isEmpty(); - } - - /** - * Validates a password - * - * @param password - * @return true if the entered password length is greater or equal to eight (8). - */ - private boolean wellFormedPassword(String password) { - return password != null && password.length() >= 8; - } - - /** - * Sends an HTTP POST request to the authentication server with the SRP Parameter A. - * - * @param server_url - * @param username - * @param clientA First SRP parameter sent - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) { - SrpCredentials srpCredentials = new SrpCredentials(username, clientA); - return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient); - } - - /** - * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M). - * - * @param server_url - * @param username - * @param m1 Second SRP parameter sent - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) { - String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; - return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient); - } - - /** - * Sends an HTTP POST request to the api server to register a new user. - * - * @param server_url - * @param username - * @param salt - * @param password_verifier - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) { - return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient); - } - - /** - * Executes an HTTP request expecting a JSON response. - * - * @param url - * @param request_method - * @return response from authentication server - */ - private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) { - return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient); - } - - protected String sendGetStringToServer(String url, List> headerArgs, OkHttpClient okHttpClient) { - return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); - } - - - - private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { - JSONObject responseJson; - String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); - - try { - responseJson = new JSONObject(plain_response); - } catch (JSONException e) { - e.printStackTrace(); - responseJson = getErrorMessageAsJson(error_json_exception_user_message); - } - return responseJson; - - } - - private String requestStringFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { - Response response; - String plainResponseBody = null; - - RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null; - Request.Builder requestBuilder = new Request.Builder() - .url(url) - .method(request_method, jsonBody); - if (headerArgs != null) { - for (Pair keyValPair : headerArgs) { - requestBuilder.addHeader(keyValPair.first, keyValPair.second); - } - } - //TODO: move to getHeaderArgs()? - String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); - requestBuilder.addHeader("Accept-Language", locale); - Request request = requestBuilder.build(); - - try { - response = okHttpClient.newCall(request).execute(); - - InputStream inputStream = response.body().byteStream(); - Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - if (scanner.hasNext()) { - plainResponseBody = scanner.next(); - } - - } catch (NullPointerException npe) { - plainResponseBody = formatErrorMessage(error_json_exception_user_message); - } catch (UnknownHostException | SocketTimeoutException e) { - plainResponseBody = formatErrorMessage(server_unreachable_message); - } catch (MalformedURLException e) { - plainResponseBody = formatErrorMessage(malformed_url); - } catch (SSLHandshakeException e) { - plainResponseBody = formatErrorMessage(certificate_error); - } catch (ConnectException e) { - plainResponseBody = formatErrorMessage(service_is_down_error); - } catch (IllegalArgumentException e) { - plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); - } catch (UnknownServiceException e) { - //unable to find acceptable protocols - tlsv1.2 not enabled? - plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); - } catch (IOException e) { - plainResponseBody = formatErrorMessage(error_io_exception_user_message); - } - - return plainResponseBody; - } - - /** - * Downloads a provider.json from a given URL, adding a new provider using the given name. - * - * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. - * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. - */ - protected abstract Bundle setUpProvider(Bundle task); - - /** - * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways - * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the download was successful. - */ - protected abstract Bundle getAndSetEipServiceJson(); - - /** - * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. - * - * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. - */ - protected abstract boolean updateVpnCertificate(); - - - protected static boolean caCertDownloaded() { - return CA_CERT_DOWNLOADED; - } - - protected boolean isValidJson(String jsonString) { - try { - new JSONObject(jsonString); - return true; - } catch(JSONException e) { - return false; - } catch(NullPointerException e) { - e.printStackTrace(); - return false; - } - } - - protected boolean validCertificate(String cert_string) { - boolean result = false; - if (!ConfigHelper.checkErroneousDownload(cert_string)) { - X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string); - try { - if (certificate != null) { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - String fingerprint = provider_json.getString(Provider.CA_CERT_FINGERPRINT); - String encoding = fingerprint.split(":")[0]; - String expected_fingerprint = fingerprint.split(":")[1]; - String real_fingerprint = base64toHex(Base64.encodeToString( - MessageDigest.getInstance(encoding).digest(certificate.getEncoded()), - Base64.DEFAULT)); - - result = real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim()); - } else - result = false; - } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) { - result = false; - } - } - - return result; - } - - protected void checkPersistedProviderUpdates() { - String providerDomain = getProviderDomain(providerDefinition); - if (hasUpdatedProviderDetails(providerDomain)) { - providerCaCert = getPersistedProviderCA(providerDomain); - providerDefinition = getPersistedProviderDefinition(providerDomain); - providerCaCertFingerprint = getPersistedCaCertFingerprint(providerDomain); - providerApiUrl = getApiUrlWithVersion(providerDefinition); - } - } - - protected Bundle validateProviderDetails() { - Bundle result = validateCertificateForProvider(providerCaCert, providerDefinition, lastProviderMainUrl); - - //invalid certificate or no certificate - if (result.containsKey(ERRORS) || (result.containsKey(RESULT_KEY) && !result.getBoolean(RESULT_KEY)) ) { - return result; - } - - //valid certificate: skip download, save loaded provider CA cert and provider definition directly - try { - preferences.edit().putString(Provider.KEY, providerDefinition.toString()). - putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). - putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). - putString(Provider.CA_CERT, providerCaCert).commit(); - CA_CERT_DOWNLOADED = true; - PROVIDER_JSON_DOWNLOADED = true; - result.putBoolean(RESULT_KEY, true); - } catch (JSONException e) { - e.printStackTrace(); - result.putBoolean(RESULT_KEY, false); - result = setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); - } - - return result; - } - - protected Bundle validateCertificateForProvider(String cert_string, JSONObject providerDefinition, String mainUrl) { - Bundle result = new Bundle(); - result.putBoolean(RESULT_KEY, false); - - if (ConfigHelper.checkErroneousDownload(cert_string)) { - return result; - } - - X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string); - if (certificate == null) { - return setErrorResult(result, getString(R.string.warning_corrupted_provider_cert), ERROR_INVALID_CERTIFICATE.toString()); - } - try { - certificate.checkValidity(); - String fingerprint = getCaCertFingerprint(providerDefinition); - String encoding = fingerprint.split(":")[0]; - String expected_fingerprint = fingerprint.split(":")[1]; - String real_fingerprint = base64toHex(Base64.encodeToString( - MessageDigest.getInstance(encoding).digest(certificate.getEncoded()), - Base64.DEFAULT)); - if (!real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim())) { - return setErrorResult(result, getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); - } - - if (!hasApiUrlExpectedDomain(providerDefinition, mainUrl)){ - return setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); - } - - if (!canConnect(cert_string, providerDefinition, result)) { - return result; - } - } catch (NoSuchAlgorithmException e ) { - return setErrorResult(result, resources.getString(error_no_such_algorithm_exception_user_message), null); - } catch (ArrayIndexOutOfBoundsException e) { - return setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); - } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) { - return setErrorResult(result, getString(R.string.warning_expired_provider_cert), ERROR_INVALID_CERTIFICATE.toString()); - } - - result.putBoolean(RESULT_KEY, true); - return result; - } - - protected Bundle setErrorResult(Bundle result, String errorMessage, String errorId) { - JSONObject errorJson = new JSONObject(); - if (errorId != null) { - addErrorMessageToJson(errorJson, errorMessage, errorId); - } else { - addErrorMessageToJson(errorJson, errorMessage); - } - result.putString(ERRORS, errorJson.toString()); - return result; - } - - /** - * This method aims to prevent attacks where the provider.json file got manipulated by a third party. - * The main url is visible to the provider when setting up a new provider. - * The user is responsible to check that this is the provider main url he intends to connect to. - * - * @param providerDefinition - * @param mainUrlString - * @return - */ - private boolean hasApiUrlExpectedDomain(JSONObject providerDefinition, String mainUrlString) { - // fix against "api_uri": "https://calyx.net.malicious.url.net:4430", - String apiUrlString = getApiUrl(providerDefinition); - String providerDomain = getProviderDomain(providerDefinition); - if (mainUrlString.contains(providerDomain) && apiUrlString.contains(providerDomain + ":")) { - return true; - } - return false; - } - - private boolean canConnect(String caCert, JSONObject providerDefinition, Bundle result) { - JSONObject errorJson = new JSONObject(); - String baseUrl = getApiUrl(providerDefinition); - - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert); - if (okHttpClient == null) { - result.putString(ERRORS, errorJson.toString()); - return false; - } - - List> headerArgs = getAuthorizationHeader(); - String plain_response = requestStringFromServer(baseUrl, "GET", null, headerArgs, okHttpClient); - - try { - if (new JSONObject(plain_response).has(ERRORS)) { - result.putString(ERRORS, plain_response); - return false; - } - } catch (JSONException e) { - //eat me - } - - return true; - } - - protected String getCaCertFingerprint(JSONObject providerDefinition) { - try { - return providerDefinition.getString(Provider.CA_CERT_FINGERPRINT); - } catch (JSONException e) { - e.printStackTrace(); - } - return ""; - } - - protected String getApiUrl(JSONObject providerDefinition) { - try { - return providerDefinition.getString(Provider.API_URL); - } catch (JSONException e) { - e.printStackTrace(); - } - return ""; - } - - protected String getApiUrlWithVersion(JSONObject providerDefinition) { - try { - return providerDefinition.getString(Provider.API_URL) + "/" + providerDefinition.getString(Provider.API_VERSION); - } catch (JSONException e) { - e.printStackTrace(); - } - return ""; - } - - protected void deleteProviderDetailsFromPreferences(JSONObject providerDefinition) { - String providerDomain = getProviderDomain(providerDefinition); - - if (preferences.contains(Provider.KEY + "." + providerDomain)) { - preferences.edit().remove(Provider.KEY + "." + providerDomain).apply(); - } - if (preferences.contains(Provider.CA_CERT + "." + providerDomain)) { - preferences.edit().remove(Provider.CA_CERT + "." + providerDomain).apply(); - } - if (preferences.contains(Provider.CA_CERT_FINGERPRINT + "." + providerDomain)) { - preferences.edit().remove(Provider.CA_CERT_FINGERPRINT + "." + providerDomain).apply(); - } - } - - protected String getPersistedCaCertFingerprint(String providerDomain) { - try { - return getPersistedProviderDefinition(providerDomain).getString(Provider.CA_CERT_FINGERPRINT); - } catch (JSONException e) { - e.printStackTrace(); - } - return ""; - } - - protected JSONObject getPersistedProviderDefinition(String providerDomain) { - try { - return new JSONObject(preferences.getString(Provider.KEY + "." + providerDomain, "")); - } catch (JSONException e) { - e.printStackTrace(); - return new JSONObject(); - } - } - - protected String getPersistedProviderCA(String providerDomain) { - return preferences.getString(Provider.CA_CERT + "." + providerDomain, ""); - } - - protected String getProviderDomain(JSONObject providerDefinition) { - try { - return providerDefinition.getString(Provider.DOMAIN); - } catch (JSONException e) { - e.printStackTrace(); - } - - return ""; - } - - protected boolean hasUpdatedProviderDetails(String providerDomain) { - return preferences.contains(Provider.KEY + "." + providerDomain) && preferences.contains(Provider.CA_CERT + "." + providerDomain); - } - - /** - * Interprets the error message as a JSON object and extract the "errors" keyword pair. - * If the error message is not a JSON object, then it is returned untouched. - * - * @param string_json_error_message - * @return final error message - */ - protected String pickErrorMessage(String string_json_error_message) { - String error_message = ""; - try { - JSONObject json_error_message = new JSONObject(string_json_error_message); - error_message = json_error_message.getString(ERRORS); - } catch (JSONException e) { - // TODO Auto-generated catch block - error_message = string_json_error_message; - } - - return error_message; - } - - @NonNull - protected List> getAuthorizationHeader() { - List> headerArgs = new ArrayList<>(); - if (!LeapSRPSession.getToken().isEmpty()) { - Pair authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); - headerArgs.add(authorizationHeaderPair); - } - return headerArgs; - } - - private boolean logOut() { - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(new JSONObject()); - if (okHttpClient == null) { - return false; - } - - String deleteUrl = providerApiUrl + "/logout"; - int progress = 0; - - Request.Builder requestBuilder = new Request.Builder() - .url(deleteUrl) - .delete(); - Request request = requestBuilder.build(); - - try { - Response response = okHttpClient.newCall(request).execute(); - // v---- was already not authorized - if (response.isSuccessful() || response.code() == 401) { - broadcastProgress(progress++); - LeapSRPSession.setToken(""); - } - - } catch (IOException e) { - return false; - } - return true; - } - - //FIXME: don't save private keys in shared preferences! use the keystore - protected boolean loadCertificate(String cert_string) { - if (cert_string == null) { - return false; - } - - try { - // API returns concatenated cert & key. Split them for OpenVPN options - String certificateString = null, keyString = null; - String[] certAndKey = cert_string.split("(?<=-\n)"); - for (int i = 0; i < certAndKey.length - 1; i++) { - if (certAndKey[i].contains("KEY")) { - keyString = certAndKey[i++] + certAndKey[i]; - } else if (certAndKey[i].contains("CERTIFICATE")) { - certificateString = certAndKey[i++] + certAndKey[i]; - } - } - - RSAPrivateKey key = ConfigHelper.parseRsaKeyFromString(keyString); - keyString = Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); - preferences.edit().putString(Constants.PROVIDER_PRIVATE_KEY, "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----").commit(); - - X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certificateString); - certificateString = Base64.encodeToString(certificate.getEncoded(), Base64.DEFAULT); - preferences.edit().putString(Constants.PROVIDER_VPN_CERTIFICATE, "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----").commit(); - return true; - } catch (CertificateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } - } -} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java new file mode 100644 index 00000000..9aad14d5 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ + +package se.leap.bitmaskclient; + +import android.support.annotation.NonNull; +import android.util.Pair; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Locale; +import java.util.Scanner; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +/** + * Created by cyberta on 08.01.18. + */ + +public class ProviderApiConnector { + + private static final MediaType JSON + = MediaType.parse("application/json; charset=utf-8"); + + + public static boolean delete(OkHttpClient okHttpClient, String deleteUrl) { + try { + Request.Builder requestBuilder = new Request.Builder() + .url(deleteUrl) + .delete(); + Request request = requestBuilder.build(); + + Response response = okHttpClient.newCall(request).execute(); + if (response.isSuccessful() || response.code() == 401) { + return true; + } + } catch (IOException | RuntimeException e) { + return false; + } + + return false; + } + + public static boolean canConnect(@NonNull OkHttpClient okHttpClient, String url) { + try { + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .method("GET", null); + Request request = requestBuilder.build(); + + Response response = okHttpClient.newCall(request).execute(); + return response.isSuccessful(); + } catch (RuntimeException | IOException e) { + return false; + } + + } + + public static String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) throws RuntimeException, IOException { + + RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null; + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .method(request_method, jsonBody); + if (headerArgs != null) { + for (Pair keyValPair : headerArgs) { + requestBuilder.addHeader(keyValPair.first, keyValPair.second); + } + } + //TODO: move to getHeaderArgs()? + String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); + requestBuilder.addHeader("Accept-Language", locale); + Request request = requestBuilder.build(); + + Response response = okHttpClient.newCall(request).execute(); + InputStream inputStream = response.body().byteStream(); + Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); + if (scanner.hasNext()) { + return scanner.next(); + } + return null; + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java new file mode 100644 index 00000000..396d642b --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java @@ -0,0 +1,984 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ + +package se.leap.bitmaskclient; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.support.annotation.NonNull; +import android.util.Base64; +import android.util.Pair; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.net.ConnectException; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.net.UnknownServiceException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Scanner; + +import javax.net.ssl.SSLHandshakeException; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import se.leap.bitmaskclient.userstatus.SessionDialog; +import se.leap.bitmaskclient.userstatus.User; +import se.leap.bitmaskclient.userstatus.UserStatus; + +import static se.leap.bitmaskclient.ConfigHelper.getFingerprintFromCertificate; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE; +import static se.leap.bitmaskclient.Provider.MAIN_URL; +import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE; +import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE; +import static se.leap.bitmaskclient.ProviderAPI.CURRENT_PROGRESS; +import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_CERTIFICATE; +import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_EIP_SERVICE; +import static se.leap.bitmaskclient.ProviderAPI.ERRORID; +import static se.leap.bitmaskclient.ProviderAPI.ERRORS; +import static se.leap.bitmaskclient.ProviderAPI.FAILED_LOGIN; +import static se.leap.bitmaskclient.ProviderAPI.FAILED_SIGNUP; +import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE; +import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE; +import static se.leap.bitmaskclient.ProviderAPI.LOGOUT_FAILED; +import static se.leap.bitmaskclient.ProviderAPI.LOG_IN; +import static se.leap.bitmaskclient.ProviderAPI.LOG_OUT; +import static se.leap.bitmaskclient.ProviderAPI.PARAMETERS; +import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK; +import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_OK; +import static se.leap.bitmaskclient.ProviderAPI.RECEIVER_KEY; +import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY; +import static se.leap.bitmaskclient.ProviderAPI.SET_UP_PROVIDER; +import static se.leap.bitmaskclient.ProviderAPI.SIGN_UP; +import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGIN; +import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGOUT; +import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_SIGNUP; +import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROGRESSBAR; +import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROVIDER_DETAILS; +import static se.leap.bitmaskclient.R.string.certificate_error; +import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; +import static se.leap.bitmaskclient.R.string.error_json_exception_user_message; +import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message; +import static se.leap.bitmaskclient.R.string.malformed_url; +import static se.leap.bitmaskclient.R.string.retry; +import static se.leap.bitmaskclient.R.string.server_unreachable_message; +import static se.leap.bitmaskclient.R.string.service_is_down_error; + +/** + * Implements the logic of the http api calls. The methods of this class needs to be called from + * a background thread. + */ + +public abstract class ProviderApiManagerBase { + + public interface ProviderApiServiceCallback { + void broadcastProgress(Intent intent); + } + + private ProviderApiServiceCallback serviceCallback; + + protected static volatile boolean + CA_CERT_DOWNLOADED = false, + PROVIDER_JSON_DOWNLOADED = false, + EIP_SERVICE_JSON_DOWNLOADED = false; + + protected static String lastProviderMainUrl; + protected static boolean go_ahead = true; + protected static SharedPreferences preferences; + protected static String providerApiUrl; + protected static String providerCaCertFingerprint; + protected static String providerCaCert; + protected static JSONObject providerDefinition; + protected Resources resources; + protected OkHttpClientGenerator clientGenerator; + + public static void stop() { + go_ahead = false; + } + + public static String lastProviderMainUrl() { + return lastProviderMainUrl; + } + + + private final MediaType JSON + = MediaType.parse("application/json; charset=utf-8"); + + public ProviderApiManagerBase(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) { + this.preferences = preferences; + this.resources = resources; + this.serviceCallback = callback; + this.clientGenerator = clientGenerator; + } + + public void handleIntent(Intent command) { + final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY); + String action = command.getAction(); + Bundle parameters = command.getBundleExtra(PARAMETERS); + + if (providerApiUrl == null && preferences.contains(Provider.KEY)) { + try { + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + providerApiUrl = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION); + go_ahead = true; + } catch (JSONException e) { + go_ahead = false; + } + } + + if (action.equals(UPDATE_PROVIDER_DETAILS)) { + resetProviderDetails(); + Bundle task = new Bundle(); + task.putString(MAIN_URL, lastProviderMainUrl); + Bundle result = setUpProvider(task); + if (result.getBoolean(RESULT_KEY)) { + receiver.send(PROVIDER_OK, result); + } else { + receiver.send(PROVIDER_NOK, result); + } + } else if (action.equalsIgnoreCase(SET_UP_PROVIDER)) { + Bundle result = setUpProvider(parameters); + if (go_ahead) { + if (result.getBoolean(RESULT_KEY)) { + receiver.send(PROVIDER_OK, result); + } else { + receiver.send(PROVIDER_NOK, result); + } + } + } else if (action.equalsIgnoreCase(SIGN_UP)) { + UserStatus.updateStatus(UserStatus.SessionStatus.SIGNING_UP, resources); + Bundle result = tryToRegister(parameters); + if (result.getBoolean(RESULT_KEY)) { + receiver.send(SUCCESSFUL_SIGNUP, result); + } else { + receiver.send(FAILED_SIGNUP, result); + } + } else if (action.equalsIgnoreCase(LOG_IN)) { + UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_IN, resources); + Bundle result = tryToAuthenticate(parameters); + if (result.getBoolean(RESULT_KEY)) { + receiver.send(SUCCESSFUL_LOGIN, result); + UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_IN, resources); + } else { + receiver.send(FAILED_LOGIN, result); + UserStatus.updateStatus(UserStatus.SessionStatus.NOT_LOGGED_IN, resources); + } + } else if (action.equalsIgnoreCase(LOG_OUT)) { + UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_OUT, resources); + if (logOut()) { + receiver.send(SUCCESSFUL_LOGOUT, Bundle.EMPTY); + UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_OUT, resources); + } else { + receiver.send(LOGOUT_FAILED, Bundle.EMPTY); + UserStatus.updateStatus(UserStatus.SessionStatus.DIDNT_LOG_OUT, resources); + } + } else if (action.equalsIgnoreCase(DOWNLOAD_CERTIFICATE)) { + if (updateVpnCertificate()) { + receiver.send(CORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); + } else { + receiver.send(INCORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); + } + } else if (action.equalsIgnoreCase(DOWNLOAD_EIP_SERVICE)) { + Bundle result = getAndSetEipServiceJson(); + if (result.getBoolean(RESULT_KEY)) { + receiver.send(CORRECTLY_DOWNLOADED_EIP_SERVICE, result); + } else { + receiver.send(INCORRECTLY_DOWNLOADED_EIP_SERVICE, result); + } + } + } + + protected void resetProviderDetails() { + CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = false; + deleteProviderDetailsFromPreferences(providerDefinition); + providerCaCert = ""; + providerDefinition = new JSONObject(); + } + + protected String formatErrorMessage(final int toastStringId) { + return formatErrorMessage(resources.getString(toastStringId)); + } + + private String formatErrorMessage(String errorMessage) { + return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }"; + } + + private JSONObject getErrorMessageAsJson(final int toastStringId) { + try { + return new JSONObject(formatErrorMessage(toastStringId)); + } catch (JSONException e) { + e.printStackTrace(); + return new JSONObject(); + } + } + + protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) { + try { + jsonObject.put(ERRORS, errorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) { + try { + jsonObject.put(ERRORS, errorMessage); + jsonObject.put(ERRORID, errorId); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + + + + private Bundle tryToRegister(Bundle task) { + Bundle result = new Bundle(); + int progress = 0; + + String username = User.userName(); + String password = task.getString(SessionDialog.PASSWORD); + + if (validUserLoginData(username, password)) { + result = register(username, password); + broadcastProgress(progress++); + } else { + if (!wellFormedPassword(password)) { + result.putBoolean(RESULT_KEY, false); + result.putString(SessionDialog.USERNAME, username); + result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); + } + if (!validUsername(username)) { + result.putBoolean(RESULT_KEY, false); + result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); + } + } + + return result; + } + + private Bundle register(String username, String password) { + JSONObject stepResult = null; + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(stepResult); + if (okHttpClient == null) { + return authFailedNotification(stepResult, username); + } + + LeapSRPSession client = new LeapSRPSession(username, password); + byte[] salt = client.calculateNewSalt(); + + BigInteger password_verifier = client.calculateV(username, password, salt); + + JSONObject api_result = sendNewUserDataToSRPServer(providerApiUrl, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); + + Bundle result = new Bundle(); + if (api_result.has(ERRORS)) + result = authFailedNotification(api_result, username); + else { + result.putString(SessionDialog.USERNAME, username); + result.putString(SessionDialog.PASSWORD, password); + result.putBoolean(RESULT_KEY, true); + } + + return result; + } + + /** + * Starts the authentication process using SRP protocol. + * + * @param task containing: username, password and api url. + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if authentication was successful. + */ + private Bundle tryToAuthenticate(Bundle task) { + Bundle result = new Bundle(); + int progress = 0; + + String username = User.userName(); + String password = task.getString(SessionDialog.PASSWORD); + if (validUserLoginData(username, password)) { + result = authenticate(username, password); + broadcastProgress(progress++); + } else { + if (!wellFormedPassword(password)) { + result.putBoolean(RESULT_KEY, false); + result.putString(SessionDialog.USERNAME, username); + result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); + } + if (!validUsername(username)) { + result.putBoolean(RESULT_KEY, false); + result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); + } + } + + return result; + } + + private Bundle authenticate(String username, String password) { + Bundle result = new Bundle(); + JSONObject stepResult = new JSONObject(); + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(stepResult); + if (okHttpClient == null) { + return authFailedNotification(stepResult, username); + } + + LeapSRPSession client = new LeapSRPSession(username, password); + byte[] A = client.exponential(); + + JSONObject step_result = sendAToSRPServer(providerApiUrl, username, new BigInteger(1, A).toString(16), okHttpClient); + try { + String salt = step_result.getString(LeapSRPSession.SALT); + byte[] Bbytes = new BigInteger(step_result.getString("B"), 16).toByteArray(); + byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes); + if (M1 != null) { + step_result = sendM1ToSRPServer(providerApiUrl, username, M1, okHttpClient); + setTokenIfAvailable(step_result); + byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray(); + if (client.verify(M2)) { + result.putBoolean(RESULT_KEY, true); + } else { + authFailedNotification(step_result, username); + } + } else { + result.putBoolean(RESULT_KEY, false); + result.putString(SessionDialog.USERNAME, username); + result.putString(resources.getString(R.string.user_message), resources.getString(R.string.error_srp_math_error_user_message)); + } + } catch (JSONException e) { + result = authFailedNotification(step_result, username); + e.printStackTrace(); + } + + return result; + } + + private boolean setTokenIfAvailable(JSONObject authentication_step_result) { + try { + LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); + } catch (JSONException e) { // + return false; + } + return true; + } + + private Bundle authFailedNotification(JSONObject result, String username) { + Bundle userNotificationBundle = new Bundle(); + Object baseErrorMessage = result.opt(ERRORS); + if (baseErrorMessage != null) { + if (baseErrorMessage instanceof JSONObject) { + try { + JSONObject errorMessage = result.getJSONObject(ERRORS); + String errorType = errorMessage.keys().next().toString(); + String message = errorMessage.get(errorType).toString(); + userNotificationBundle.putString(resources.getString(R.string.user_message), message); + } catch (JSONException e) { + e.printStackTrace(); + } + } else if (baseErrorMessage instanceof String) { + try { + String errorMessage = result.getString(ERRORS); + userNotificationBundle.putString(resources.getString(R.string.user_message), errorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + if (!username.isEmpty()) + userNotificationBundle.putString(SessionDialog.USERNAME, username); + userNotificationBundle.putBoolean(RESULT_KEY, false); + + return userNotificationBundle; + } + + /** + * Sets up an intent with the progress value passed as a parameter + * and sends it as a broadcast. + * + * @param progress + */ + protected void broadcastProgress(int progress) { + Intent intentUpdate = new Intent(); + intentUpdate.setAction(UPDATE_PROGRESSBAR); + intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); + intentUpdate.putExtra(CURRENT_PROGRESS, progress); + serviceCallback.broadcastProgress(intentUpdate); + //sendBroadcast(intentUpdate); + } + + /** + * Validates parameters entered by the user to log in + * + * @param username + * @param password + * @return true if both parameters are present and the entered password length is greater or equal to eight (8). + */ + private boolean validUserLoginData(String username, String password) { + return validUsername(username) && wellFormedPassword(password); + } + + private boolean validUsername(String username) { + return username != null && !username.isEmpty(); + } + + /** + * Validates a password + * + * @param password + * @return true if the entered password length is greater or equal to eight (8). + */ + private boolean wellFormedPassword(String password) { + return password != null && password.length() >= 8; + } + + /** + * Sends an HTTP POST request to the authentication server with the SRP Parameter A. + * + * @param server_url + * @param username + * @param clientA First SRP parameter sent + * @param okHttpClient + * @return response from authentication server + */ + private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) { + SrpCredentials srpCredentials = new SrpCredentials(username, clientA); + return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient); + } + + /** + * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M). + * + * @param server_url + * @param username + * @param m1 Second SRP parameter sent + * @param okHttpClient + * @return response from authentication server + */ + private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) { + String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; + return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient); + } + + /** + * Sends an HTTP POST request to the api server to register a new user. + * + * @param server_url + * @param username + * @param salt + * @param password_verifier + * @param okHttpClient + * @return response from authentication server + */ + private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) { + return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient); + } + + /** + * Executes an HTTP request expecting a JSON response. + * + * @param url + * @param request_method + * @return response from authentication server + */ + private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) { + return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient); + } + + protected String sendGetStringToServer(String url, List> headerArgs, OkHttpClient okHttpClient) { + return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); + } + + + + private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + JSONObject responseJson; + String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); + + try { + responseJson = new JSONObject(plain_response); + } catch (JSONException e) { + e.printStackTrace(); + responseJson = getErrorMessageAsJson(error_json_exception_user_message); + } + return responseJson; + + } + + private String requestStringFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + //Response response; + String plainResponseBody = null; + + /*RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null; + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .method(request_method, jsonBody); + if (headerArgs != null) { + for (Pair keyValPair : headerArgs) { + requestBuilder.addHeader(keyValPair.first, keyValPair.second); + } + } + //TODO: move to getHeaderArgs()? + String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); + requestBuilder.addHeader("Accept-Language", locale); + Request request = requestBuilder.build(); +*/ + try { + //response = okHttpClient.newCall(request).execute(); + //response = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); + //InputStream inputStream = response.body().byteStream(); + //Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); + //if (scanner.hasNext()) { + // plainResponseBody = scanner.next(); + //} + + plainResponseBody = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); + } catch (NullPointerException npe) { + plainResponseBody = formatErrorMessage(error_json_exception_user_message); + } catch (UnknownHostException | SocketTimeoutException e) { + plainResponseBody = formatErrorMessage(server_unreachable_message); + } catch (MalformedURLException e) { + plainResponseBody = formatErrorMessage(malformed_url); + } catch (SSLHandshakeException e) { + plainResponseBody = formatErrorMessage(certificate_error); + } catch (ConnectException e) { + plainResponseBody = formatErrorMessage(service_is_down_error); + } catch (IllegalArgumentException e) { + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); + } catch (UnknownServiceException e) { + //unable to find acceptable protocols - tlsv1.2 not enabled? + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); + } catch (IOException e) { + plainResponseBody = formatErrorMessage(error_io_exception_user_message); + } + + return plainResponseBody; + } + + /** + * Downloads a provider.json from a given URL, adding a new provider using the given name. + * + * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. + */ + protected abstract Bundle setUpProvider(Bundle task); + + /** + * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the download was successful. + */ + protected abstract Bundle getAndSetEipServiceJson(); + + /** + * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. + * + * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. + */ + protected abstract boolean updateVpnCertificate(); + + + protected static boolean caCertDownloaded() { + return CA_CERT_DOWNLOADED; + } + + protected boolean isValidJson(String jsonString) { + try { + new JSONObject(jsonString); + return true; + } catch(JSONException e) { + return false; + } catch(NullPointerException e) { + e.printStackTrace(); + return false; + } + } + + protected boolean validCertificate(String cert_string) { + boolean result = false; + if (!ConfigHelper.checkErroneousDownload(cert_string)) { + X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string); + try { + if (certificate != null) { + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + String fingerprint = provider_json.getString(Provider.CA_CERT_FINGERPRINT); + String encoding = fingerprint.split(":")[0]; + String expected_fingerprint = fingerprint.split(":")[1]; + String real_fingerprint = getFingerprintFromCertificate(certificate, encoding); + + result = real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim()); + } else + result = false; + } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException /*| UnsupportedEncodingException*/ e) { + result = false; + } + } + + return result; + } + + + + protected void checkPersistedProviderUpdates() { + //String providerDomain = getProviderDomain(providerDefinition); + String providerDomain = getDomainFromMainURL(lastProviderMainUrl); + if (hasUpdatedProviderDetails(providerDomain)) { + providerCaCert = getPersistedProviderCA(providerDomain); + providerDefinition = getPersistedProviderDefinition(providerDomain); + providerCaCertFingerprint = getPersistedCaCertFingerprint(providerDomain); + providerApiUrl = getApiUrlWithVersion(providerDefinition); + } + } + + protected Bundle validateProviderDetails() { + Bundle result = validateCertificateForProvider(providerCaCert, providerDefinition, lastProviderMainUrl); + + //invalid certificate or no certificate + if (result.containsKey(ERRORS) || (result.containsKey(RESULT_KEY) && !result.getBoolean(RESULT_KEY)) ) { + return result; + } + + //valid certificate: skip download, save loaded provider CA cert and provider definition directly + try { + preferences.edit().putString(Provider.KEY, providerDefinition.toString()). + putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). + putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). + putString(Provider.CA_CERT, providerCaCert).commit(); + CA_CERT_DOWNLOADED = true; + PROVIDER_JSON_DOWNLOADED = true; + result.putBoolean(RESULT_KEY, true); + } catch (JSONException e) { + e.printStackTrace(); + result.putBoolean(RESULT_KEY, false); + result = setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + } + + return result; + } + + protected Bundle validateCertificateForProvider(String cert_string, JSONObject providerDefinition, String mainUrl) { + Bundle result = new Bundle(); + result.putBoolean(RESULT_KEY, false); + + if (ConfigHelper.checkErroneousDownload(cert_string)) { + return result; + } + + X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string); + if (certificate == null) { + return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_INVALID_CERTIFICATE.toString()); + } + try { + certificate.checkValidity(); + String fingerprint = getCaCertFingerprint(providerDefinition); + String encoding = fingerprint.split(":")[0]; + String expected_fingerprint = fingerprint.split(":")[1]; + String real_fingerprint = getFingerprintFromCertificate(certificate, encoding); + if (!real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim())) { + return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); + } + + + if (!hasApiUrlExpectedDomain(providerDefinition, mainUrl)){ + return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + } + + if (!canConnect(cert_string, providerDefinition, result)) { + return result; + } + } catch (NoSuchAlgorithmException e ) { + return setErrorResult(result, resources.getString(error_no_such_algorithm_exception_user_message), null); + } catch (ArrayIndexOutOfBoundsException e) { + return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) { + return setErrorResult(result, resources.getString(R.string.warning_expired_provider_cert), ERROR_INVALID_CERTIFICATE.toString()); + } /*catch (UnsupportedEncodingException e) { + return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); + }*/ + + result.putBoolean(RESULT_KEY, true); + return result; + } + + protected Bundle setErrorResult(Bundle result, String errorMessage, String errorId) { + JSONObject errorJson = new JSONObject(); + if (errorId != null) { + addErrorMessageToJson(errorJson, errorMessage, errorId); + } else { + addErrorMessageToJson(errorJson, errorMessage); + } + result.putString(ERRORS, errorJson.toString()); + return result; + } + + /** + * This method aims to prevent attacks where the provider.json file got manipulated by a third party. + * The main url is visible to the provider when setting up a new provider. + * The user is responsible to check that this is the provider main url he intends to connect to. + * + * @param providerDefinition + * @param mainUrlString + * @return + */ + private boolean hasApiUrlExpectedDomain(JSONObject providerDefinition, String mainUrlString) { + // fix against "api_uri": "https://calyx.net.malicious.url.net:4430", + String apiUrlString = getApiUrl(providerDefinition); + String providerDomain = getProviderDomain(providerDefinition); + if (mainUrlString.contains(providerDomain) && apiUrlString.contains(providerDomain + ":")) { + return true; + } + return false; + } + + private boolean canConnect(String caCert, JSONObject providerDefinition, Bundle result) { + JSONObject errorJson = new JSONObject(); + String baseUrl = getApiUrl(providerDefinition); + + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert); + if (okHttpClient == null) { + result.putString(ERRORS, errorJson.toString()); + return false; + } + + //try { + + return ProviderApiConnector.canConnect(okHttpClient, baseUrl); + /*} catch (RuntimeException | IOException e) { + e.printStackTrace(); + }*/ + + //return false; + + + /*List> headerArgs = getAuthorizationHeader(); + String plain_response = requestStringFromServer(baseUrl, "GET", null, headerArgs, okHttpClient); + + try { + if (new JSONObject(plain_response).has(ERRORS)) { + result.putString(ERRORS, plain_response); + return false; + } + } catch (JSONException e) { + //eat me + } + + return true;*/ + } + + protected String getCaCertFingerprint(JSONObject providerDefinition) { + try { + return providerDefinition.getString(Provider.CA_CERT_FINGERPRINT); + } catch (JSONException e) { + e.printStackTrace(); + } + return ""; + } + + protected String getApiUrl(JSONObject providerDefinition) { + try { + return providerDefinition.getString(Provider.API_URL); + } catch (JSONException e) { + e.printStackTrace(); + } + return ""; + } + + protected String getApiUrlWithVersion(JSONObject providerDefinition) { + try { + return providerDefinition.getString(Provider.API_URL) + "/" + providerDefinition.getString(Provider.API_VERSION); + } catch (JSONException e) { + e.printStackTrace(); + } + return ""; + } + + protected void deleteProviderDetailsFromPreferences(JSONObject providerDefinition) { + String providerDomain = getProviderDomain(providerDefinition); + + if (preferences.contains(Provider.KEY + "." + providerDomain)) { + preferences.edit().remove(Provider.KEY + "." + providerDomain).apply(); + } + if (preferences.contains(Provider.CA_CERT + "." + providerDomain)) { + preferences.edit().remove(Provider.CA_CERT + "." + providerDomain).apply(); + } + if (preferences.contains(Provider.CA_CERT_FINGERPRINT + "." + providerDomain)) { + preferences.edit().remove(Provider.CA_CERT_FINGERPRINT + "." + providerDomain).apply(); + } + } + + protected String getPersistedCaCertFingerprint(String providerDomain) { + try { + return getPersistedProviderDefinition(providerDomain).getString(Provider.CA_CERT_FINGERPRINT); + } catch (JSONException e) { + e.printStackTrace(); + } + return ""; + } + + protected JSONObject getPersistedProviderDefinition(String providerDomain) { + try { + return new JSONObject(preferences.getString(Provider.KEY + "." + providerDomain, "")); + } catch (JSONException e) { + e.printStackTrace(); + return new JSONObject(); + } + } + + protected String getPersistedProviderCA(String providerDomain) { + return preferences.getString(Provider.CA_CERT + "." + providerDomain, ""); + } + + protected String getProviderDomain(JSONObject providerDefinition) { + try { + return providerDefinition.getString(Provider.DOMAIN); + } catch (JSONException e) { + e.printStackTrace(); + } + + return ""; + } + + protected boolean hasUpdatedProviderDetails(String domain) { + return preferences.contains(Provider.KEY + "." + domain) && preferences.contains(Provider.CA_CERT + "." + domain); + } + + protected String getDomainFromMainURL(String mainUrl) { + return mainUrl.replaceFirst("http[s]?://", "").replaceFirst("/.*", ""); + + } + + /** + * Interprets the error message as a JSON object and extract the "errors" keyword pair. + * If the error message is not a JSON object, then it is returned untouched. + * + * @param string_json_error_message + * @return final error message + */ + protected String pickErrorMessage(String string_json_error_message) { + String error_message = ""; + try { + JSONObject json_error_message = new JSONObject(string_json_error_message); + error_message = json_error_message.getString(ERRORS); + } catch (JSONException e) { + // TODO Auto-generated catch block + error_message = string_json_error_message; + } catch (NullPointerException e) { + //do nothing + } + + return error_message; + } + + @NonNull + protected List> getAuthorizationHeader() { + List> headerArgs = new ArrayList<>(); + if (!LeapSRPSession.getToken().isEmpty()) { + Pair authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); + headerArgs.add(authorizationHeaderPair); + } + return headerArgs; + } + + private boolean logOut() { + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(new JSONObject()); + if (okHttpClient == null) { + return false; + } + + String deleteUrl = providerApiUrl + "/logout"; + int progress = 0; + + /* Request.Builder requestBuilder = new Request.Builder() + .url(deleteUrl) + .delete(); + Request request = requestBuilder.build();*/ + + //try { + + //Response response = okHttpClient.newCall(request).execute(); +// Response response = ProviderApiConnector.delete(okHttpClient, deleteUrl); +// // v---- was already not authorized +// if (response.isSuccessful() || response.code() == 401) { +// broadcastProgress(progress++); +// LeapSRPSession.setToken(""); +// } +// +// } catch (IOException | RuntimeException e) { +// return false; +// } +// return true; + + if (ProviderApiConnector.delete(okHttpClient, deleteUrl)) { + broadcastProgress(progress++); + LeapSRPSession.setToken(""); + return true; + } + return false; + } + + //FIXME: don't save private keys in shared preferences! use the keystore + protected boolean loadCertificate(String cert_string) { + if (cert_string == null) { + return false; + } + + try { + // API returns concatenated cert & key. Split them for OpenVPN options + String certificateString = null, keyString = null; + String[] certAndKey = cert_string.split("(?<=-\n)"); + for (int i = 0; i < certAndKey.length - 1; i++) { + if (certAndKey[i].contains("KEY")) { + keyString = certAndKey[i++] + certAndKey[i]; + } else if (certAndKey[i].contains("CERTIFICATE")) { + certificateString = certAndKey[i++] + certAndKey[i]; + } + } + + RSAPrivateKey key = ConfigHelper.parseRsaKeyFromString(keyString); + keyString = Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); + preferences.edit().putString(Constants.PROVIDER_PRIVATE_KEY, "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----").commit(); + + X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certificateString); + certificateString = Base64.encodeToString(certificate.getEncoded(), Base64.DEFAULT); + preferences.edit().putString(Constants.PROVIDER_VPN_CERTIFICATE, "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----").commit(); + return true; + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } + } +} diff --git a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java index 61349490..bd9324bb 100644 --- a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java +++ b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java @@ -23,8 +23,6 @@ import android.view.*; import android.widget.*; import butterknife.*; -import se.leap.bitmaskclient.ProviderAPI; -import se.leap.bitmaskclient.VpnFragment; import se.leap.bitmaskclient.Provider; import se.leap.bitmaskclient.R; diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java deleted file mode 100644 index 39651a43..00000000 --- a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java +++ /dev/null @@ -1,327 +0,0 @@ - -/** - * Copyright (c) 2013 LEAP Encryption Access Project and contributers - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 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 . - */ -package se.leap.bitmaskclient; - -import android.os.Bundle; -import android.util.Pair; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.URL; -import java.util.List; - -import okhttp3.OkHttpClient; -import se.leap.bitmaskclient.eip.EIP; - -import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON; -import static se.leap.bitmaskclient.R.string.malformed_url; - -/** - * Implements HTTP api methods used to manage communications with the provider server. - * It extends the abstract ProviderApiBase and implements the diverging method calls between the different flavors - * of ProviderAPI. - *

- * It extends an IntentService because it downloads data from the Internet, so it operates in the background. - * - * @author parmegv - * @author MeanderingCode - * @author cyberta - */ -public class ProviderAPI extends ProviderApiBase { - - /** - * Downloads a provider.json from a given URL, adding a new provider using the given name. - * - * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. - * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. - */ - @Override - protected Bundle setUpProvider(Bundle task) { - int progress = 0; - Bundle currentDownload = new Bundle(); - - if (task != null) { - //FIXME: this should be refactored in order to avoid static variables all over here - lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ? - task.getString(Provider.MAIN_URL) : - ""; - //TODO: remove that - providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ? - task.getString(Provider.CA_CERT_FINGERPRINT) : - ""; - providerCaCert = task.containsKey(Provider.CA_CERT) ? - task.getString(Provider.CA_CERT) : - ""; - - try { - providerDefinition = task.containsKey(Provider.KEY) ? - new JSONObject(task.getString(Provider.KEY)) : - new JSONObject(); - } catch (JSONException e) { - e.printStackTrace(); - providerDefinition = new JSONObject(); - } - providerApiUrl = getApiUrlWithVersion(providerDefinition); - - checkPersistedProviderUpdates(); - currentDownload = validateProviderDetails(); - - //provider details invalid - if (currentDownload.containsKey(ERRORS)) { - return currentDownload; - } - - //no provider certificate available - if (currentDownload.containsKey(RESULT_KEY) && !currentDownload.getBoolean(RESULT_KEY)) { - resetProviderDetails(); - } - - EIP_SERVICE_JSON_DOWNLOADED = false; - go_ahead = true; - } - - if (!PROVIDER_JSON_DOWNLOADED) - currentDownload = getAndSetProviderJson(lastProviderMainUrl, providerCaCert, providerDefinition); - if (PROVIDER_JSON_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { - broadcastProgress(progress++); - PROVIDER_JSON_DOWNLOADED = true; - - if (!CA_CERT_DOWNLOADED) - currentDownload = downloadCACert(); - if (CA_CERT_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { - broadcastProgress(progress++); - CA_CERT_DOWNLOADED = true; - currentDownload = getAndSetEipServiceJson(); - if (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY)) { - broadcastProgress(progress++); - EIP_SERVICE_JSON_DOWNLOADED = true; - } - } - } - - return currentDownload; - } - - - private Bundle getAndSetProviderJson(String providerMainUrl, String caCert, JSONObject providerDefinition) { - Bundle result = new Bundle(); - - if (go_ahead) { - String providerDotJsonString; - if(providerDefinition.length() == 0 || caCert.isEmpty()) - providerDotJsonString = downloadWithCommercialCA(providerMainUrl + "/provider.json"); - else { - providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition); - } - - if (!isValidJson(providerDotJsonString)) { - result.putString(ERRORS, getString(malformed_url)); - result.putBoolean(RESULT_KEY, false); - return result; - } - - try { - JSONObject providerJson = new JSONObject(providerDotJsonString); - String providerDomain = providerJson.getString(Provider.DOMAIN); - providerApiUrl = getApiUrlWithVersion(providerJson); - String name = providerJson.getString(Provider.NAME); - //TODO setProviderName(name); - - preferences.edit().putString(Provider.KEY, providerJson.toString()). - putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). - putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). - putString(Provider.KEY + "." + providerDomain, providerJson.toString()).commit(); - result.putBoolean(RESULT_KEY, true); - } catch (JSONException e) { - String reason_to_fail = pickErrorMessage(providerDotJsonString); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - } - return result; - } - - /** - * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways - * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the download was successful. - */ - @Override - protected Bundle getAndSetEipServiceJson() { - Bundle result = new Bundle(); - String eip_service_json_string = ""; - if (go_ahead) { - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH; - eip_service_json_string = downloadWithProviderCA(eip_service_url); - JSONObject eip_service_json = new JSONObject(eip_service_json_string); - eip_service_json.getInt(Provider.API_RETURN_SERIAL); - - preferences.edit().putString(Constants.PROVIDER_KEY, eip_service_json.toString()).commit(); - - result.putBoolean(RESULT_KEY, true); - } catch (NullPointerException | JSONException e) { - String reason_to_fail = pickErrorMessage(eip_service_json_string); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - } - return result; - } - - /** - * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. - * - * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. - */ - @Override - protected boolean updateVpnCertificate() { - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - - String provider_main_url = provider_json.getString(Provider.API_URL); - URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.PROVIDER_VPN_CERTIFICATE); - - String cert_string = downloadWithProviderCA(new_cert_string_url.toString()); - - if (ConfigHelper.checkErroneousDownload(cert_string)) - return false; - else - return loadCertificate(cert_string); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } - } - - private Bundle downloadCACert() { - Bundle result = new Bundle(); - try { - JSONObject providerJson = new JSONObject(preferences.getString(Provider.KEY, "")); - String caCertUrl = providerJson.getString(Provider.CA_CERT_URI); - String providerDomain = providerJson.getString(Provider.DOMAIN); - - String cert_string = downloadWithCommercialCA(caCertUrl); - - if (validCertificate(cert_string) && go_ahead) { - preferences.edit().putString(Provider.CA_CERT, cert_string).commit(); - preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, cert_string).commit(); - result.putBoolean(RESULT_KEY, true); - } else { - String reason_to_fail = pickErrorMessage(cert_string); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - } catch (JSONException e) { - String reason_to_fail = formatErrorMessage(malformed_url); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - - return result; - } - - /** - * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider. - * - * @param string_url - * @return - */ - protected String downloadWithCommercialCA(String string_url) { - String responseString; - JSONObject errorJson = new JSONObject(); - - OkHttpClient okHttpClient = initCommercialCAHttpClient(errorJson); - if (okHttpClient == null) { - return errorJson.toString(); - } - - List> headerArgs = getAuthorizationHeader(); - - responseString = sendGetStringToServer(string_url, headerArgs, okHttpClient); - - if (responseString != null && responseString.contains(ERRORS)) { - try { - // try to download with provider CA on certificate error - JSONObject responseErrorJson = new JSONObject(responseString); - if (responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) { - responseString = downloadWithProviderCA(string_url); - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - - return responseString; - } - - - /** - * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. - * - * @return an empty string if it fails, the response body if not. - */ - private String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition) { - String responseString; - JSONObject errorJson = new JSONObject(); - String baseUrl = getApiUrl(providerDefinition); - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert); - if (okHttpClient == null) { - return errorJson.toString(); - } - - String urlString = baseUrl + path; - List> headerArgs = getAuthorizationHeader(); - responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); - - return responseString; - - } - - - - /** - * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. - * - * @param urlString as a string - * @return an empty string if it fails, the url content if not. - */ - protected String downloadWithProviderCA(String urlString) { - JSONObject initError = new JSONObject(); - String responseString; - - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(initError); - if (okHttpClient == null) { - return initError.toString(); - } - - List> headerArgs = getAuthorizationHeader(); - - responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); - - return responseString; - } - -} diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java new file mode 100644 index 00000000..31c63d01 --- /dev/null +++ b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java @@ -0,0 +1,340 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ + +package se.leap.bitmaskclient; + +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Bundle; +import android.util.Pair; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import okhttp3.OkHttpClient; +import se.leap.bitmaskclient.eip.EIP; + +import static android.text.TextUtils.isEmpty; +import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING; +import static se.leap.bitmaskclient.ProviderAPI.ERRORS; +import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY; +import static se.leap.bitmaskclient.R.string.malformed_url; + +/** + * Implements the logic of the provider api http requests. The methods of this class need to be called from + * a background thread. + */ + + +public class ProviderApiManager extends ProviderApiManagerBase { + + public ProviderApiManager(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) { + super(preferences, resources, clientGenerator, callback); + } + + /** + * Only used in insecure flavor. + */ + static boolean lastDangerOn() { + return false; + } + + /** + * Downloads a provider.json from a given URL, adding a new provider using the given name. + * + * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. + */ + @Override + protected Bundle setUpProvider(Bundle task) { + int progress = 0; + Bundle currentDownload = new Bundle(); + + if (task != null) { + //FIXME: this should be refactored in order to avoid static variables all over here + lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ? + task.getString(Provider.MAIN_URL) : + ""; + + if (isEmpty(lastProviderMainUrl)) { + currentDownload.putBoolean(RESULT_KEY, false); + setErrorResult(currentDownload, resources.getString(R.string.malformed_url), null); + return currentDownload; + } + + //TODO: remove that + providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ? + task.getString(Provider.CA_CERT_FINGERPRINT) : + ""; + providerCaCert = task.containsKey(Provider.CA_CERT) ? + task.getString(Provider.CA_CERT) : + ""; + + try { + providerDefinition = task.containsKey(Provider.KEY) ? + new JSONObject(task.getString(Provider.KEY)) : + new JSONObject(); + } catch (JSONException e) { + e.printStackTrace(); + providerDefinition = new JSONObject(); + } + providerApiUrl = getApiUrlWithVersion(providerDefinition); + + checkPersistedProviderUpdates(); + currentDownload = validateProviderDetails(); + + //provider details invalid + if (currentDownload.containsKey(ERRORS)) { + return currentDownload; + } + + //no provider certificate available + if (currentDownload.containsKey(RESULT_KEY) && !currentDownload.getBoolean(RESULT_KEY)) { + resetProviderDetails(); + } + + EIP_SERVICE_JSON_DOWNLOADED = false; + go_ahead = true; + } + + if (!PROVIDER_JSON_DOWNLOADED) + currentDownload = getAndSetProviderJson(lastProviderMainUrl, providerCaCert, providerDefinition); + if (PROVIDER_JSON_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { + broadcastProgress(progress++); + PROVIDER_JSON_DOWNLOADED = true; + + if (!CA_CERT_DOWNLOADED) + currentDownload = downloadCACert(); + if (CA_CERT_DOWNLOADED || (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY))) { + broadcastProgress(progress++); + CA_CERT_DOWNLOADED = true; + currentDownload = getAndSetEipServiceJson(); + if (currentDownload.containsKey(RESULT_KEY) && currentDownload.getBoolean(RESULT_KEY)) { + broadcastProgress(progress++); + EIP_SERVICE_JSON_DOWNLOADED = true; + } + } + } + + return currentDownload; + } + + + private Bundle getAndSetProviderJson(String providerMainUrl, String caCert, JSONObject providerDefinition) { + Bundle result = new Bundle(); + + if (go_ahead) { + String providerDotJsonString; + if(providerDefinition.length() == 0 || caCert.isEmpty()) + providerDotJsonString = downloadWithCommercialCA(providerMainUrl + "/provider.json"); + else { + providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition); + } + + if (!isValidJson(providerDotJsonString)) { + result.putString(ERRORS, resources.getString(malformed_url)); + result.putBoolean(RESULT_KEY, false); + return result; + } + + try { + JSONObject providerJson = new JSONObject(providerDotJsonString); + String providerDomain = getDomainFromMainURL(lastProviderMainUrl); + providerApiUrl = getApiUrlWithVersion(providerJson); + //String name = providerJson.getString(Provider.NAME); + //TODO setProviderName(name); + + preferences.edit().putString(Provider.KEY, providerJson.toString()). + putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)). + putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerJson.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)). + putString(Provider.KEY + "." + providerDomain, providerJson.toString()).commit(); + result.putBoolean(RESULT_KEY, true); + } catch (JSONException e) { + String reason_to_fail = pickErrorMessage(providerDotJsonString); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + } + return result; + } + + /** + * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the download was successful. + */ + @Override + protected Bundle getAndSetEipServiceJson() { + Bundle result = new Bundle(); + String eip_service_json_string = ""; + if (go_ahead) { + try { + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH; + eip_service_json_string = downloadWithProviderCA(eip_service_url); + JSONObject eip_service_json = new JSONObject(eip_service_json_string); + eip_service_json.getInt(Provider.API_RETURN_SERIAL); + + preferences.edit().putString(Constants.PROVIDER_KEY, eip_service_json.toString()).commit(); + + result.putBoolean(RESULT_KEY, true); + } catch (NullPointerException | JSONException e) { + String reason_to_fail = pickErrorMessage(eip_service_json_string); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + } + return result; + } + + /** + * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. + * + * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. + */ + @Override + protected boolean updateVpnCertificate() { + try { + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + + String provider_main_url = provider_json.getString(Provider.API_URL); + URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.PROVIDER_VPN_CERTIFICATE); + + String cert_string = downloadWithProviderCA(new_cert_string_url.toString()); + + if (ConfigHelper.checkErroneousDownload(cert_string)) + return false; + else + return loadCertificate(cert_string); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } + } + + private Bundle downloadCACert() { + Bundle result = new Bundle(); + try { + JSONObject providerJson = new JSONObject(preferences.getString(Provider.KEY, "")); + String caCertUrl = providerJson.getString(Provider.CA_CERT_URI); + String providerDomain = getDomainFromMainURL(lastProviderMainUrl); + String cert_string = downloadWithCommercialCA(caCertUrl); + + if (validCertificate(cert_string) && go_ahead) { + preferences.edit().putString(Provider.CA_CERT, cert_string).commit(); + preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, cert_string).commit(); + result.putBoolean(RESULT_KEY, true); + } else { + setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); + result.putBoolean(RESULT_KEY, false); + } + } catch (JSONException e) { + setErrorResult(result, resources.getString(malformed_url), null); + result.putBoolean(RESULT_KEY, false); + } + + return result; + } + + /** + * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider. + * + * @param string_url + * @return + */ + private String downloadWithCommercialCA(String string_url) { + String responseString; + JSONObject errorJson = new JSONObject(); + + OkHttpClient okHttpClient = clientGenerator.initCommercialCAHttpClient(errorJson); + if (okHttpClient == null) { + return errorJson.toString(); + } + + List> headerArgs = getAuthorizationHeader(); + + responseString = sendGetStringToServer(string_url, headerArgs, okHttpClient); + + if (responseString != null && responseString.contains(ERRORS)) { + try { + // try to download with provider CA on certificate error + JSONObject responseErrorJson = new JSONObject(responseString); + if (responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) { + responseString = downloadWithProviderCA(string_url); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + return responseString; + } + + + /** + * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. + * + * @return an empty string if it fails, the response body if not. + */ + private String downloadFromApiUrlWithProviderCA(String path, String caCert, JSONObject providerDefinition) { + String responseString; + JSONObject errorJson = new JSONObject(); + String baseUrl = getApiUrl(providerDefinition); + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert); + if (okHttpClient == null) { + return errorJson.toString(); + } + + String urlString = baseUrl + path; + List> headerArgs = getAuthorizationHeader(); + responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); + + return responseString; + + } + + + /** + * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. + * + * @param urlString as a string + * @return an empty string if it fails, the url content if not. + */ + private String downloadWithProviderCA(String urlString) { + JSONObject initError = new JSONObject(); + String responseString; + + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(initError); + if (okHttpClient == null) { + return initError.toString(); + } + + List> headerArgs = getAuthorizationHeader(); + + responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); + + return responseString; + } +} diff --git a/app/src/test/java/se/leap/bitmaskclient/TestUtils.java b/app/src/test/java/se/leap/bitmaskclient/TestUtils.java deleted file mode 100644 index 96b11df6..00000000 --- a/app/src/test/java/se/leap/bitmaskclient/TestUtils.java +++ /dev/null @@ -1,26 +0,0 @@ -package se.leap.bitmaskclient; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -/** - * Created by cyberta on 08.10.17. - */ - -public class TestUtils { - - public static String getInputAsString(InputStream fileAsInputStream) throws IOException { - BufferedReader br = new BufferedReader(new InputStreamReader(fileAsInputStream)); - StringBuilder sb = new StringBuilder(); - String line = br.readLine(); - while (line != null) { - sb.append(line); - line = br.readLine(); - } - - return sb.toString(); - } - -} diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java index ea212480..4726cab7 100644 --- a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java +++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java @@ -16,7 +16,7 @@ import java.io.IOException; import se.leap.bitmaskclient.Constants; import se.leap.bitmaskclient.Provider; -import se.leap.bitmaskclient.TestUtils; +import se.leap.bitmaskclient.testutils.TestSetupHelper; import static junit.framework.Assert.assertEquals; import static org.mockito.ArgumentMatchers.anyInt; @@ -61,17 +61,17 @@ public class GatewaysManagerTest { @Test public void testFromEipServiceJson_ignoreDuplicateGateways() throws Exception { - String eipServiceJson = TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json")); + String eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json")); gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson)); assertEquals(2, gatewaysManager.size()); - eipServiceJson = TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-one-gateway.json")); + eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-one-gateway.json")); gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson)); assertEquals(2, gatewaysManager.size()); } @Test public void testClearGatewaysAndProfiles_resetGateways() throws Exception { - String eipServiceJson = TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json")); + String eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json")); gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson)); assertEquals(2, gatewaysManager.size()); gatewaysManager.clearGatewaysAndProfiles(); @@ -79,7 +79,7 @@ public class GatewaysManagerTest { } private String getJsonStringFor(String filename) throws IOException { - return TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream(filename)); + return TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream(filename)); } } \ No newline at end of file diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java new file mode 100644 index 00000000..c39681c4 --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java @@ -0,0 +1,249 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ + +package se.leap.bitmaskclient.eip; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Bundle; +import android.text.TextUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; + +import se.leap.bitmaskclient.ConfigHelper; +import se.leap.bitmaskclient.Provider; +import se.leap.bitmaskclient.ProviderAPI; +import se.leap.bitmaskclient.ProviderApiConnector; +import se.leap.bitmaskclient.ProviderApiManager; +import se.leap.bitmaskclient.ProviderApiManagerBase; +import se.leap.bitmaskclient.testutils.MockSharedPreferences; + +import static se.leap.bitmaskclient.ProviderAPI.ERRORS; +import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK; +import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_OK; +import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY; +import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString; +import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockBundle; +import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockClientGenerator; +import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockFingerprintForCertificate; +import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockIntent; +import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockProviderApiConnector; +import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockResources; +import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockResultReceiver; +import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockTextUtils; +import static se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric.TestBackendErrorCase.NO_ERROR; + +/** + * Created by cyberta on 04.01.18. + */ + +@RunWith(PowerMockRunner.class) +@PrepareForTest({ProviderApiManager.class, TextUtils.class, ConfigHelper.class, ProviderApiConnector.class}) +public class ProviderApiManagerTest { + + private SharedPreferences mockPreferences; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Resources mockResources; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Context mockContext; + + private ProviderApiManager providerApiManager; + + class TestProviderApiServiceCallback implements ProviderApiManagerBase.ProviderApiServiceCallback { + + //Intent expectedIntent; + TestProviderApiServiceCallback(/*Intent expectedIntent*/) { + //this.expectedIntent = expectedIntent; + } + + @Override + public void broadcastProgress(Intent intent) { + //assertEquals("expected intent: ", expectedIntent, intent); + } + } + + @Before + public void setUp() throws Exception { + + Bundle bundle = mockBundle(); + PowerMockito.whenNew(Bundle.class).withAnyArguments().thenReturn(bundle); + Intent intent = mockIntent(); + PowerMockito.whenNew(Intent.class).withAnyArguments().thenReturn(intent); + mockTextUtils(); + mockPreferences = new MockSharedPreferences(); + mockResources = mockResources(getClass().getClassLoader().getResourceAsStream("error_messages.json")); + } + + + @Test + public void test_handleIntentSetupProvider_noProviderMainURL() { + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, false); + expectedResult.putString(ERRORS, "{\"errors\":\"It doesn't seem to be a Bitmask provider.\"}"); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, ""); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + @Test + public void test_handleIntentSetupProvider_happyPath_preseededProviderAndCA() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494"); + mockProviderApiConnector(NO_ERROR); + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, true); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))); + parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + @Test + public void test_handleIntentSetupProvider_happyPath_no_preseededProviderAndCA() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494"); + mockProviderApiConnector(NO_ERROR); + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, true); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + @Test + public void test_handleIntentSetupProvider_happyPath_storedProviderAndCAFromPreviousSetup() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494"); + mockProviderApiConnector(NO_ERROR); + mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply(); + mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))).apply(); + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, true); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + @Test + public void test_handleIntentSetupProvider_preseededProviderAndCA_failedCAPinning() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495"); + mockProviderApiConnector(NO_ERROR); + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, false); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))); + parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + @Test + public void test_handleIntentSetupProvider_no_preseededProviderAndCA_failedPinning() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495"); + mockProviderApiConnector(NO_ERROR); + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, false); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + @Test + public void test_handleIntentSetupProvider_storedProviderAndCAFromPreviousSetup_failedPinning() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495"); + mockProviderApiConnector(NO_ERROR); + mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply(); + mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))).apply(); + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, false); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + +} diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java index 7e60edb9..8c8cdb61 100644 --- a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java +++ b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java @@ -4,7 +4,7 @@ import org.json.JSONObject; import org.junit.Before; import org.junit.Test; -import se.leap.bitmaskclient.TestUtils; +import se.leap.bitmaskclient.testutils.TestSetupHelper; import static junit.framework.Assert.assertTrue; @@ -217,13 +217,13 @@ public class VpnConfigGeneratorTest { @Before public void setUp() throws Exception { - generalConfig = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("general_configuration.json"))); - secrets = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("secrets.json"))); + generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("general_configuration.json"))); + secrets = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("secrets.json"))); } @Test public void testGenerate_tcp_udp() throws Exception { - gateway = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json"))); + gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json"))); vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway); String vpnConfig = vpnConfigGenerator.generate(); @@ -232,7 +232,7 @@ public class VpnConfigGeneratorTest { @Test public void testGenerate_udp_tcp() throws Exception { - gateway = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json"))); + gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json"))); vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway); String vpnConfig = vpnConfigGenerator.generate(); diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java b/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java new file mode 100644 index 00000000..af35af31 --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java @@ -0,0 +1,165 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ + +package se.leap.bitmaskclient.testutils; + +import android.content.SharedPreferences; +import android.support.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Created by cyberta on 09.01.18. + */ + +public class MockSharedPreferences implements SharedPreferences { + HashMap mockedStringPrefs = new HashMap<>(); + HashMap mockedIntPrefs = new HashMap<>(); + HashMap mockedBooleanPrefs = new HashMap<>(); + + @Override + public Map getAll() { + return null; + } + + @Nullable + @Override + public String getString(String key, @Nullable String defValue) { + String value = mockedStringPrefs.get(key); + return value != null ? value : defValue; + } + + @Nullable + @Override + public Set getStringSet(String key, @Nullable Set defValues) { + return null; + } + + @Override + public int getInt(String key, int defValue) { + Integer value = mockedIntPrefs.get(key); + return value != null ? value : defValue; + } + + @Override + public long getLong(String key, long defValue) { + return 0; + } + + @Override + public float getFloat(String key, float defValue) { + return 0; + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + Boolean value = mockedBooleanPrefs.get(key); + return value != null ? value : defValue; + } + + @Override + public boolean contains(String key) { + return mockedStringPrefs.containsKey(key) || + mockedBooleanPrefs.containsKey(key) || + mockedIntPrefs.containsKey(key); + } + + @Override + public Editor edit() { + return new Editor() { + private HashMap tempStrings = new HashMap<>(mockedStringPrefs); + private HashMap tempIntegers = new HashMap<>(mockedIntPrefs); + private HashMap tempBoolean = new HashMap<>(mockedBooleanPrefs); + + @Override + public Editor putString(String key, @Nullable String value) { + tempStrings.put(key, value); + return this; + } + + @Override + public Editor putStringSet(String key, @Nullable Set values) { + return null; + } + + @Override + public Editor putInt(String key, int value) { + tempIntegers.put(key, value); + return this; + } + + @Override + public Editor putLong(String key, long value) { + return null; + } + + @Override + public Editor putFloat(String key, float value) { + return null; + } + + @Override + public Editor putBoolean(String key, boolean value) { + tempBoolean.put(key, value); + return this; + } + + @Override + public Editor remove(String key) { + tempBoolean.remove(key); + tempStrings.remove(key); + tempIntegers.remove(key); + return this; + } + + @Override + public Editor clear() { + tempBoolean.clear(); + tempStrings.clear(); + tempIntegers.clear(); + return this; + } + + @Override + public boolean commit() { + mockedStringPrefs = new HashMap<>(tempStrings); + mockedBooleanPrefs = new HashMap<>(tempBoolean); + mockedIntPrefs = new HashMap<>(tempIntegers); + return true; + } + + @Override + public void apply() { + mockedStringPrefs = new HashMap<>(tempStrings); + mockedBooleanPrefs = new HashMap<>(tempBoolean); + mockedIntPrefs = new HashMap<>(tempIntegers); + } + }; + } + + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + + } + + @Override + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { + + } +} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java new file mode 100644 index 00000000..19d7e13f --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java @@ -0,0 +1,443 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ + +package se.leap.bitmaskclient.testutils; + +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.ResultReceiver; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.Pair; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mockito.ArgumentMatchers; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import okhttp3.OkHttpClient; +import se.leap.bitmaskclient.ConfigHelper; +import se.leap.bitmaskclient.OkHttpClientGenerator; +import se.leap.bitmaskclient.ProviderApiConnector; +import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric; +import se.leap.bitmaskclient.testutils.matchers.BundleMatcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric.TestBackendErrorCase.ERROR_NO_CONNECTION; +import static se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric.getAnswerForErrorcase; + +/** + * Created by cyberta on 08.10.17. + */ + +public class TestSetupHelper { + + + + public static String getInputAsString(InputStream fileAsInputStream) throws IOException { + BufferedReader br = new BufferedReader(new InputStreamReader(fileAsInputStream)); + StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + while (line != null) { + sb.append(line); + line = br.readLine(); + } + + return sb.toString(); + } + + @NonNull + public static Bundle mockBundle() { + final Map fakeBooleanBundle = new HashMap<>(); + final Map fakeStringBundle = new HashMap<>(); + final Map fakeIntBundle = new HashMap<>(); + final Map fakeParcelableBundle = new HashMap<>(); + + Bundle bundle = mock(Bundle.class); + + //mock String values in Bundle + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + String value = ((String) arguments[1]); + fakeStringBundle.put(key, value); + return null; + } + }).when(bundle).putString(anyString(), anyString()); + when(bundle.getString(anyString())).thenAnswer(new Answer() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + return fakeStringBundle.get(key); + } + }); + + //mock Boolean values in Bundle + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + Boolean value = ((boolean) arguments[1]); + fakeBooleanBundle.put(key, value); + return null; + } + }).when(bundle).putBoolean(anyString(), anyBoolean()); + when(bundle.getBoolean(anyString())).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + return fakeBooleanBundle.get(key); + } + }); + + //mock Integer values in Bundle + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + Integer value = ((int) arguments[1]); + fakeIntBundle.put(key, value); + return null; + } + }).when(bundle).putInt(anyString(), anyInt()); + when(bundle.getInt(anyString())).thenAnswer(new Answer() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + return fakeIntBundle.get(key); + } + }); + + //mock Parcelable values in Bundle + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + Parcelable value = ((Parcelable) arguments[1]); + fakeParcelableBundle.put(key, value); + return null; + } + }).when(bundle).putParcelable(anyString(), any(Parcelable.class)); + when(bundle.getParcelable(anyString())).thenAnswer(new Answer() { + @Override + public Parcelable answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + return fakeParcelableBundle.get(key); + } + }); + + //mock get + when(bundle.get(anyString())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + if (fakeBooleanBundle.containsKey(key)) { + return fakeBooleanBundle.get(key); + } else if (fakeIntBundle.containsKey(key)) { + return fakeIntBundle.get(key); + } else if (fakeStringBundle.containsKey(key)) { + return fakeStringBundle.get(key); + } else { + return fakeParcelableBundle.get(key); + } + } + }); + + //mock getKeySet + when(bundle.keySet()).thenAnswer(new Answer>() { + @Override + public Set answer(InvocationOnMock invocation) throws Throwable { + //this whole approach as a drawback: + //you should not add the same keys for values of different types + HashSet keys = new HashSet(); + keys.addAll(fakeBooleanBundle.keySet()); + keys.addAll(fakeIntBundle.keySet()); + keys.addAll(fakeStringBundle.keySet()); + keys.addAll(fakeParcelableBundle.keySet()); + return keys; + } + }); + + //mock containsKey + when(bundle.containsKey(anyString())).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + String key = (String) invocation.getArguments()[0]; + return fakeBooleanBundle.containsKey(key) || + fakeStringBundle.containsKey(key) || + fakeIntBundle.containsKey(key) || + fakeParcelableBundle.containsKey(key); + } + }); + + return bundle; + } + + public static Intent mockIntent() { + Intent intent = mock(Intent.class); + final String[] action = new String[1]; + final Map fakeExtras = new HashMap<>(); + final List categories = new ArrayList<>(); + + + //mock Action in intent + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + action[0] = ((String) arguments[0]); + return null; + } + }).when(intent).setAction(anyString()); + when(intent.getAction()).thenAnswer(new Answer() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + return action[0]; + } + }); + + //mock Bundle in intent extras + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + Bundle value = ((Bundle) arguments[1]); + fakeExtras.put(key, value); + return null; + } + }).when(intent).putExtra(anyString(), any(Bundle.class)); + when(intent.getBundleExtra(anyString())).thenAnswer(new Answer() { + @Override + public Bundle answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + return (Bundle) fakeExtras.get(key); + } + }); + + //mock Parcelable in intent extras + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + Parcelable value = ((Parcelable) arguments[1]); + fakeExtras.put(key, value); + return null; + } + }).when(intent).putExtra(anyString(), any(Parcelable.class)); + when(intent.getParcelableExtra(anyString())).thenAnswer(new Answer() { + @Override + public Parcelable answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + String key = ((String) arguments[0]); + return (Parcelable) fakeExtras.get(key); + } + }); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + categories.add(((String) arguments[0])); + return null; + } + }).when(intent).addCategory(anyString()); + + when(intent.getCategories()).thenAnswer(new Answer>() { + @Override + public Set answer(InvocationOnMock invocation) throws Throwable { + return new HashSet<>(categories); + } + }); + + return intent; + } + + public static void mockTextUtils() { + mockStatic(TextUtils.class); + + when(TextUtils.equals(any(CharSequence.class), any(CharSequence.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + CharSequence a = (CharSequence) invocation.getArguments()[0]; + CharSequence b = (CharSequence) invocation.getArguments()[1]; + if (a == b) return true; + int length; + if (a != null && b != null && (length = a.length()) == b.length()) { + if (a instanceof String && b instanceof String) { + return a.equals(b); + } else { + for (int i = 0; i < length; i++) { + if (a.charAt(i) != b.charAt(i)) return false; + } + return true; + } + } + return false; + } + }); + + when(TextUtils.isEmpty(any(CharSequence.class))).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + CharSequence param = (CharSequence) invocation.getArguments()[0]; + return param == null || param.length() == 0; + } + }); + } + + + public static ResultReceiver mockResultReceiver(final int expectedResultCode, final Bundle expectedBundle) { + ResultReceiver resultReceiver = mock(ResultReceiver.class); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + int resultCode = (int) arguments[0]; + Bundle bundle = (Bundle) arguments[1]; + Set keys = expectedBundle.keySet(); + Iterator iterator = keys.iterator(); + HashMap expectedIntegers = new HashMap<>(); + HashMap expectedStrings = new HashMap<>(); + HashMap expectedBooleans = new HashMap<>(); + HashMap expectedParcelables = new HashMap<>(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = expectedBundle.get(key); + + if (value instanceof Boolean) { + expectedBooleans.put(key, (Boolean) value); + } else if (value instanceof Integer) { + expectedIntegers.put(key, (Integer) value); + } else if (value instanceof String) { + expectedStrings.put(key, (String) value); + } else if (value instanceof Parcelable) { + expectedParcelables.put(key, (Parcelable) value); + } + } + assertThat("expected bundle: ", bundle, new BundleMatcher(expectedIntegers, expectedStrings, expectedBooleans, expectedParcelables)); + assertEquals("expected resultCode: ", expectedResultCode, resultCode); + return null; + } + }).when(resultReceiver).send(anyInt(), any(Bundle.class)); + return resultReceiver; + } + + public static void mockFingerprintForCertificate(String mockedFingerprint) throws CertificateEncodingException, NoSuchAlgorithmException { + mockStatic(ConfigHelper.class); + when(ConfigHelper.getFingerprintFromCertificate(any(X509Certificate.class), anyString())).thenReturn(mockedFingerprint); + when(ConfigHelper.checkErroneousDownload(anyString())).thenCallRealMethod(); + when(ConfigHelper.base64toHex(anyString())).thenCallRealMethod(); + when(ConfigHelper.parseX509CertificateFromString(anyString())).thenCallRealMethod(); + } + + public static void mockProviderApiConnector(final BackendAnswerFabric.TestBackendErrorCase errorCase) throws IOException { + mockStatic(ProviderApiConnector.class); + when(ProviderApiConnector.canConnect(any(OkHttpClient.class), anyString())).thenReturn(errorCase != ERROR_NO_CONNECTION); + when(ProviderApiConnector.requestStringFromServer(anyString(), anyString(), nullable(String.class), ArgumentMatchers.>anyList(), any(OkHttpClient.class))).thenAnswer(getAnswerForErrorcase(errorCase)); + } + + public static OkHttpClientGenerator mockClientGenerator() { + OkHttpClientGenerator mockClientGenerator = mock(OkHttpClientGenerator.class); + OkHttpClient mockedOkHttpClient = mock(OkHttpClient.class); + when(mockClientGenerator.initCommercialCAHttpClient(any(JSONObject.class))).thenReturn(mockedOkHttpClient); + when(mockClientGenerator.initSelfSignedCAHttpClient(any(JSONObject.class))).thenReturn(mockedOkHttpClient); + when(mockClientGenerator.initSelfSignedCAHttpClient(any(JSONObject.class), anyString())).thenReturn(mockedOkHttpClient); + return mockClientGenerator; + } + + public static Resources mockResources(InputStream inputStream) throws IOException, JSONException { + Resources mockedResources = mock(Resources.class, RETURNS_DEEP_STUBS); + JSONObject errorMessages = new JSONObject(getInputAsString(inputStream)); + + + when(mockedResources.getString(R.string.warning_corrupted_provider_details)). + thenReturn(errorMessages.getString("warning_corrupted_provider_details")); + when(mockedResources.getString(R.string.server_unreachable_message)). + thenReturn(errorMessages.getString("server_unreachable_message")); + when(mockedResources.getString(R.string.error_security_pinnedcertificate)). + thenReturn(errorMessages.getString("error.security.pinnedcertificate")); + when(mockedResources.getString(R.string.malformed_url)). + thenReturn(errorMessages.getString("malformed_url")); + when(mockedResources.getString(R.string.certificate_error)). + thenReturn(errorMessages.getString("certificate_error")); + when(mockedResources.getString(R.string.error_srp_math_error_user_message)). + thenReturn(errorMessages.getString("error_srp_math_error_user_message")); + when(mockedResources.getString(R.string.error_bad_user_password_user_message)). + thenReturn(errorMessages.getString("error_bad_user_password_user_message")); + when(mockedResources.getString(R.string.error_not_valid_password_user_message)). + thenReturn(errorMessages.getString("error_not_valid_password_user_message")); + when(mockedResources.getString(R.string.error_client_http_user_message)). + thenReturn(errorMessages.getString("error_client_http_user_message")); + when(mockedResources.getString(R.string.error_io_exception_user_message)). + thenReturn(errorMessages.getString("error_io_exception_user_message")); + when(mockedResources.getString(R.string.error_json_exception_user_message)). + thenReturn(errorMessages.getString("error_json_exception_user_message")); + when(mockedResources.getString(R.string.error_no_such_algorithm_exception_user_message)). + thenReturn(errorMessages.getString("error_no_such_algorithm_exception_user_message")); + when(mockedResources.getString(R.string.warning_corrupted_provider_details)). + thenReturn(errorMessages.getString("warning_corrupted_provider_details")); + when(mockedResources.getString(R.string.warning_corrupted_provider_cert)). + thenReturn(errorMessages.getString("warning_corrupted_provider_cert")); + when(mockedResources.getString(R.string.warning_expired_provider_cert)). + thenReturn(errorMessages.getString("warning_expired_provider_cert")); + return mockedResources; + } + +} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java new file mode 100644 index 00000000..00e276f4 --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ + +package se.leap.bitmaskclient.testutils.answers; + +import org.mockito.stubbing.Answer; + +/** + * Created by cyberta on 09.01.18. + */ + +public class BackendAnswerFabric { + /** + * This enum can be useful to provide different responses from a mocked ProviderApiConnector + * in order to test different error scenarios + */ + public enum TestBackendErrorCase { + NO_ERROR, + ERROR_NO_RESPONSE_BODY, // => NullPointerException + ERROR_DNS_RESOLUTION_ERROR, // => UnkownHostException + ERROR_SOCKET_TIMEOUT, // => SocketTimeoutException + ERROR_WRONG_PROTOCOL, // => MalformedURLException + ERROR_CERTIFICATE_INVALID, // => SSLHandshakeException + ERROR_WRONG_PORT, // => ConnectException + ERROR_PAYLOAD_MISSING, // => IllegalArgumentException + ERROR_TLS_1_2_NOT_SUPPORTED, // => UnknownServiceException + ERROR_UNKNOWN_IO_EXCEPTION, // => IOException + ERROR_NO_ACCESS, + ERROR_INVALID_SESSION_TOKEN, + ERROR_NO_CONNECTION, + ERROR_WRONG_SRP_CREDENTIALS + } + + public static Answer getAnswerForErrorcase(TestBackendErrorCase errorCase) { + switch (errorCase) { + case NO_ERROR: + return new NoErrorAnswer(); + case ERROR_NO_RESPONSE_BODY: + break; + case ERROR_DNS_RESOLUTION_ERROR: + break; + case ERROR_SOCKET_TIMEOUT: + break; + case ERROR_WRONG_PROTOCOL: + break; + case ERROR_CERTIFICATE_INVALID: + break; + case ERROR_WRONG_PORT: + break; + case ERROR_PAYLOAD_MISSING: + break; + case ERROR_TLS_1_2_NOT_SUPPORTED: + break; + case ERROR_UNKNOWN_IO_EXCEPTION: + break; + case ERROR_NO_ACCESS: + break; + case ERROR_INVALID_SESSION_TOKEN: + break; + case ERROR_NO_CONNECTION: + break; + case ERROR_WRONG_SRP_CREDENTIALS: + break; + } + return null; + } + +} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java new file mode 100644 index 00000000..cbf9f6b8 --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ + +package se.leap.bitmaskclient.testutils.answers; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString; + +/** + * Created by cyberta on 09.01.18. + */ + +public class NoErrorAnswer implements Answer { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + String url = (String) invocation.getArguments()[0]; + String requestMethod = (String) invocation.getArguments()[1]; + String jsonPayload = (String) invocation.getArguments()[2]; + + if (url.contains("/provider.json")) { + //download provider json + return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")); + } else if (url.contains("/ca.crt")) { + //download provider ca cert + return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem")); + } else if (url.contains("config/eip-service.json")) { + // download provider service json containing gateways, locations and openvpn settings + return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.service.json")); + } else if (url.contains("/users.json")) { + //create new user + //TODO: implement me + } else if (url.contains("/sessions.json")) { + //srp auth: sendAToSRPServer + //TODO: implement me + } else if (url.contains("/sessions/parmegvtest10.json")){ + //srp auth: sendM1ToSRPServer + //TODO: implement me + } + + return null; + } +} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java new file mode 100644 index 00000000..a7867e08 --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java @@ -0,0 +1,192 @@ +package se.leap.bitmaskclient.testutils.matchers; + +import android.os.Bundle; +import android.os.Parcelable; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; + +/** + * Created by cyberta on 09.01.18. + */ + +public class BundleMatcher extends BaseMatcher { + + private final HashMap expectedIntegers; + private final HashMap expectedStrings; + private final HashMap expectedBooleans; + private final HashMap expectedParcelables; + private HashMap unfoundExpectedInteger = new HashMap<>(); + private HashMap unfoundExpectedBoolean = new HashMap<>(); + private HashMap unfoundExpectedString = new HashMap<>(); + private HashMap unfoundExpectedParcelable = new HashMap<>(); + private HashMap unexpectedAdditionalObjects = new HashMap<>(); + + public BundleMatcher(HashMap expectedIntegers, HashMap expectedStrings, HashMap expectedBooleans, HashMap expectedParcelables) { + this.expectedBooleans = expectedBooleans; + this.expectedIntegers = expectedIntegers; + this.expectedStrings = expectedStrings; + this.expectedParcelables = expectedParcelables; + } + + @Override + public boolean matches(Object item) { + if (item instanceof Bundle) { + Bundle actualBundle = (Bundle) item; + return checkActualBundleHasAllExpectedBooleanValues(actualBundle) && + checkActualBundleHasAllExpectedStringValues(actualBundle) && + checkActualBundleHasAllExpectedIntValues(actualBundle) && + checkActualBundleHasAllExpectedParcelableValues(actualBundle) && + checkUnexpectedAdditionalValuesIn(actualBundle); + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("Bundle didn't match expectation!"); + + if (!unfoundExpectedInteger.isEmpty()) { + Iterator iterator = unfoundExpectedInteger.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (unfoundExpectedInteger.get(key) == null) { + description.appendText("\n unfound Integer in actual Bundle: ").appendValue(iterator.next()); + } else { + description.appendText("\n expected Integer for key " + key).appendValue(expectedIntegers.get(key)). + appendText("\n found Integer was: ").appendValue(unfoundExpectedInteger.get(key)); + } + } + } + if (!unfoundExpectedBoolean.isEmpty()) { + Iterator iterator = unfoundExpectedBoolean.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (unfoundExpectedBoolean.get(key) == null) { + description.appendText("\n unfound Boolean in actual Bundle: ").appendValue(iterator.next()); + } else { + description.appendText("\n expected Boolean for key " + key).appendValue(expectedBooleans.get(key)). + appendText("\n found Boolean was: ").appendValue(unfoundExpectedBoolean.get(key)); + } + } + } + if (!unfoundExpectedString.isEmpty()) { + Iterator iterator = unfoundExpectedString.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (unfoundExpectedString.get(key) == null) { + description.appendText("\n unfound String in actual Bundle: ").appendValue(iterator.next()); + } else { + description.appendText("\n expected String for key " + key).appendValue(expectedStrings.get(key)). + appendText("\n found String was: ").appendValue(unfoundExpectedString.get(key)); + } + } + } + if (!unfoundExpectedParcelable.isEmpty()) { + Iterator iterator = unfoundExpectedInteger.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (unfoundExpectedParcelable.get(key) == null) { + description.appendText("\n unfound Parcelable in actual Bundle: ").appendValue(iterator.next()); + } else { + description.appendText("\n expected Parcelable or key " + key).appendValue(expectedParcelables.get(key)). + appendText("\n found Parcelable was: ").appendValue(unfoundExpectedParcelable.get(key)); + } + } + } + + if (!unexpectedAdditionalObjects.isEmpty()) { + Iterator iterator = unexpectedAdditionalObjects.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = unexpectedAdditionalObjects.get(key); + if (value instanceof String) { + description.appendText("\n unexpected String found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value); + } else if (value instanceof Boolean) { + description.appendText("\n unexpected Boolean found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value); + } else if (value instanceof Integer) { + description.appendText("\n unexpected Integer found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value); + } else if (value instanceof Parcelable) { + description.appendText("\n unexpected Parcelable found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value); + } else { + description.appendText("\n unexpected Object found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value); + } + } + } + } + + private boolean checkActualBundleHasAllExpectedBooleanValues(Bundle actualBundle) { + Set booleanKeys = expectedBooleans.keySet(); + for (String key : booleanKeys) { + Object valueObject = actualBundle.get(key); + if (valueObject == null || + !(valueObject instanceof Boolean) || + valueObject != expectedBooleans.get(key)) { + unfoundExpectedBoolean.put(key, (Boolean) valueObject); + return false; + } + } + return true; + } + + private boolean checkActualBundleHasAllExpectedStringValues(Bundle actualBundle) { + Set stringKeys = expectedStrings.keySet(); + for (String key : stringKeys) { + Object valueObject = actualBundle.get(key); + if (valueObject == null || + !(valueObject instanceof String) || + !valueObject.equals(expectedStrings.get(key))) { + unfoundExpectedString.put(key, (String) valueObject); + return false; + } + } + return true; + } + + private boolean checkActualBundleHasAllExpectedIntValues(Bundle actualBundle) { + Set stringKeys = expectedIntegers.keySet(); + for (String key : stringKeys) { + Object valueObject = actualBundle.get(key); + if (valueObject == null || + !(valueObject instanceof Integer) || + ((Integer) valueObject).compareTo(expectedIntegers.get(key)) != 0) { + unfoundExpectedInteger.put(key, (Integer) valueObject); + return false; + } + } + return true; + } + + private boolean checkActualBundleHasAllExpectedParcelableValues(Bundle actualBundle) { + Set stringKeys = expectedParcelables.keySet(); + for (String key : stringKeys) { + Object valueObject = actualBundle.get(key); + if (valueObject == null || + !(valueObject instanceof Parcelable) || + !valueObject.equals(expectedParcelables.get(key))) { + unfoundExpectedParcelable.put(key, (Parcelable) valueObject); + return false; + } + } + return true; + } + + private boolean checkUnexpectedAdditionalValuesIn(Bundle actualBundle) { + Set keys = actualBundle.keySet(); + + for (String key : keys) { + if (!expectedStrings.containsKey(key) && + !expectedIntegers.containsKey(key) && + !expectedBooleans.containsKey(key) && + !expectedParcelables.containsKey(key) + ) { + unexpectedAdditionalObjects.put(key, actualBundle.getString(key)); + } + } + return unexpectedAdditionalObjects.isEmpty(); + } +} diff --git a/app/src/test/resources/error_messages.json b/app/src/test/resources/error_messages.json new file mode 100644 index 00000000..486a3dab --- /dev/null +++ b/app/src/test/resources/error_messages.json @@ -0,0 +1,16 @@ +{ + "server_unreachable_message": "Server is unreachable, please try again.", +"error.security.pinnedcertificate": "Security error, update the app or choose another provider.", +"malformed_url": "It doesn't seem to be a Bitmask provider.", +"certificate_error": "This is not a trusted Bitmask provider.", +"error_srp_math_error_user_message": "Try again: server math error.", +"error_bad_user_password_user_message": "Incorrect username or password.", +"error_not_valid_password_user_message": "It should have at least 8 characters.", +"error_client_http_user_message": "Try again: Client HTTP error", +"error_io_exception_user_message": "Try again: I/O error", +"error_json_exception_user_message": "Try again: Bad response from the server", +"error_no_such_algorithm_exception_user_message": "Encryption algorithm not found. Please update your OS!", +"warning_corrupted_provider_details": "Stored provider details are corrupted. You can either update Bitmask (recommended) or update the provider details using a commercial CA certificate.", +"warning_corrupted_provider_cert": "Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.", +"warning_expired_provider_cert": "Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate." +} \ No newline at end of file diff --git a/app/src/test/resources/riseup.net.json b/app/src/test/resources/riseup.net.json new file mode 100644 index 00000000..9a5ec79e --- /dev/null +++ b/app/src/test/resources/riseup.net.json @@ -0,0 +1,37 @@ +{ + "api_uri": "https://api.black.riseup.net:443", + "api_version": "1", + "ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494", + "ca_cert_uri": "https://black.riseup.net/ca.crt", + "default_language": "en", + "description": { + "en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change." + }, + "domain": "riseup.net", + "enrollment_policy": "open", + "languages": [ + "en" + ], + "name": { + "en": "Riseup Networks" + }, + "service": { + "allow_anonymous": false, + "allow_free": true, + "allow_limited_bandwidth": false, + "allow_paid": false, + "allow_registration": true, + "allow_unlimited_bandwidth": true, + "bandwidth_limit": 102400, + "default_service_level": 1, + "levels": { + "1": { + "description": "Please donate.", + "name": "free" + } + } + }, + "services": [ + "openvpn" + ] +} \ No newline at end of file diff --git a/app/src/test/resources/riseup.net.pem b/app/src/test/resources/riseup.net.pem new file mode 100644 index 00000000..c890aff4 --- /dev/null +++ b/app/src/test/resources/riseup.net.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl +dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE +AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw +NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM +Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv +b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m +TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a +7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE +LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY +iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK +5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx +HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58 +m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF +PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q +hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj +shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k +ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu +f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD +VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB +AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v +qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/ +3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ +4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7 +3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch +Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf +Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg +tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF +tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ +UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp +0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO +-----END CERTIFICATE----- \ No newline at end of file diff --git a/app/src/test/resources/riseup.service.json b/app/src/test/resources/riseup.service.json new file mode 100644 index 00000000..05f23bc1 --- /dev/null +++ b/app/src/test/resources/riseup.service.json @@ -0,0 +1,93 @@ +{ + "gateways":[ + { + "capabilities":{ + "adblock":false, + "filter_dns":false, + "limited":false, + "ports":[ + "443" + ], + "protocols":[ + "tcp" + ], + "transport":[ + "openvpn" + ], + "user_ips":false + }, + "host":"garza.riseup.net", + "ip_address":"198.252.153.28", + "location":"seattle" + }, + { + "capabilities":{ + "adblock":false, + "filter_dns":false, + "limited":false, + "ports":[ + "443" + ], + "protocols":[ + "tcp" + ], + "transport":[ + "openvpn" + ], + "user_ips":false + }, + "host":"tenca.riseup.net", + "ip_address":"5.79.86.180", + "location":"amsterdam" + }, + { + "capabilities":{ + "adblock":false, + "filter_dns":false, + "limited":false, + "ports":[ + "443" + ], + "protocols":[ + "tcp" + ], + "transport":[ + "openvpn" + ], + "user_ips":false + }, + "host":"yal.riseup.net", + "ip_address":"199.58.81.145", + "location":"montreal" + } + ], + "locations":{ + "amsterdam":{ + "country_code":"NL", + "hemisphere":"N", + "name":"Amsterdam", + "timezone":"+2" + }, + "montreal":{ + "country_code":"CA", + "hemisphere":"N", + "name":"Montreal", + "timezone":"-5" + }, + "seattle":{ + "country_code":"US", + "hemisphere":"N", + "name":"Seattle", + "timezone":"-7" + } + }, + "openvpn_configuration":{ + "auth":"SHA1", + "cipher":"AES-128-CBC", + "keepalive":"10 30", + "tls-cipher":"DHE-RSA-AES128-SHA", + "tun-ipv6":true + }, + "serial":1, + "version":1 +} \ No newline at end of file diff --git a/app/src/test/resources/secrets.json b/app/src/test/resources/secrets.json index 36ba5977..f3b68919 100644 --- a/app/src/test/resources/secrets.json +++ b/app/src/test/resources/secrets.json @@ -1 +1 @@ -{"ca_cert":"-----BEGIN CERTIFICATE-----\nMIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\nYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\nYml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\nFgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\nBAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\nggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\ndHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r\/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\nCA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\nznCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe\/RGk4\nMEqGFuOzrtsgEhPIX0hplhb0Tgz\/rtug+yTT7oJjBa3u20AAOQ38\/M99EfdeJvc4\nlPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu\/qt3DfvLfhboq+0\nbQvLUPXrVDr70onv5UDjpmEA\/cLmaIqqrduuTkFZOym65\/PfAPvpGnt7crQj\/Ibl\nDEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\nlfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\nYMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\nXjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH\/BAQDAgIE\nMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\nDQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\ncXYyB6hY5hv\/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\nk\/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\nRnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\nhtD\/JKwt6xBmNwktH0GI\/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\nEIQ0ZR56bFuJA\/CwValBqV\/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\naF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\nmlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\nG6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\nJa8zlx64jmMZPg\/t3wWqkZgXZ14qnbyG5\/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n69db12\/g4f6phldhxiWuGC\/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\nyV8e\n-----END CERTIFICATE-----\n","cert":"-----BEGIN CERTIFICATE-----\nMIIEjDCCAnSgAwIBAgIQG6MBp\/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\nDAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\nIFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\nMDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\neDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\nhL9wzo\/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV\/gO4\njcaPU+\/Wu0hMFKG28J\/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\ndbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\nrYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW\/opdlnPk5ZrP3i0qI32\/boRe0EWZGXJvr4P3K\ndJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\ngDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\nvZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\nxYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\nPfqnRw8mHfHJuE3g+4YNUMwggzwc\/VZATdV\/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\nFbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n2doqWYNqH2kq7B5R\/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A\/DhAm8n47tUH\nlBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK\/cweSRV8FwyUcn\nR0prRm3QEi9fbXqEddzjSY9y\/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\nyPoBP60TPVWMRM4WJm6nTogAz2qBrFsf\/XwT\/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\nSKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\nK2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n-----END CERTIFICATE-----","Constants.PRIVATE_KEY":"-----BEGIN RSA PRIVATE KEY-----\nMIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\nMBpyK4S\/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\nVf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\njwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\nchPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt\/YvPAATJI8\nIpFNsXcyaXBp\/M57oRemgnxp\/8UJPJmFdWX99H4hvffh\/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\nEDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa\/81ECgYEA7pLoBU\/Y\nZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\nr+r7x8TD25L7I6HJw3M351RWOAfkF0w\/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\nKSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\nyuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG\/B4Ls2T+6pl+aNJIo4e+no\nrURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji\/l9ZA3PFY135bxClVzSzUIjuO3N\nrGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK\/ZNW7g\ndQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3\/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\nAmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl\/\/PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\nispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor\/vsx9igQOlZUgzRDQsR8jo1o9\nefOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw\/8wDYg6fSosdB9utspm\nM698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n-----END RSA PRIVATE KEY-----"} +{"ca_cert":"-----BEGIN CERTIFICATE-----\nMIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\nYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\nYml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\nFgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\nBAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\nggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\ndHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r\/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\nCA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\nznCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe\/RGk4\nMEqGFuOzrtsgEhPIX0hplhb0Tgz\/rtug+yTT7oJjBa3u20AAOQ38\/M99EfdeJvc4\nlPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu\/qt3DfvLfhboq+0\nbQvLUPXrVDr70onv5UDjpmEA\/cLmaIqqrduuTkFZOym65\/PfAPvpGnt7crQj\/Ibl\nDEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\nlfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\nYMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\nXjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH\/BAQDAgIE\nMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\nDQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\ncXYyB6hY5hv\/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\nk\/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\nRnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\nhtD\/JKwt6xBmNwktH0GI\/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\nEIQ0ZR56bFuJA\/CwValBqV\/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\naF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\nmlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\nG6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\nJa8zlx64jmMZPg\/t3wWqkZgXZ14qnbyG5\/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n69db12\/g4f6phldhxiWuGC\/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\nyV8e\n-----END CERTIFICATE-----\n","cert":"-----BEGIN CERTIFICATE-----\nMIIEjDCCAnSgAwIBAgIQG6MBp\/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\nDAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\nIFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\nMDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\neDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\nhL9wzo\/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV\/gO4\njcaPU+\/Wu0hMFKG28J\/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\ndbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\nrYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW\/opdlnPk5ZrP3i0qI32\/boRe0EWZGXJvr4P3K\ndJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\ngDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\nvZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\nxYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\nPfqnRw8mHfHJuE3g+4YNUMwggzwc\/VZATdV\/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\nFbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n2doqWYNqH2kq7B5R\/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A\/DhAm8n47tUH\nlBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK\/cweSRV8FwyUcn\nR0prRm3QEi9fbXqEddzjSY9y\/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\nyPoBP60TPVWMRM4WJm6nTogAz2qBrFsf\/XwT\/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\nSKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\nK2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n-----END CERTIFICATE-----","Constants.PROVIDER_PRIVATE_KEY":"-----BEGIN RSA PRIVATE KEY-----\nMIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\nMBpyK4S\/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\nVf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\njwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\nchPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt\/YvPAATJI8\nIpFNsXcyaXBp\/M57oRemgnxp\/8UJPJmFdWX99H4hvffh\/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\nEDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa\/81ECgYEA7pLoBU\/Y\nZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\nr+r7x8TD25L7I6HJw3M351RWOAfkF0w\/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\nKSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\nyuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG\/B4Ls2T+6pl+aNJIo4e+no\nrURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji\/l9ZA3PFY135bxClVzSzUIjuO3N\nrGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK\/ZNW7g\ndQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3\/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\nAmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl\/\/PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\nispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor\/vsx9igQOlZUgzRDQsR8jo1o9\nefOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw\/8wDYg6fSosdB9utspm\nM698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n-----END RSA PRIVATE KEY-----"} -- cgit v1.2.3 From ae8341cf1f563fbcf2bdd6a4eff9525f42e9e995 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 10 Jan 2018 17:14:45 +0100 Subject: 8773 more test cases and clean-up --- app/build.gradle | 6 +- .../se/leap/bitmaskclient/ProviderApiManager.java | 11 +- .../bitmaskclient/BaseConfigurationWizard.java | 3 - .../java/se/leap/bitmaskclient/ConfigHelper.java | 25 ++-- .../leap/bitmaskclient/ProviderApiConnector.java | 18 +-- .../leap/bitmaskclient/ProviderApiManagerBase.java | 159 +++++++-------------- app/src/main/res/values/strings.xml | 2 +- .../se/leap/bitmaskclient/ProviderApiManager.java | 12 +- .../bitmaskclient/eip/ProviderApiManagerTest.java | 96 ++++++++++++- .../BackendMockResponses/BackendMockProvider.java | 86 +++++++++++ .../BackendMockResponses/BaseBackendResponse.java | 75 ++++++++++ .../NoErrorBackendResponse.java | 89 ++++++++++++ .../UpdatedCertificateBackendResponse.java | 97 +++++++++++++ .../bitmaskclient/testutils/TestSetupHelper.java | 15 +- .../testutils/answers/BackendAnswerFabric.java | 82 ----------- .../testutils/answers/NoErrorAnswer.java | 58 -------- .../testutils/matchers/BundleMatcher.java | 16 +++ app/src/test/resources/error_messages.json | 2 +- app/src/test/resources/outdated_cert.pem | 33 +++++ app/src/test/resources/updated_cert.pem | 33 +++++ 20 files changed, 611 insertions(+), 307 deletions(-) create mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java create mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BaseBackendResponse.java create mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/NoErrorBackendResponse.java create mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/UpdatedCertificateBackendResponse.java delete mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java delete mode 100644 app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java create mode 100644 app/src/test/resources/outdated_cert.pem create mode 100644 app/src/test/resources/updated_cert.pem diff --git a/app/build.gradle b/app/build.gradle index 8b956944..b894af38 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,9 +70,9 @@ dependencies { testCompile 'org.powermock:powermock-module-junit4:1.7.3' testCompile 'org.powermock:powermock-core:1.7.3' testCompile 'org.powermock:powermock-module-junit4-rule:1.7.3' - testCompile 'com.madgag.spongycastle:bctls-jdk15on:1.58.0.0' - testCompile 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' - testCompile 'com.madgag.spongycastle:bcpg-jdk15on:1.58.0.0' + //testCompile 'com.madgag.spongycastle:bctls-jdk15on:1.58.0.0' + //testCompile 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' + //testCompile 'com.madgag.spongycastle:bcpg-jdk15on:1.58.0.0' androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.6.3' testCompile 'junit:junit:4.12' diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java index 9c27fd1f..6105c4b7 100644 --- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java +++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java @@ -49,11 +49,11 @@ import se.leap.bitmaskclient.eip.EIP; import static android.text.TextUtils.isEmpty; import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING; -import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON; import static se.leap.bitmaskclient.ProviderAPI.ERRORS; import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY; import static se.leap.bitmaskclient.R.string.certificate_error; import static se.leap.bitmaskclient.R.string.malformed_url; +import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert; /** * Created by cyberta on 04.01.18. @@ -89,8 +89,7 @@ public class ProviderApiManager extends ProviderApiManagerBase { ""; if (isEmpty(lastProviderMainUrl)) { - currentDownload.putBoolean(RESULT_KEY, false); - setErrorResult(currentDownload, resources.getString(R.string.malformed_url), null); + setErrorResult(currentDownload, malformed_url, null); return currentDownload; } @@ -260,12 +259,10 @@ public class ProviderApiManager extends ProviderApiManagerBase { preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, certString).commit(); result.putBoolean(RESULT_KEY, true); } else { - setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); - result.putBoolean(RESULT_KEY, false); + setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString()); } } catch (JSONException e) { - setErrorResult(result, resources.getString(malformed_url), null); - result.putBoolean(RESULT_KEY, false); + setErrorResult(result, malformed_url, null); } return result; diff --git a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java index c26184bb..63453ac3 100644 --- a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java +++ b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java @@ -409,8 +409,6 @@ public abstract class BaseConfigurationWizard extends Activity } - - /** * Once selected a provider, this fragment offers the user to log in, * use it anonymously (if possible) @@ -430,7 +428,6 @@ public abstract class BaseConfigurationWizard extends Activity } } - @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.configuration_wizard_activity, menu); diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java index 54bcc1f4..0e861059 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java +++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java @@ -47,7 +47,7 @@ import java.security.spec.PKCS8EncodedKeySpec; import static android.R.attr.name; /** - * Stores constants, and implements auxiliary methods used across all LEAP Android classes. + * Stores constants, and implements auxiliary methods used across all Bitmask Android classes. * * @author parmegv * @author MeanderingCode @@ -172,25 +172,30 @@ public class ConfigHelper { return key; } - public static String base64toHex(String base64_input) { - byte[] byteArray = Base64.decode(base64_input); - int readBytes = byteArray.length; + private static String byteArrayToHex(byte[] input) { + int readBytes = input.length; StringBuffer hexData = new StringBuffer(); int onebyte; for (int i = 0; i < readBytes; i++) { - onebyte = ((0x000000ff & byteArray[i]) | 0xffffff00); + onebyte = ((0x000000ff & input[i]) | 0xffffff00); hexData.append(Integer.toHexString(onebyte).substring(6)); } return hexData.toString(); } + /** + * Calculates the hexadecimal representation of a sha256/sha1 fingerprint of a certificate + * + * @param certificate + * @param encoding + * @return + * @throws NoSuchAlgorithmException + * @throws CertificateEncodingException + */ @NonNull public static String getFingerprintFromCertificate(X509Certificate certificate, String encoding) throws NoSuchAlgorithmException, CertificateEncodingException /*, UnsupportedEncodingException*/ { - return base64toHex( - //new String(Base64.encode(MessageDigest.getInstance(encoding).digest(certificate.getEncoded())), "US-ASCII")); - android.util.Base64.encodeToString( - MessageDigest.getInstance(encoding).digest(certificate.getEncoded()), - android.util.Base64.DEFAULT)); + byte[] byteArray = MessageDigest.getInstance(encoding).digest(certificate.getEncoded()); + return byteArrayToHex(byteArray); } /** diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java index 9aad14d5..439fb5e2 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java @@ -60,18 +60,14 @@ public class ProviderApiConnector { return false; } - public static boolean canConnect(@NonNull OkHttpClient okHttpClient, String url) { - try { - Request.Builder requestBuilder = new Request.Builder() - .url(url) - .method("GET", null); - Request request = requestBuilder.build(); + public static boolean canConnect(@NonNull OkHttpClient okHttpClient, String url) throws RuntimeException, IOException { + Request.Builder requestBuilder = new Request.Builder() + .url(url) + .method("GET", null); + Request request = requestBuilder.build(); - Response response = okHttpClient.newCall(request).execute(); - return response.isSuccessful(); - } catch (RuntimeException | IOException e) { - return false; - } + Response response = okHttpClient.newCall(request).execute(); + return response.isSuccessful(); } diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java index 396d642b..cc005fcd 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java @@ -30,7 +30,6 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; -import java.io.InputStream; import java.math.BigInteger; import java.net.ConnectException; import java.net.MalformedURLException; @@ -46,16 +45,10 @@ import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.util.ArrayList; import java.util.List; -import java.util.Locale; -import java.util.Scanner; import javax.net.ssl.SSLHandshakeException; -import okhttp3.MediaType; import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import se.leap.bitmaskclient.userstatus.SessionDialog; import se.leap.bitmaskclient.userstatus.User; import se.leap.bitmaskclient.userstatus.UserStatus; @@ -96,9 +89,11 @@ import static se.leap.bitmaskclient.R.string.error_io_exception_user_message; import static se.leap.bitmaskclient.R.string.error_json_exception_user_message; import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message; import static se.leap.bitmaskclient.R.string.malformed_url; -import static se.leap.bitmaskclient.R.string.retry; import static se.leap.bitmaskclient.R.string.server_unreachable_message; import static se.leap.bitmaskclient.R.string.service_is_down_error; +import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert; +import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details; +import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert; /** * Implements the logic of the http api calls. The methods of this class needs to be called from @@ -136,10 +131,6 @@ public abstract class ProviderApiManagerBase { return lastProviderMainUrl; } - - private final MediaType JSON - = MediaType.parse("application/json; charset=utf-8"); - public ProviderApiManagerBase(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) { this.preferences = preferences; this.resources = resources; @@ -390,7 +381,7 @@ public abstract class ProviderApiManagerBase { private boolean setTokenIfAvailable(JSONObject authentication_step_result) { try { LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); - } catch (JSONException e) { // + } catch (JSONException e) { return false; } return true; @@ -540,33 +531,12 @@ public abstract class ProviderApiManagerBase { } private String requestStringFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { - //Response response; String plainResponseBody = null; - /*RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null; - Request.Builder requestBuilder = new Request.Builder() - .url(url) - .method(request_method, jsonBody); - if (headerArgs != null) { - for (Pair keyValPair : headerArgs) { - requestBuilder.addHeader(keyValPair.first, keyValPair.second); - } - } - //TODO: move to getHeaderArgs()? - String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); - requestBuilder.addHeader("Accept-Language", locale); - Request request = requestBuilder.build(); -*/ try { - //response = okHttpClient.newCall(request).execute(); - //response = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); - //InputStream inputStream = response.body().byteStream(); - //Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); - //if (scanner.hasNext()) { - // plainResponseBody = scanner.next(); - //} plainResponseBody = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); + } catch (NullPointerException npe) { plainResponseBody = formatErrorMessage(error_json_exception_user_message); } catch (UnknownHostException | SocketTimeoutException e) { @@ -589,6 +559,39 @@ public abstract class ProviderApiManagerBase { return plainResponseBody; } + private boolean canConnect(String caCert, JSONObject providerDefinition, Bundle result) { + JSONObject errorJson = new JSONObject(); + String baseUrl = getApiUrl(providerDefinition); + + OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert); + if (okHttpClient == null) { + result.putString(ERRORS, errorJson.toString()); + return false; + } + + try { + + return ProviderApiConnector.canConnect(okHttpClient, baseUrl); + + } catch (UnknownHostException | SocketTimeoutException e) { + setErrorResult(result, server_unreachable_message, null); + } catch (MalformedURLException e) { + setErrorResult(result, malformed_url, null); + } catch (SSLHandshakeException e) { + setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString()); + } catch (ConnectException e) { + setErrorResult(result, service_is_down_error, null); + } catch (IllegalArgumentException e) { + setErrorResult(result, error_no_such_algorithm_exception_user_message, null); + } catch (UnknownServiceException e) { + //unable to find acceptable protocols - tlsv1.2 not enabled? + setErrorResult(result, error_no_such_algorithm_exception_user_message, null); + } catch (IOException e) { + setErrorResult(result, error_io_exception_user_message, null); + } + return false; + } + /** * Downloads a provider.json from a given URL, adding a new provider using the given name. * @@ -642,7 +645,7 @@ public abstract class ProviderApiManagerBase { result = real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim()); } else result = false; - } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException /*| UnsupportedEncodingException*/ e) { + } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) { result = false; } } @@ -650,10 +653,7 @@ public abstract class ProviderApiManagerBase { return result; } - - protected void checkPersistedProviderUpdates() { - //String providerDomain = getProviderDomain(providerDefinition); String providerDomain = getDomainFromMainURL(lastProviderMainUrl); if (hasUpdatedProviderDetails(providerDomain)) { providerCaCert = getPersistedProviderCA(providerDomain); @@ -682,8 +682,7 @@ public abstract class ProviderApiManagerBase { result.putBoolean(RESULT_KEY, true); } catch (JSONException e) { e.printStackTrace(); - result.putBoolean(RESULT_KEY, false); - result = setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString()); } return result; @@ -699,7 +698,7 @@ public abstract class ProviderApiManagerBase { X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string); if (certificate == null) { - return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_INVALID_CERTIFICATE.toString()); + return setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString()); } try { certificate.checkValidity(); @@ -708,39 +707,38 @@ public abstract class ProviderApiManagerBase { String expected_fingerprint = fingerprint.split(":")[1]; String real_fingerprint = getFingerprintFromCertificate(certificate, encoding); if (!real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim())) { - return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); + return setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString()); } if (!hasApiUrlExpectedDomain(providerDefinition, mainUrl)){ - return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString()); } if (!canConnect(cert_string, providerDefinition, result)) { return result; } } catch (NoSuchAlgorithmException e ) { - return setErrorResult(result, resources.getString(error_no_such_algorithm_exception_user_message), null); + return setErrorResult(result, error_no_such_algorithm_exception_user_message, null); } catch (ArrayIndexOutOfBoundsException e) { - return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString()); + return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString()); } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) { - return setErrorResult(result, resources.getString(R.string.warning_expired_provider_cert), ERROR_INVALID_CERTIFICATE.toString()); - } /*catch (UnsupportedEncodingException e) { - return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); - }*/ + return setErrorResult(result, warning_expired_provider_cert, ERROR_INVALID_CERTIFICATE.toString()); + } result.putBoolean(RESULT_KEY, true); return result; } - protected Bundle setErrorResult(Bundle result, String errorMessage, String errorId) { + protected Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) { JSONObject errorJson = new JSONObject(); if (errorId != null) { - addErrorMessageToJson(errorJson, errorMessage, errorId); + addErrorMessageToJson(errorJson, resources.getString(errorMessageId), errorId); } else { - addErrorMessageToJson(errorJson, errorMessage); + addErrorMessageToJson(errorJson, resources.getString(errorMessageId)); } result.putString(ERRORS, errorJson.toString()); + result.putBoolean(RESULT_KEY, false); return result; } @@ -763,41 +761,6 @@ public abstract class ProviderApiManagerBase { return false; } - private boolean canConnect(String caCert, JSONObject providerDefinition, Bundle result) { - JSONObject errorJson = new JSONObject(); - String baseUrl = getApiUrl(providerDefinition); - - OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert); - if (okHttpClient == null) { - result.putString(ERRORS, errorJson.toString()); - return false; - } - - //try { - - return ProviderApiConnector.canConnect(okHttpClient, baseUrl); - /*} catch (RuntimeException | IOException e) { - e.printStackTrace(); - }*/ - - //return false; - - - /*List> headerArgs = getAuthorizationHeader(); - String plain_response = requestStringFromServer(baseUrl, "GET", null, headerArgs, okHttpClient); - - try { - if (new JSONObject(plain_response).has(ERRORS)) { - result.putString(ERRORS, plain_response); - return false; - } - } catch (JSONException e) { - //eat me - } - - return true;*/ - } - protected String getCaCertFingerprint(JSONObject providerDefinition) { try { return providerDefinition.getString(Provider.CA_CERT_FINGERPRINT); @@ -921,26 +884,6 @@ public abstract class ProviderApiManagerBase { String deleteUrl = providerApiUrl + "/logout"; int progress = 0; - /* Request.Builder requestBuilder = new Request.Builder() - .url(deleteUrl) - .delete(); - Request request = requestBuilder.build();*/ - - //try { - - //Response response = okHttpClient.newCall(request).execute(); -// Response response = ProviderApiConnector.delete(okHttpClient, deleteUrl); -// // v---- was already not authorized -// if (response.isSuccessful() || response.code() == 401) { -// broadcastProgress(progress++); -// LeapSRPSession.setToken(""); -// } -// -// } catch (IOException | RuntimeException e) { -// return false; -// } -// return true; - if (ProviderApiConnector.delete(okHttpClient, deleteUrl)) { broadcastProgress(progress++); LeapSRPSession.setToken(""); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 680f92e1..f59acd3d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,6 +85,6 @@ Update provider details Update certificate Stored provider details are corrupted. You can either update Bitmask (recommended) or update the provider details using a commercial CA certificate. - Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate. + Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate. Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate. diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java index 31c63d01..b20a7759 100644 --- a/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java +++ b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java @@ -37,6 +37,7 @@ import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_C import static se.leap.bitmaskclient.ProviderAPI.ERRORS; import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY; import static se.leap.bitmaskclient.R.string.malformed_url; +import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert; /** * Implements the logic of the provider api http requests. The methods of this class need to be called from @@ -76,7 +77,7 @@ public class ProviderApiManager extends ProviderApiManagerBase { if (isEmpty(lastProviderMainUrl)) { currentDownload.putBoolean(RESULT_KEY, false); - setErrorResult(currentDownload, resources.getString(R.string.malformed_url), null); + setErrorResult(currentDownload, malformed_url, null); return currentDownload; } @@ -150,8 +151,7 @@ public class ProviderApiManager extends ProviderApiManagerBase { } if (!isValidJson(providerDotJsonString)) { - result.putString(ERRORS, resources.getString(malformed_url)); - result.putBoolean(RESULT_KEY, false); + setErrorResult(result, malformed_url, null); return result; } @@ -247,12 +247,10 @@ public class ProviderApiManager extends ProviderApiManagerBase { preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, cert_string).commit(); result.putBoolean(RESULT_KEY, true); } else { - setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString()); - result.putBoolean(RESULT_KEY, false); + setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString()); } } catch (JSONException e) { - setErrorResult(result, resources.getString(malformed_url), null); - result.putBoolean(RESULT_KEY, false); + setErrorResult(result, malformed_url, null); } return result; diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java index c39681c4..9ca90b17 100644 --- a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java +++ b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java @@ -49,6 +49,8 @@ import static se.leap.bitmaskclient.ProviderAPI.ERRORS; import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK; import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_OK; import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY; +import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_CASE_UPDATED_CERTIFICATE; +import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.NO_ERROR; import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString; import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockBundle; import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockClientGenerator; @@ -58,7 +60,7 @@ import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockProviderApiCon import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockResources; import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockResultReceiver; import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockTextUtils; -import static se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric.TestBackendErrorCase.NO_ERROR; + /** * Created by cyberta on 04.01.18. @@ -188,7 +190,7 @@ public class ProviderApiManagerTest { providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); Bundle expectedResult = mockBundle(); expectedResult.putBoolean(RESULT_KEY, false); - expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); Intent provider_API_command = mockIntent(); Bundle parameters = mockBundle(); @@ -210,7 +212,7 @@ public class ProviderApiManagerTest { providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); Bundle expectedResult = mockBundle(); expectedResult.putBoolean(RESULT_KEY, false); - expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); Intent provider_API_command = mockIntent(); Bundle parameters = mockBundle(); @@ -232,11 +234,33 @@ public class ProviderApiManagerTest { providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); Bundle expectedResult = mockBundle(); expectedResult.putBoolean(RESULT_KEY, false); - expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + + @Test + public void test_handleIntentSetupProvider_preseededProviderAndCA_outdatedCertificate() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockProviderApiConnector(NO_ERROR); + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, false); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); Intent provider_API_command = mockIntent(); Bundle parameters = mockBundle(); parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("outdated_cert.pem"))); + parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))); provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); @@ -245,5 +269,69 @@ public class ProviderApiManagerTest { providerApiManager.handleIntent(provider_API_command); } + @Test + public void test_handleIntentSetupProvider_storedProviderAndCAFromPreviousSetup_outdatedCertificate() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockProviderApiConnector(NO_ERROR); + mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply(); + mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("outdated_cert.pem"))).apply(); + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, false); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + @Test + public void test_handleIntentSetupProvider_preseededProviderAndCA_ValidCertificateButUpdatedCertificateOnServerSide() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494"); + mockProviderApiConnector(ERROR_CASE_UPDATED_CERTIFICATE); + + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, false); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))); + parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } + + @Test + public void test_handleIntentSetupProvider_storedProviderAndCAFromPreviousSetup_ValidCertificateButUpdatedCertificateOnServerSide() throws IOException, CertificateEncodingException, NoSuchAlgorithmException { + mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494"); + mockProviderApiConnector(ERROR_CASE_UPDATED_CERTIFICATE); + mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply(); + mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))).apply(); + providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback()); + Bundle expectedResult = mockBundle(); + expectedResult.putBoolean(RESULT_KEY, false); + expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}"); + + Intent provider_API_command = mockIntent(); + Bundle parameters = mockBundle(); + parameters.putString(Provider.MAIN_URL, "https://riseup.net"); + + provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER); + provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters); + provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult)); + + providerApiManager.handleIntent(provider_API_command); + } } diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java new file mode 100644 index 00000000..9069661f --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ +package se.leap.bitmaskclient.testutils.BackendMockResponses; + +import java.io.IOException; + +/** + * Created by cyberta on 10.01.18. + */ + +public class BackendMockProvider { + /** + * This enum can be useful to provide different responses from a mocked ProviderApiConnector + * in order to test different error scenarios + */ + public enum TestBackendErrorCase { + NO_ERROR, + ERROR_CASE_UPDATED_CERTIFICATE, + ERROR_NO_RESPONSE_BODY, // => NullPointerException + ERROR_DNS_RESOLUTION_ERROR, // => UnkownHostException + ERROR_SOCKET_TIMEOUT, // => SocketTimeoutException + ERROR_WRONG_PROTOCOL, // => MalformedURLException + ERROR_CERTIFICATE_INVALID, // => SSLHandshakeException + ERROR_WRONG_PORT, // => ConnectException + ERROR_PAYLOAD_MISSING, // => IllegalArgumentException + ERROR_TLS_1_2_NOT_SUPPORTED, // => UnknownServiceException + ERROR_UNKNOWN_IO_EXCEPTION, // => IOException + ERROR_NO_ACCESS, + ERROR_INVALID_SESSION_TOKEN, + ERROR_NO_CONNECTION, + ERROR_WRONG_SRP_CREDENTIALS + } + + + public static void provideBackendResponsesFor(TestBackendErrorCase errorCase) throws IOException { + switch (errorCase) { + + case NO_ERROR: + new NoErrorBackendResponse(); + break; + case ERROR_CASE_UPDATED_CERTIFICATE: + new UpdatedCertificateBackendResponse(); + break; + case ERROR_NO_RESPONSE_BODY: + break; + case ERROR_DNS_RESOLUTION_ERROR: + break; + case ERROR_SOCKET_TIMEOUT: + break; + case ERROR_WRONG_PROTOCOL: + break; + case ERROR_CERTIFICATE_INVALID: + break; + case ERROR_WRONG_PORT: + break; + case ERROR_PAYLOAD_MISSING: + break; + case ERROR_TLS_1_2_NOT_SUPPORTED: + break; + case ERROR_UNKNOWN_IO_EXCEPTION: + break; + case ERROR_NO_ACCESS: + break; + case ERROR_INVALID_SESSION_TOKEN: + break; + case ERROR_NO_CONNECTION: + break; + case ERROR_WRONG_SRP_CREDENTIALS: + break; + } + } +} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BaseBackendResponse.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BaseBackendResponse.java new file mode 100644 index 00000000..98224019 --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BaseBackendResponse.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ +package se.leap.bitmaskclient.testutils.BackendMockResponses; + +import android.util.Pair; + +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import java.io.IOException; + +import okhttp3.OkHttpClient; +import se.leap.bitmaskclient.ProviderApiConnector; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +/** + * Created by cyberta on 10.01.18. + */ + +public abstract class BaseBackendResponse { + + private Answer answerRequestStringFromServer; + private Answer answerCanConnect; + private Answer answerDelete; + + public BaseBackendResponse() throws IOException { + mockStatic(ProviderApiConnector.class); + this.answerRequestStringFromServer = getAnswerForRequestStringFromServer(); + this.answerCanConnect = getAnswerForCanConnect(); + this.answerDelete = getAnswerForDelete(); + + responseOnRequestStringFromServer(); + responseOnCanConnect(); + responseOnDelete(); + + } + + public abstract Answer getAnswerForRequestStringFromServer(); + public abstract Answer getAnswerForCanConnect(); + public abstract Answer getAnswerForDelete(); + + + public void responseOnRequestStringFromServer() throws IOException, RuntimeException { + Mockito.when(ProviderApiConnector.requestStringFromServer(anyString(), anyString(), nullable(String.class), ArgumentMatchers.>anyList(), any(OkHttpClient.class))). + thenAnswer(answerRequestStringFromServer); + } + + public void responseOnCanConnect() throws IOException, RuntimeException { + Mockito.when(ProviderApiConnector.canConnect(any(OkHttpClient.class), anyString())).thenAnswer(answerCanConnect); + } + + public void responseOnDelete() throws IOException, RuntimeException { + Mockito.when(ProviderApiConnector.delete(any(OkHttpClient.class), anyString())).thenAnswer(answerDelete); + } + +} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/NoErrorBackendResponse.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/NoErrorBackendResponse.java new file mode 100644 index 00000000..fa318e42 --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/NoErrorBackendResponse.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ +package se.leap.bitmaskclient.testutils.BackendMockResponses; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; + +import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString; + +/** + * Created by cyberta on 10.01.18. + */ + +public class NoErrorBackendResponse extends BaseBackendResponse { + public NoErrorBackendResponse() throws IOException { + super(); + } + + @Override + public Answer getAnswerForRequestStringFromServer() { + return new Answer() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + String url = (String) invocation.getArguments()[0]; + String requestMethod = (String) invocation.getArguments()[1]; + String jsonPayload = (String) invocation.getArguments()[2]; + + if (url.contains("/provider.json")) { + //download provider json + return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")); + } else if (url.contains("/ca.crt")) { + //download provider ca cert + return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem")); + } else if (url.contains("config/eip-service.json")) { + // download provider service json containing gateways, locations and openvpn settings + return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.service.json")); + } else if (url.contains("/users.json")) { + //create new user + //TODO: implement me + } else if (url.contains("/sessions.json")) { + //srp auth: sendAToSRPServer + //TODO: implement me + } else if (url.contains("/sessions/parmegvtest10.json")){ + //srp auth: sendM1ToSRPServer + //TODO: implement me + } + + return null; + } + }; + } + + @Override + public Answer getAnswerForCanConnect() { + return new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + return true; + } + }; + } + + @Override + public Answer getAnswerForDelete() { + return new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + return true; + } + }; + } + +} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/UpdatedCertificateBackendResponse.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/UpdatedCertificateBackendResponse.java new file mode 100644 index 00000000..232649a1 --- /dev/null +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/UpdatedCertificateBackendResponse.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ +package se.leap.bitmaskclient.testutils.BackendMockResponses; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; + +import javax.net.ssl.SSLHandshakeException; + +import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString; + +/** + * Created by cyberta on 10.01.18. + */ + +public class UpdatedCertificateBackendResponse extends BaseBackendResponse { + static volatile boolean wasCACertCalled = false; + + + public UpdatedCertificateBackendResponse() throws IOException { + super(); + } + + @Override + public Answer getAnswerForRequestStringFromServer() { + return new Answer() { + + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + String url = (String) invocation.getArguments()[0]; + + if (url.contains("/provider.json")) { + if (!wasCACertCalled) { + throw new SSLHandshakeException("Updated certificate on server side"); + } + //download provider json + return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")); + } else if (url.contains("/ca.crt")) { + //download provider ca cert + wasCACertCalled = true; + return getInputAsString(getClass().getClassLoader().getResourceAsStream("updated_cert.pem")); + } else if (url.contains("config/eip-service.json")) { + // download provider service json containing gateways, locations and openvpn settings + if (!wasCACertCalled) { + throw new SSLHandshakeException("Updated certificate on server side"); + } + return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.service.json")); + } + + return null; + } + }; + } + + @Override + public Answer getAnswerForCanConnect() { + return new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + if (!wasCACertCalled) { + throw new SSLHandshakeException("Updated certificate on server side"); + } + return true; + } + }; + } + + @Override + public Answer getAnswerForDelete() { + return new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + if (!wasCACertCalled) { + throw new SSLHandshakeException("Updated certificate on server side"); + } + return true; + } + }; + } + +} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java index 19d7e13f..f8f70eaf 100644 --- a/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java @@ -24,11 +24,9 @@ import android.os.Parcelable; import android.os.ResultReceiver; import android.support.annotation.NonNull; import android.text.TextUtils; -import android.util.Pair; import org.json.JSONException; import org.json.JSONObject; -import org.mockito.ArgumentMatchers; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; @@ -50,9 +48,8 @@ import java.util.Set; import okhttp3.OkHttpClient; import se.leap.bitmaskclient.ConfigHelper; import se.leap.bitmaskclient.OkHttpClientGenerator; -import se.leap.bitmaskclient.ProviderApiConnector; import se.leap.bitmaskclient.R; -import se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric; +import se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider; import se.leap.bitmaskclient.testutils.matchers.BundleMatcher; import static org.junit.Assert.assertEquals; @@ -61,14 +58,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mockStatic; -import static se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric.TestBackendErrorCase.ERROR_NO_CONNECTION; -import static se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric.getAnswerForErrorcase; /** * Created by cyberta on 08.10.17. @@ -383,14 +377,11 @@ public class TestSetupHelper { mockStatic(ConfigHelper.class); when(ConfigHelper.getFingerprintFromCertificate(any(X509Certificate.class), anyString())).thenReturn(mockedFingerprint); when(ConfigHelper.checkErroneousDownload(anyString())).thenCallRealMethod(); - when(ConfigHelper.base64toHex(anyString())).thenCallRealMethod(); when(ConfigHelper.parseX509CertificateFromString(anyString())).thenCallRealMethod(); } - public static void mockProviderApiConnector(final BackendAnswerFabric.TestBackendErrorCase errorCase) throws IOException { - mockStatic(ProviderApiConnector.class); - when(ProviderApiConnector.canConnect(any(OkHttpClient.class), anyString())).thenReturn(errorCase != ERROR_NO_CONNECTION); - when(ProviderApiConnector.requestStringFromServer(anyString(), anyString(), nullable(String.class), ArgumentMatchers.>anyList(), any(OkHttpClient.class))).thenAnswer(getAnswerForErrorcase(errorCase)); + public static void mockProviderApiConnector(final BackendMockProvider.TestBackendErrorCase errorCase) throws IOException { + BackendMockProvider.provideBackendResponsesFor(errorCase); } public static OkHttpClientGenerator mockClientGenerator() { diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java deleted file mode 100644 index 00e276f4..00000000 --- a/app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) 2018 LEAP Encryption Access Project and contributers - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 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 . - */ - -package se.leap.bitmaskclient.testutils.answers; - -import org.mockito.stubbing.Answer; - -/** - * Created by cyberta on 09.01.18. - */ - -public class BackendAnswerFabric { - /** - * This enum can be useful to provide different responses from a mocked ProviderApiConnector - * in order to test different error scenarios - */ - public enum TestBackendErrorCase { - NO_ERROR, - ERROR_NO_RESPONSE_BODY, // => NullPointerException - ERROR_DNS_RESOLUTION_ERROR, // => UnkownHostException - ERROR_SOCKET_TIMEOUT, // => SocketTimeoutException - ERROR_WRONG_PROTOCOL, // => MalformedURLException - ERROR_CERTIFICATE_INVALID, // => SSLHandshakeException - ERROR_WRONG_PORT, // => ConnectException - ERROR_PAYLOAD_MISSING, // => IllegalArgumentException - ERROR_TLS_1_2_NOT_SUPPORTED, // => UnknownServiceException - ERROR_UNKNOWN_IO_EXCEPTION, // => IOException - ERROR_NO_ACCESS, - ERROR_INVALID_SESSION_TOKEN, - ERROR_NO_CONNECTION, - ERROR_WRONG_SRP_CREDENTIALS - } - - public static Answer getAnswerForErrorcase(TestBackendErrorCase errorCase) { - switch (errorCase) { - case NO_ERROR: - return new NoErrorAnswer(); - case ERROR_NO_RESPONSE_BODY: - break; - case ERROR_DNS_RESOLUTION_ERROR: - break; - case ERROR_SOCKET_TIMEOUT: - break; - case ERROR_WRONG_PROTOCOL: - break; - case ERROR_CERTIFICATE_INVALID: - break; - case ERROR_WRONG_PORT: - break; - case ERROR_PAYLOAD_MISSING: - break; - case ERROR_TLS_1_2_NOT_SUPPORTED: - break; - case ERROR_UNKNOWN_IO_EXCEPTION: - break; - case ERROR_NO_ACCESS: - break; - case ERROR_INVALID_SESSION_TOKEN: - break; - case ERROR_NO_CONNECTION: - break; - case ERROR_WRONG_SRP_CREDENTIALS: - break; - } - return null; - } - -} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java deleted file mode 100644 index cbf9f6b8..00000000 --- a/app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2018 LEAP Encryption Access Project and contributers - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 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 . - */ - -package se.leap.bitmaskclient.testutils.answers; - -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; - -import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString; - -/** - * Created by cyberta on 09.01.18. - */ - -public class NoErrorAnswer implements Answer { - @Override - public String answer(InvocationOnMock invocation) throws Throwable { - String url = (String) invocation.getArguments()[0]; - String requestMethod = (String) invocation.getArguments()[1]; - String jsonPayload = (String) invocation.getArguments()[2]; - - if (url.contains("/provider.json")) { - //download provider json - return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")); - } else if (url.contains("/ca.crt")) { - //download provider ca cert - return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem")); - } else if (url.contains("config/eip-service.json")) { - // download provider service json containing gateways, locations and openvpn settings - return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.service.json")); - } else if (url.contains("/users.json")) { - //create new user - //TODO: implement me - } else if (url.contains("/sessions.json")) { - //srp auth: sendAToSRPServer - //TODO: implement me - } else if (url.contains("/sessions/parmegvtest10.json")){ - //srp auth: sendM1ToSRPServer - //TODO: implement me - } - - return null; - } -} diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java index a7867e08..d2d2a102 100644 --- a/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java +++ b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java @@ -1,3 +1,19 @@ +/** + * Copyright (c) 2018 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 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 . + */ package se.leap.bitmaskclient.testutils.matchers; import android.os.Bundle; diff --git a/app/src/test/resources/error_messages.json b/app/src/test/resources/error_messages.json index 486a3dab..4d72b074 100644 --- a/app/src/test/resources/error_messages.json +++ b/app/src/test/resources/error_messages.json @@ -11,6 +11,6 @@ "error_json_exception_user_message": "Try again: Bad response from the server", "error_no_such_algorithm_exception_user_message": "Encryption algorithm not found. Please update your OS!", "warning_corrupted_provider_details": "Stored provider details are corrupted. You can either update Bitmask (recommended) or update the provider details using a commercial CA certificate.", -"warning_corrupted_provider_cert": "Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.", +"warning_corrupted_provider_cert": "Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.", "warning_expired_provider_cert": "Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate." } \ No newline at end of file diff --git a/app/src/test/resources/outdated_cert.pem b/app/src/test/resources/outdated_cert.pem new file mode 100644 index 00000000..269efe5f --- /dev/null +++ b/app/src/test/resources/outdated_cert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFyTCCA7GgAwIBAgIJANNtrHEcx/tBMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJOWTEWMBQGA1UEBwwNTmV3IFlvcmsgQ2l0eTENMAsG +A1UECgwETEVBUDEVMBMGA1UECwwMVGVzdGluZyBEZXAuMSEwHwYJKoZIhvcNAQkB +FhJkb25vdHJlcGx5QGxlYXAuc2UwHhcNMTgwMTA5MjMxNjA2WhcNMTgwMTA4MjMx +NjA2WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxFjAUBgNVBAcMDU5ldyBZ +b3JrIENpdHkxDTALBgNVBAoMBExFQVAxFTATBgNVBAsMDFRlc3RpbmcgRGVwLjEh +MB8GCSqGSIb3DQEJARYSZG9ub3RyZXBseUBsZWFwLnNlMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA2RE9GgRSt9re96fKpBjZ2sKv+YC+efULvp1+3/RB +2SQP1vcSsNWDtiPq+KpyMlmfou6Xuvuz8c5YEbWyjFHC/IimMu8GG2XAqfs1Zmrj +IKFX/7Zwprf5OYpfe5BaDV2bKXS+/nHk1GPeZNWWlKZfI/f2mE3p4phwCPjKxFmo +A4WDq5u1rxQ+iskTi3PEKiO5S7lE7/MuPuWuYDLLyia2VkZddS7/OhxhhtBI8U7k +VUjY8VeyHqa1w9wxzZOovUXFmrsBbzg0D0BXrafZv6heVZZZFC9DRp3OXBJLbZOM +gYhK7WIZTRfzl1km+U3Iw+ZUr/bXYy0HRRXq5h3mcXhHnVSBu9uUJYgTaSzWNCpL +VzbEjYmFlsVsEcFmOTBwKEDmlVwwPuzhreFDkxXHX28xrw6laoClcngYG93pFWw7 +e1NrUuqTw4eR4uM1ZEF7gBwYFKu8B/51Z5wGaYKsbdjOYcEGNqSNBa/emlVByUkG +kZ9RIiorUoiagCvQBCZCTgwTQ5RQFRx+I5eMjTq+bcbMkdl6/MFfZJg1c+ZVHNpW +2Q+asu6JZG9MGa0QtJjLSQCE5XFxrG/pQ/2x4PEbS131WUl/BIYLUVsB8ElFerGY +4D8aAo/z5kPlwtX05lZ9AfchD+iunjEFMaJNib2QevBzk6xOGacuLkB0yu4ep9gU +qFkCAwEAAaNQME4wHQYDVR0OBBYEFMTuQ+qLzmPNXVMCW2a68cLfKYwqMB8GA1Ud +IwQYMBaAFMTuQ+qLzmPNXVMCW2a68cLfKYwqMAwGA1UdEwQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAEfqSITTr7H86ZCrOt2AtDEucN58qyKd9RLEBdxkaPB1yUyu +iHDiRoE0gvMVu6HUXGLsj3kaAjWdsS+wSfXBIXFZiKeeW6qnBwRNk8dA0vYrS4Ag +YRS3BgCADlL+NKBdwoKYCNvgIV68XwjcvfEMEkoULr1WUlikX5MH33XKNtjVIid0 +wqswY2wZrRrwssNUz7tXBuBczj7FNJyboDruYyTDloIXoqiqRw1eexA183HeMIad +leQwVBBdp1drhZdBUwI3r+MtZuyJTkf0+AFAqjKTptu5XvExYj9wvHYsohW5PV8L +RgzQ6oXCd6s7grvVQEkOXiMq5m3bS4Y1wHd1ispoMUSeYYHhMcNJuanjpulEuWmu +k4aEuGsgTAtDXNO7nBRw82cbJySoOWT0uKjzU0nT80VkuM45eFD71J1rZIoqJRoP +jXVzei1B6jcyh2nfKRAkYPd8V9fu50nlUOfJeGAiWxTyBNEhmWiEc4MeDrw3tiIj +zUrLveNr0z00rCKvuTBPVjM+FQ8Pg0FNAxVJUeaz4qpG3TT7QI+Np7wMqtJYNA3A +R3JjaZXGx6dF/0+2fUZHQKJvvWNAp8Xu9DouRQWGR1pmAbWpB8xL7zC6S8wWySIH +07nOyVLK2gFm5jvstvaHQvGPK6Xb4ydlp550vy8NQ9ji9cWehdJdzQ9bjKL6 +-----END CERTIFICATE----- diff --git a/app/src/test/resources/updated_cert.pem b/app/src/test/resources/updated_cert.pem new file mode 100644 index 00000000..21f9a693 --- /dev/null +++ b/app/src/test/resources/updated_cert.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFyTCCA7GgAwIBAgIJALD2RMhYzVdWMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJOWTEWMBQGA1UEBwwNTmV3IFlvcmsgQ2l0eTENMAsG +A1UECgwETEVBUDEVMBMGA1UECwwMVGVzdGluZyBEZXAuMSEwHwYJKoZIhvcNAQkB +FhJkb25vdHJlcGx5QGxlYXAuc2UwHhcNMTgwMTA5MjMyMTQzWhcNMzMwMTA1MjMy +MTQzWjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxFjAUBgNVBAcMDU5ldyBZ +b3JrIENpdHkxDTALBgNVBAoMBExFQVAxFTATBgNVBAsMDFRlc3RpbmcgRGVwLjEh +MB8GCSqGSIb3DQEJARYSZG9ub3RyZXBseUBsZWFwLnNlMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAvJqdroZQdvKOE9o/aga9GRfaY05Bs7EKdRp3pabu +O5uEyx0tRpbif+rcb3DRDWfIH+wxrNcSp5rRbQollttnH5Sdd0aG7taZLPCMHVlk +j92RRCggQQd3jrDzzKKNo3080B2BerWRK+Rzr0wVF7iD37Lcz5F74FoRhuXoYYSd +euMPqtp29S6U0bEXtTVdSEYzGKi/EaF5eNcRXUJcPl4aTCH7HTGmhsoCeQuKriU8 +JluGtPxJVji+z3JuIjXFmoxBY2KKb9mEosvXsNNDKvhrrVApSQf/SvDL3FDx9D81 +MAHjQknn1INvSk3M7IWV/dGL0tnDQMiCPUM6XT+RQuM03aCMfu/IvWuDDUNumWBU +5xtBX0Iu2OybRUzL9kyWsXFSQx617v7tuXuPWg158Dg3RyCIC2VdwouzGGsJXslc +W4T74w6HsxDpzxqrzwnwVHahn5/qntptFBlB37waGKRdvrTqlWq2jwHSp0F+y/x6 +h9BobBAJ7E6Ix5tprQSkPI6X62yXBfz2GDfZRYL2ltdD72mN5zzoA/SaT+HTtjOK +wS2GPxb5IZkB7u0yZQOkQ3hDtI4Ve8rj8Z0mK1S0HETU9kvA6/+3KmA1xeMjvlzL +HNmyIfXtfd5NIIrrRuWwGR2Y9KrezAdW+UuxHDE2SLDNCzJKjyQhzi8110Vtlm4A +45UCAwEAAaNQME4wHQYDVR0OBBYEFNm1kQeEsxrCNeJA7q/67Pz43/mVMB8GA1Ud +IwQYMBaAFNm1kQeEsxrCNeJA7q/67Pz43/mVMAwGA1UdEwQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAJpQL0Wh6eP3XpzS+PyiSFAjtKLsbtkDId0NTSrUiARFSj7I +S9YH1b0iG9pEd3XaNRffC72/R7UnYz0tbRC8tXYOKRB+gOkOAQXAleq01Qs93mA5 +6Titg8k9qXw6Vv+QK08CJUUFva6vMNOIbWkKN7GfdQZOig2EqLZ+jBdIai/NZ+uD +US2vk87Lyd+8VQZHsazxLLS/hMyBvJdiFhQmjBpak9J1EisqGWjmRVvo1Vq1aVy8 +IrqW6GweWs0toFflbd57xFeV7+VJ1WDg6HU+JY5hWnTyH8HDQJFTY0GBoPA4gnxx +c8P6VCuOO67bJPRxlI89+o7lRqfWXc+C2qQqdYTQda9850EwrTRl6dcHQZvZUYak +DV+vxN+zHZ+lN3RmQMl+BY/sIuDqu3MunPt8c8KXeL0zGw8A3v6UBzMJAN4FbFX6 +Rc9jdCvqi+DqtMq7nGJ+V9swlfPSeg5kot/Z9xlmjqT0256PW6yaxRsXhhNTGM/f +qCUmxf0aP3W8V6tCorSg6aiJ/xi0tLrfFDttgEH4CJHJh1n6w/D9aM9Q+ZRHTAlN +JXUZyhyv7OoyA/gsHoL6g28AzuCYAmPLVNS4Ym1L9r4R2Ltq1b4em7aUfIMomgqQ +xm+bOUujZtIJhDA8ckKKyb7xP0fJ13sTQyUt0/6CxJq6rj0OfVlQ3shGOBTU +-----END CERTIFICATE----- -- cgit v1.2.3 From 08ccdd30e8bf0addc15454feab923804c58ba9a2 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 10 Jan 2018 19:00:52 +0100 Subject: add unit testing to CI --- .gitlab-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4db92eb7..0edd09a9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,7 @@ stages: - docker_image_sdk - docker_image_other - build + - test # When using dind, it's wise to use the overlayfs driver for # improved performance. @@ -50,6 +51,12 @@ docker_image:emulator: DOCKER_IMAGE: android-emulator <<: *build_docker_image +unit_test: + image: "0xacab.org:4567/leap/bitmask_android/android-ndk:latest" + stage: test + script: + - ./gradlew test + build: image: "0xacab.org:4567/leap/bitmask_android/android-ndk:latest" stage: build -- cgit v1.2.3 From dd95ed8905846ed9d715e86426609869e12f58f9 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 11 Jan 2018 12:03:34 +0100 Subject: 8773 avoid image recreation for running tests --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0edd09a9..de2572bf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,7 +52,6 @@ docker_image:emulator: <<: *build_docker_image unit_test: - image: "0xacab.org:4567/leap/bitmask_android/android-ndk:latest" stage: test script: - ./gradlew test -- cgit v1.2.3 From 0105cb76e1fc6b59d42dff3059d5bb9548dab372 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 11 Jan 2018 12:23:00 +0100 Subject: Revert "8773 avoid image recreation for running tests" This reverts commit dd95ed8905846ed9d715e86426609869e12f58f9. --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index de2572bf..0edd09a9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,6 +52,7 @@ docker_image:emulator: <<: *build_docker_image unit_test: + image: "0xacab.org:4567/leap/bitmask_android/android-ndk:latest" stage: test script: - ./gradlew test -- cgit v1.2.3 From 1e94e6e1403d97e47119318bd43b173ef20658b1 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 11 Jan 2018 14:59:56 +0100 Subject: #8773 code review fixes --- app/build.gradle | 2 +- app/src/main/java/se/leap/bitmaskclient/Provider.java | 5 +++-- app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java | 8 ++++---- .../main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b894af38..52c64ab7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -81,7 +81,7 @@ dependencies { provided 'com.squareup.dagger:dagger-compiler:1.2.2' compile 'com.github.pedrovgs:renderers:1.5' compile 'com.intellij:annotations:12.0' - compile 'com.google.code.gson:gson:2.7' + compile 'com.google.code.gson:gson:2.8.2' compile 'com.squareup.okhttp3:okhttp:3.9.0' compile 'mbanje.kurt:fabbutton:1.1.4' compile "com.android.support:support-core-utils:26.1.0" diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java index ae07bc25..60b1b93c 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Provider.java +++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java @@ -234,7 +234,7 @@ public final class Provider implements Parcelable { public boolean equals(Object o) { if (o instanceof Provider) { Provider p = (Provider) o; - return p.getMainUrl().getDomain().equals(getMainUrl().getDomain()); + return p.getDomain().equals(getDomain()); } else return false; } @@ -253,7 +253,7 @@ public final class Provider implements Parcelable { @Override public int hashCode() { - return getMainUrl().getDomain().hashCode(); + return getDomain().hashCode(); } @Override @@ -261,6 +261,7 @@ public final class Provider implements Parcelable { return new Gson().toJson(this); } + //TODO: write a test for marshalling! private Provider(Parcel in) { try { mainUrl.setUrl(new URL(in.readString())); diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java index 439fb5e2..af79a95e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java @@ -50,6 +50,7 @@ public class ProviderApiConnector { Request request = requestBuilder.build(); Response response = okHttpClient.newCall(request).execute(); + //response code 401: already logged out if (response.isSuccessful() || response.code() == 401) { return true; } @@ -77,11 +78,10 @@ public class ProviderApiConnector { Request.Builder requestBuilder = new Request.Builder() .url(url) .method(request_method, jsonBody); - if (headerArgs != null) { - for (Pair keyValPair : headerArgs) { - requestBuilder.addHeader(keyValPair.first, keyValPair.second); - } + for (Pair keyValPair : headerArgs) { + requestBuilder.addHeader(keyValPair.first, keyValPair.second); } + //TODO: move to getHeaderArgs()? String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); requestBuilder.addHeader("Accept-Language", locale); diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java index cc005fcd..9f5fdc2d 100644 --- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java @@ -510,13 +510,13 @@ public abstract class ProviderApiManagerBase { return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient); } - protected String sendGetStringToServer(String url, List> headerArgs, OkHttpClient okHttpClient) { + protected String sendGetStringToServer(@NonNull String url, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) { return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); } - private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + private JSONObject requestJsonFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) { JSONObject responseJson; String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); @@ -530,7 +530,7 @@ public abstract class ProviderApiManagerBase { } - private String requestStringFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + private String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List> headerArgs, @NonNull OkHttpClient okHttpClient) { String plainResponseBody = null; try { -- cgit v1.2.3