Przeglądaj źródła

Merge pull request #1116 from nextcloud/sso

Proof of concept for Single Sign On
Andy Scherzinger 6 lat temu
rodzic
commit
c83356cd40

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

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

+ 14 - 1
src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java

@@ -258,6 +258,19 @@ public class EncryptionTestIT {
         }
     }
 
+    @Test
+    public void testSHA512() {
+        // sent to 3rd party app in cleartext
+        String token = "4ae5978bf5354cd284b539015d442141";
+        String salt = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.randomBytes(EncryptionUtils.saltLength));
+
+        // stored in database
+        String hashedToken = EncryptionUtils.generateSHA512(token, salt);
+
+        // check: use passed cleartext and salt to verify hashed token
+        assertTrue(EncryptionUtils.verifySHA512(hashedToken, token));
+    }
+
 
     // Helper
     private boolean compareJsonStrings(String expected, String actual) {
@@ -362,4 +375,4 @@ public class EncryptionTestIT {
 
         return temp;
     }
-}
+}

+ 9 - 0
src/main/AndroidManifest.xml

@@ -66,6 +66,7 @@
     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 
 
+
     <application
         android:name=".MainApp"
         android:icon="@mipmap/ic_launcher"
@@ -295,6 +296,14 @@
         <activity android:name=".ui.activity.ManageSpaceActivity"
                   android:label="@string/manage_space_title"
                   android:theme="@style/Theme.ownCloud" />
+
+
+        <service
+            android:name=".services.AccountManagerService"
+            android:enabled="true"
+            android:exported="true" >
+        </service>
+
     </application>
 
 </manifest>

+ 25 - 0
src/main/aidl/com/nextcloud/android/sso/aidl/IInputStreamService.aidl

@@ -0,0 +1,25 @@
+/***
+	Copyright (c) 2008-2011 CommonsWare, LLC
+	Licensed under the Apache License, Version 2.0 (the "License"); you may not
+	use this file except in compliance with the License. You may obtain	a copy
+	of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
+	by applicable law or agreed to in writing, software distributed under the
+	License is distributed on an "AS IS" BASIS,	WITHOUT	WARRANTIES OR CONDITIONS
+	OF ANY KIND, either express or implied. See the License for the specific
+	language governing permissions and limitations under the License.
+
+	From _The Busy Coder's Guide to Advanced Android Development_
+		http://commonsware.com/AdvAndroid
+
+
+	More information here: https://github.com/abeluck/android-streams-ipc
+*/
+
+package com.nextcloud.android.sso.aidl;
+
+// Declare the interface.
+interface IInputStreamService {
+
+    ParcelFileDescriptor performNextcloudRequest(in ParcelFileDescriptor input);
+
+}

+ 16 - 0
src/main/java/com/nextcloud/android/sso/Constants.java

@@ -0,0 +1,16 @@
+package com.nextcloud.android.sso;
+
+public class Constants {
+
+    // Authenticator related constants
+    public final static String SSO_USERNAME = "username";
+    public final static String SSO_TOKEN = "token";
+    public final static String SSO_SERVER_URL = "server_url";
+
+    // Custom Exceptions
+    static final String EXCEPTION_INVALID_TOKEN = "CE_1";
+    static final String EXCEPTION_ACCOUNT_NOT_FOUND = "CE_2";
+    static final String EXCEPTION_UNSUPPORTED_METHOD = "CE_3";
+    static final String EXCEPTION_INVALID_REQUEST_URL = "CE_4";
+    static final String EXCEPTION_HTTP_REQUEST_FAILED = "CE_5";
+}

+ 251 - 0
src/main/java/com/nextcloud/android/sso/InputStreamBinder.java

@@ -0,0 +1,251 @@
+/*
+ * Nextcloud SingleSignOn
+ *
+ * @author David Luhmer
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * More information here: https://github.com/abeluck/android-streams-ipc
+ */
+
+package com.nextcloud.android.sso;
+
+import android.accounts.Account;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.nextcloud.android.sso.aidl.IInputStreamService;
+import com.nextcloud.android.sso.aidl.NextcloudRequest;
+import com.nextcloud.android.sso.aidl.ParcelFileDescriptorUtil;
+import com.owncloud.android.authentication.AccountAuthenticator;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientManager;
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.EncryptionUtils;
+
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.NameValuePair;
+import org.apache.commons.httpclient.methods.DeleteMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.commons.httpclient.methods.StringRequestEntity;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Map;
+
+import static com.nextcloud.android.sso.Constants.EXCEPTION_ACCOUNT_NOT_FOUND;
+import static com.nextcloud.android.sso.Constants.EXCEPTION_HTTP_REQUEST_FAILED;
+import static com.nextcloud.android.sso.Constants.EXCEPTION_INVALID_REQUEST_URL;
+import static com.nextcloud.android.sso.Constants.EXCEPTION_INVALID_TOKEN;
+import static com.nextcloud.android.sso.Constants.EXCEPTION_UNSUPPORTED_METHOD;
+
+
+/**
+ * Stream binder to pass usable InputStreams across the process boundary in Android.
+ */
+public class InputStreamBinder extends IInputStreamService.Stub {
+
+    private final static String TAG = "InputStreamBinder";
+    private static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
+    private static final String CHARSET_UTF8 = "UTF-8";
+
+    private static final int HTTP_STATUS_CODE_OK = 200;
+    private static final int HTTP_STATUS_CODE_MULTIPLE_CHOICES = 300;
+
+    private static final char PATH_SEPARATOR = '/';
+    private Context context;
+
+    public InputStreamBinder(Context context) {
+        this.context = context;
+    }
+
+    private NameValuePair[] convertMapToNVP(Map<String, String> map) {
+        NameValuePair[] nvp = new NameValuePair[map.size()];
+        int i = 0;
+        for (String key : map.keySet()) {
+            nvp[i] = new NameValuePair(key, map.get(key));
+            i++;
+        }
+        return nvp;
+    }
+
+    public ParcelFileDescriptor performNextcloudRequest(ParcelFileDescriptor input) {
+        // read the input
+        final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input);
+
+        Exception exception = null;
+        InputStream httpStream = new InputStream() {
+            @Override
+            public int read() {
+                return 0;
+            }
+        };
+        try {
+            // Start request and catch exceptions
+            NextcloudRequest request = deserializeObjectAndCloseStream(is);
+            httpStream = processRequest(request);
+        } catch (Exception e) {
+            Log_OC.e(TAG, e.getMessage());
+            exception = e;
+        }
+
+        try {
+            // Write exception to the stream followed by the actual network stream
+            InputStream exceptionStream = serializeObjectToInputStream(exception);
+            InputStream resultStream = new java.io.SequenceInputStream(exceptionStream, httpStream);
+            return ParcelFileDescriptorUtil.pipeFrom(resultStream, thread -> Log.d(TAG, "Done sending result"));
+        } catch (IOException e) {
+            Log_OC.e(TAG, e.getMessage());
+        }
+        return null;
+    }
+
+    private <T extends Serializable> ByteArrayInputStream serializeObjectToInputStream(T obj) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(baos);
+        oos.writeObject(obj);
+        oos.flush();
+        oos.close();
+        return new ByteArrayInputStream(baos.toByteArray());
+    }
+
+    private <T extends Serializable> T deserializeObjectAndCloseStream(InputStream is) throws IOException, ClassNotFoundException {
+        ObjectInputStream ois = new ObjectInputStream(is);
+        T result = (T) ois.readObject();
+        is.close();
+        ois.close();
+        return result;
+    }
+
+
+    private InputStream processRequest(final NextcloudRequest request) throws UnsupportedOperationException, com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException, OperationCanceledException, AuthenticatorException, IOException {
+        Account account = AccountUtils.getOwnCloudAccountByName(context, request.getAccountName()); // TODO handle case that account is not found!
+        if(account == null) {
+            throw new IllegalStateException(EXCEPTION_ACCOUNT_NOT_FOUND);
+        }
+
+        // Validate token
+        if (!isValid(request)) {
+            throw new IllegalStateException(EXCEPTION_INVALID_TOKEN);
+        }
+
+        // Validate URL
+        if(request.getUrl().length() == 0 || request.getUrl().charAt(0) != PATH_SEPARATOR) {
+            throw new IllegalStateException(EXCEPTION_INVALID_REQUEST_URL, new IllegalStateException("URL need to start with a /"));
+        }
+
+        OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton();
+        OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
+        OwnCloudClient client = ownCloudClientManager.getClientFor(ocAccount, context);
+
+        String requestUrl = client.getBaseUri() + request.getUrl();
+        HttpMethodBase method;
+
+        switch (request.getMethod()) {
+            case "GET":
+                method = new GetMethod(requestUrl);
+                break;
+
+            case "POST":
+                method = new PostMethod(requestUrl);
+                if (request.getRequestBody() != null) {
+                    StringRequestEntity requestEntity = new StringRequestEntity(
+                            request.getRequestBody(),
+                            CONTENT_TYPE_APPLICATION_JSON,
+                            CHARSET_UTF8);
+                    ((PostMethod) method).setRequestEntity(requestEntity);
+                }
+                break;
+
+            case "PUT":
+                method = new PutMethod(requestUrl);
+                if (request.getRequestBody() != null) {
+                    StringRequestEntity requestEntity = new StringRequestEntity(
+                            request.getRequestBody(),
+                            CONTENT_TYPE_APPLICATION_JSON,
+                            CHARSET_UTF8);
+                    ((PutMethod) method).setRequestEntity(requestEntity);
+                }
+                break;
+
+            case "DELETE":
+                method = new DeleteMethod(requestUrl);
+                break;
+
+            default:
+                throw new UnsupportedOperationException(EXCEPTION_UNSUPPORTED_METHOD);
+
+        }
+
+        method.setQueryString(convertMapToNVP(request.getParameter()));
+        method.addRequestHeader("OCS-APIREQUEST", "true");
+
+        client.setFollowRedirects(request.isFollowRedirects());
+        int status = client.executeMethod(method);
+
+        // Check if status code is 2xx --> https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success
+        if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) {
+            return method.getResponseBodyAsStream();
+        } else {
+            throw new IllegalStateException(EXCEPTION_HTTP_REQUEST_FAILED, new IllegalStateException(String.valueOf(status)));
+        }
+    }
+
+    private boolean isValid(NextcloudRequest request) {
+        String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid());
+
+        SharedPreferences sharedPreferences = context.getSharedPreferences(AccountAuthenticator.SSO_SHARED_PREFERENCE,
+                Context.MODE_PRIVATE);
+        String hash = sharedPreferences.getString(callingPackageName, "");
+        return validateToken(hash, request.getToken());
+    }
+
+    private boolean validateToken(String hash, String token) {
+        String salt = hash.split("\\$")[1]; // TODO extract "$"
+
+        String newHash = EncryptionUtils.generateSHA512(token, salt);
+
+        // As discussed with Lukas R. at the Nextcloud Conf 2018, always compare whole strings
+        // and don't exit prematurely if the string does not match anymore to prevent timing-attacks
+        return isEqual(hash.getBytes(), newHash.getBytes());
+    }
+
+    // Taken from http://codahale.com/a-lesson-in-timing-attacks/
+    private static boolean isEqual(byte[] a, byte[] b) {
+        if (a.length != b.length) {
+            return false;
+        }
+
+        int result = 0;
+        for (int i = 0; i < a.length; i++) {
+            result |= a[i] ^ b[i];
+        }
+        return result == 0;
+    }
+}

+ 26 - 0
src/main/java/com/nextcloud/android/sso/aidl/IThreadListener.java

@@ -0,0 +1,26 @@
+/*
+ *  Nextcloud SingleSignOn
+ *
+ *  @author David Luhmer
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.android.sso.aidl;
+
+public interface IThreadListener {
+
+    void onThreadFinished(final Thread thread);
+
+}

+ 147 - 0
src/main/java/com/nextcloud/android/sso/aidl/NextcloudRequest.java

@@ -0,0 +1,147 @@
+/*
+ *  Nextcloud SingleSignOn
+ *
+ *  @author David Luhmer
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.android.sso.aidl;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class NextcloudRequest implements Serializable {
+
+    private static final long serialVersionUID = 215521212534240L; //assign a long value
+
+    private String method;
+    private Map<String, List<String>> header = new HashMap<>();
+    private Map<String, String> parameter = new HashMap<>();
+    private String requestBody;
+    private String url;
+    private String token;
+    private String packageName;
+    private String accountName;
+    private boolean followRedirects;
+
+    private NextcloudRequest() { }
+
+    public static class Builder {
+        private NextcloudRequest ncr;
+
+        public Builder() {
+            ncr = new NextcloudRequest();
+        }
+
+        public NextcloudRequest build() {
+            return ncr;
+        }
+
+        public Builder setMethod(String method) {
+            ncr.method = method;
+            return this;
+        }
+
+        public Builder setHeader(Map<String, List<String>> header) {
+            ncr.header = header;
+            return this;
+        }
+
+        public Builder setParameter(Map<String, String> parameter) {
+            ncr.parameter = parameter;
+            return this;
+        }
+
+        public Builder setRequestBody(String requestBody) {
+            ncr.requestBody = requestBody;
+            return this;
+        }
+
+        public Builder setUrl(String url) {
+            ncr.url = url;
+            return this;
+        }
+
+        public Builder setToken(String token) {
+            ncr.token = token;
+            return this;
+        }
+
+        public Builder setAccountName(String accountName) {
+            ncr.accountName = accountName;
+            return this;
+        }
+
+        /**
+         * Default value: true
+         * @param followRedirects
+         * @return
+         */
+        public Builder setFollowRedirects(boolean followRedirects) {
+            ncr.followRedirects = followRedirects;
+            return this;
+        }
+    }
+
+    public String getMethod() {
+        return this.method;
+    }
+
+    public Map<String, List<String>> getHeader() {
+        return this.header;
+    }
+
+    public Map<String, String> getParameter() {
+        return this.parameter;
+    }
+
+    public String getRequestBody() {
+        return this.requestBody;
+    }
+
+    public String getUrl() {
+        return this.url;
+    }
+
+    public String getToken() {
+        return this.token;
+    }
+
+    public void setToken(String token) {
+        this.token = token;
+    }
+
+    public String getPackageName() {
+        return this.packageName;
+    }
+
+    public void setPackageName(String packageName) {
+        this.packageName = packageName;
+    }
+
+    public String getAccountName() {
+        return this.accountName;
+    }
+
+    public void setAccountName(String accountName) {
+        this.accountName = accountName;
+    }
+
+    public boolean isFollowRedirects() {
+        return this.followRedirects;
+    }
+}

+ 106 - 0
src/main/java/com/nextcloud/android/sso/aidl/ParcelFileDescriptorUtil.java

@@ -0,0 +1,106 @@
+/*
+ *  Nextcloud SingleSignOn
+ *
+ *  @author David Luhmer
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.android.sso.aidl;
+
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public final class ParcelFileDescriptorUtil {
+
+    private ParcelFileDescriptorUtil() { }
+
+    public static ParcelFileDescriptor pipeFrom(InputStream inputStream, IThreadListener listener)
+            throws IOException {
+        ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+        ParcelFileDescriptor readSide = pipe[0];
+        ParcelFileDescriptor writeSide = pipe[1];
+
+        // start the transfer thread
+        new TransferThread(inputStream, new ParcelFileDescriptor.AutoCloseOutputStream(writeSide),
+                listener)
+                .start();
+
+        return readSide;
+    }
+
+    public static ParcelFileDescriptor pipeTo(OutputStream outputStream, IThreadListener listener)
+            throws IOException {
+        ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+        ParcelFileDescriptor readSide = pipe[0];
+        ParcelFileDescriptor writeSide = pipe[1];
+
+        // start the transfer thread
+        new TransferThread(new ParcelFileDescriptor.AutoCloseInputStream(readSide), outputStream,
+                listener)
+                .start();
+
+        return writeSide;
+    }
+
+    public static class TransferThread extends Thread {
+        private static final String TAG = TransferThread.class.getCanonicalName();
+        private final InputStream mIn;
+        private final OutputStream mOut;
+        private final IThreadListener mListener;
+
+        TransferThread(InputStream in, OutputStream out, IThreadListener listener) {
+            super("ParcelFileDescriptor Transfer Thread");
+            mIn = in;
+            mOut = out;
+            mListener = listener;
+            setDaemon(true);
+        }
+
+        @Override
+        public void run() {
+            byte[] buf = new byte[1024];
+            int len;
+
+            try {
+                while ((len = mIn.read(buf)) > 0) {
+                    mOut.write(buf, 0, len);
+                }
+                mOut.flush(); // just to be safe
+            } catch (IOException e) {
+                Log.e("TransferThread", "writing failed");
+            } finally {
+                try {
+                    mIn.close();
+                } catch (IOException e) {
+                    Log_OC.e(TAG, e.getMessage());
+                }
+                try {
+                    mOut.close();
+                } catch (IOException e) {
+                    Log_OC.e(TAG, e.getMessage());
+                }
+            }
+            if (mListener != null) {
+                mListener.onThreadFinished(this);
+            }
+        }
+    }
+}

+ 67 - 18
src/main/java/com/owncloud/android/authentication/AccountAuthenticator.java

@@ -1,4 +1,4 @@
-/**
+/*
  *   ownCloud Android client application
  *
  *   @author David A. Velasco
@@ -28,14 +28,21 @@ import android.accounts.AccountManager;
 import android.accounts.NetworkErrorException;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.Handler;
 import android.widget.Toast;
 
+import com.nextcloud.android.sso.Constants;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
+import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.accounts.AccountTypeUtils;
+import com.owncloud.android.lib.common.accounts.AccountUtils;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.EncryptionUtils;
+
+import java.util.UUID;
 
 
 /**
@@ -57,6 +64,9 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
     public static final String KEY_REQUIRED_FEATURES = "requiredFeatures";
     public static final String KEY_LOGIN_OPTIONS = "loginOptions";
     public static final String KEY_ACCOUNT = "account";
+    public static final String SSO_SHARED_PREFERENCE = "sso";
+    
+    private static final String NEXTCLOUD_SSO = "NextcloudSSO";
     
     private static final String TAG = AccountAuthenticator.class.getSimpleName();
     
@@ -75,16 +85,15 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
      */
     @Override
     public Bundle addAccount(AccountAuthenticatorResponse response,
-            String accountType, String authTokenType,
-            String[] requiredFeatures, Bundle options)
-            throws NetworkErrorException {
+                             String accountType, String authTokenType,
+                             String[] requiredFeatures, Bundle options) {
         Log_OC.i(TAG, "Adding account with type " + accountType + " and auth token " + authTokenType);
-        
-        final Bundle bundle = new Bundle();
-        
+
         AccountManager accountManager = AccountManager.get(mContext);
         Account[] accounts = accountManager.getAccountsByType(MainApp.getAccountType(mContext));
-        
+
+        final Bundle bundle = new Bundle();
+
         if (mContext.getResources().getBoolean(R.bool.multiaccount_support) || accounts.length < 1) {
             try {
                 validateAccountType(accountType);
@@ -104,16 +113,13 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
             setIntentFlags(intent);
             
             bundle.putParcelable(AccountManager.KEY_INTENT, intent);
-        
         } else {
-
             // Return an error
             bundle.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION);
             final String message = String.format(mContext.getString(R.string.auth_unsupported_multiaccount), mContext.getString(R.string.app_name)); 
             bundle.putString(AccountManager.KEY_ERROR_MESSAGE, message);
            
             mHandler.post(() -> Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show());
-            
         }
         
         return bundle;
@@ -124,7 +130,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
      */
     @Override
     public Bundle confirmCredentials(AccountAuthenticatorResponse response,
-            Account account, Bundle options) throws NetworkErrorException {
+                                     Account account, Bundle options) {
         try {
             validateAccountType(account.type);
         } catch (AuthenticatorException e) {
@@ -155,9 +161,53 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
      */
     @Override
     public Bundle getAuthToken(AccountAuthenticatorResponse response,
-            Account account, String authTokenType, Bundle options)
-            throws NetworkErrorException {
-        /// validate parameters
+                               Account account, String authTokenType, Bundle options) {
+
+        if (NEXTCLOUD_SSO.equals(authTokenType)) {
+            final Bundle result = new Bundle();
+
+            String packageName = options.getString("androidPackageName");
+
+            if (packageName == null) {
+                Log_OC.e(TAG, "No calling package, exit.");
+                return result;
+            }
+
+            // create token
+            SharedPreferences sharedPreferences = mContext.getSharedPreferences(SSO_SHARED_PREFERENCE,
+                    Context.MODE_PRIVATE);
+            String token = UUID.randomUUID().toString().replaceAll("-", "");
+
+            String hashedTokenWithSalt = EncryptionUtils.generateSHA512(token);
+
+            SharedPreferences.Editor editor = sharedPreferences.edit();
+            editor.putString(packageName, hashedTokenWithSalt);
+            editor.apply();
+                        
+            String serverUrl;
+            String userId;
+            try {
+                OwnCloudAccount ocAccount = new OwnCloudAccount(account, mContext);
+                serverUrl = ocAccount.getBaseUri().toString();
+                AccountManager accountManager = AccountManager.get(mContext);
+                userId = accountManager.getUserData(account,
+                        com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
+            } catch (AccountUtils.AccountNotFoundException e) {
+                Log_OC.e(TAG, "Account not found");
+                return new Bundle();
+            }
+
+            result.putString(AccountManager.KEY_ACCOUNT_NAME,  account.name);
+            result.putString(AccountManager.KEY_ACCOUNT_TYPE,  MainApp.getAccountType(mContext));
+            result.putString(AccountManager.KEY_AUTHTOKEN,     NEXTCLOUD_SSO);
+            result.putString(Constants.SSO_USERNAME,   userId);
+            result.putString(Constants.SSO_TOKEN,      token);
+            result.putString(Constants.SSO_SERVER_URL, serverUrl);
+
+            return result;
+        }
+
+        // validate parameters
         try {
             validateAccountType(account.type);
             validateAuthTokenType(authTokenType);
@@ -203,7 +253,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
 
     @Override
     public Bundle hasFeatures(AccountAuthenticatorResponse response,
-            Account account, String[] features) throws NetworkErrorException {
+                              Account account, String[] features) {
         final Bundle result = new Bundle();
         result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
         return result;
@@ -211,8 +261,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator {
 
     @Override
     public Bundle updateCredentials(AccountAuthenticatorResponse response,
-            Account account, String authTokenType, Bundle options)
-            throws NetworkErrorException {
+                                    Account account, String authTokenType, Bundle options) {
 
         Intent intent = new Intent(mContext, AuthenticatorActivity.class);
         intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);

+ 45 - 0
src/main/java/com/owncloud/android/services/AccountManagerService.java

@@ -0,0 +1,45 @@
+/*
+ *  Nextcloud SingleSignOn
+ *
+ *  @author David Luhmer
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.services;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import com.nextcloud.android.sso.InputStreamBinder;
+
+public class AccountManagerService extends Service {
+
+    private InputStreamBinder mBinder;
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        if(mBinder == null) {
+            mBinder = new InputStreamBinder(this);
+        }
+        return mBinder;
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        return super.onUnbind(intent);
+    }
+
+}

+ 76 - 29
src/main/java/com/owncloud/android/utils/EncryptionUtils.java

@@ -57,7 +57,6 @@ import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
 import java.security.PrivateKey;
 import java.security.PublicKey;
 import java.security.SecureRandom;
@@ -65,7 +64,6 @@ import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
-import java.security.spec.InvalidParameterSpecException;
 import java.security.spec.KeySpec;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.ArrayList;
@@ -80,7 +78,6 @@ import javax.crypto.KeyGenerator;
 import javax.crypto.NoSuchPaddingException;
 import javax.crypto.SecretKey;
 import javax.crypto.SecretKeyFactory;
-import javax.crypto.ShortBufferException;
 import javax.crypto.spec.GCMParameterSpec;
 import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.PBEKeySpec;
@@ -98,6 +95,7 @@ public class EncryptionUtils {
     public static final String MNEMONIC = "MNEMONIC";
     public static final int ivLength = 16;
     public static final int saltLength = 40;
+    public static final String HASH_DELIMITER = "$";
 
     private static final String ivDelimiter = "fA=="; // "|" base64 encoded
     private static final int iterationCount = 1024;
@@ -132,9 +130,9 @@ public class EncryptionUtils {
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     public static EncryptedFolderMetadata encryptFolderMetadata(DecryptedFolderMetadata decryptedFolderMetadata,
                                                                 String privateKey)
-            throws IOException, NoSuchAlgorithmException, ShortBufferException, InvalidKeyException,
+            throws NoSuchAlgorithmException, InvalidKeyException,
             InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
-            NoSuchProviderException, IllegalBlockSizeException, InvalidKeySpecException, CertificateException {
+            IllegalBlockSizeException, InvalidKeySpecException {
 
         HashMap<String, EncryptedFolderMetadata.EncryptedFile> files = new HashMap<>();
         EncryptedFolderMetadata encryptedFolderMetadata = new EncryptedFolderMetadata(decryptedFolderMetadata
@@ -171,9 +169,9 @@ public class EncryptionUtils {
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     public static DecryptedFolderMetadata decryptFolderMetaData(EncryptedFolderMetadata encryptedFolderMetadata,
                                                                 String privateKey)
-            throws IOException, NoSuchAlgorithmException, ShortBufferException, InvalidKeyException,
+            throws NoSuchAlgorithmException, InvalidKeyException,
             InvalidAlgorithmParameterException, NoSuchPaddingException, BadPaddingException,
-            NoSuchProviderException, IllegalBlockSizeException, CertificateException, InvalidKeySpecException {
+            IllegalBlockSizeException, InvalidKeySpecException {
 
         HashMap<String, DecryptedFolderMetadata.DecryptedFile> files = new HashMap<>();
         DecryptedFolderMetadata decryptedFolderMetadata = new DecryptedFolderMetadata(
@@ -278,9 +276,9 @@ public class EncryptionUtils {
      */
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     public static EncryptedFile encryptFile(OCFile ocFile, byte[] encryptionKeyBytes, byte[] iv)
-            throws NoSuchProviderException, NoSuchAlgorithmException,
+            throws NoSuchAlgorithmException,
             InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
-            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException {
+            BadPaddingException, IllegalBlockSizeException, IOException {
         File file = new File(ocFile.getStoragePath());
 
         return encryptFile(file, encryptionKeyBytes, iv);
@@ -294,9 +292,9 @@ public class EncryptionUtils {
      */
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     public static EncryptedFile encryptFile(File file, byte[] encryptionKeyBytes, byte[] iv)
-            throws NoSuchProviderException, NoSuchAlgorithmException,
+            throws NoSuchAlgorithmException,
             InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
-            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException {
+            BadPaddingException, IllegalBlockSizeException, IOException {
 
         Cipher cipher = Cipher.getInstance(AES_CIPHER);
 
@@ -325,9 +323,9 @@ public class EncryptionUtils {
      */
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     public static byte[] decryptFile(File file, byte[] encryptionKeyBytes, byte[] iv, byte[] authenticationTag)
-            throws NoSuchProviderException, NoSuchAlgorithmException,
+            throws NoSuchAlgorithmException,
             InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
-            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException {
+            BadPaddingException, IllegalBlockSizeException, IOException {
 
 
         Cipher cipher = Cipher.getInstance(AES_CIPHER);
@@ -370,9 +368,9 @@ public class EncryptionUtils {
      */
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     public static String encryptStringAsymmetric(String string, String cert)
-            throws NoSuchProviderException, NoSuchAlgorithmException,
-            InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
-            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, InvalidKeySpecException,
+            throws NoSuchAlgorithmException,
+            NoSuchPaddingException, InvalidKeyException,
+            BadPaddingException, IllegalBlockSizeException, IOException,
             CertificateException {
 
         Cipher cipher = Cipher.getInstance(RSA_CIPHER);
@@ -406,9 +404,9 @@ public class EncryptionUtils {
      */
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     public static String decryptStringAsymmetric(String string, String privateKeyString)
-            throws NoSuchProviderException, NoSuchAlgorithmException,
-            InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
-            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, CertificateException,
+            throws NoSuchAlgorithmException,
+            NoSuchPaddingException, InvalidKeyException,
+            BadPaddingException, IllegalBlockSizeException,
             InvalidKeySpecException {
 
         Cipher cipher = Cipher.getInstance(RSA_CIPHER);
@@ -437,10 +435,9 @@ public class EncryptionUtils {
      */
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     public static String encryptStringSymmetric(String string, byte[] encryptionKeyBytes)
-            throws NoSuchProviderException, NoSuchAlgorithmException,
+            throws NoSuchAlgorithmException,
             InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
-            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, InvalidKeySpecException,
-            CertificateException {
+            BadPaddingException, IllegalBlockSizeException {
 
         Cipher cipher = Cipher.getInstance(AES_CIPHER);
         byte[] iv = randomBytes(ivLength);
@@ -469,10 +466,9 @@ public class EncryptionUtils {
      */
     @RequiresApi(api = Build.VERSION_CODES.KITKAT)
     public static String decryptStringSymmetric(String string, byte[] encryptionKeyBytes)
-            throws NoSuchProviderException, NoSuchAlgorithmException,
+            throws NoSuchAlgorithmException,
             InvalidAlgorithmParameterException, NoSuchPaddingException, InvalidKeyException,
-            BadPaddingException, IllegalBlockSizeException, IOException, ShortBufferException, CertificateException,
-            InvalidKeySpecException {
+            BadPaddingException, IllegalBlockSizeException {
 
         Cipher cipher = Cipher.getInstance(AES_CIPHER);
 
@@ -502,8 +498,8 @@ public class EncryptionUtils {
      * @return encrypted string, bytes first encoded base64, IV separated with "|", then to string
      */
     public static String encryptPrivateKey(String privateKey, String keyPhrase) throws NoSuchPaddingException,
-            NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, BadPaddingException,
-            IllegalBlockSizeException, InvalidKeySpecException, InvalidParameterSpecException {
+            NoSuchAlgorithmException, InvalidKeyException, BadPaddingException,
+            IllegalBlockSizeException, InvalidKeySpecException {
         Cipher cipher = Cipher.getInstance(AES_CIPHER);
 
         SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
@@ -533,7 +529,7 @@ public class EncryptionUtils {
      * @return decrypted string
      */
     public static String decryptPrivateKey(String privateKey, String keyPhrase) throws NoSuchPaddingException,
-            NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, BadPaddingException,
+            NoSuchAlgorithmException, InvalidKeyException, BadPaddingException,
             IllegalBlockSizeException, InvalidKeySpecException, InvalidAlgorithmParameterException {
 
         // split up iv, salt
@@ -559,7 +555,7 @@ public class EncryptionUtils {
                 .replace("-----END PRIVATE KEY-----", "");
     }
 
-    public static String privateKeyToPEM(PrivateKey privateKey) throws IOException {
+    public static String privateKeyToPEM(PrivateKey privateKey) {
         String privateKeyString = encodeBytesToBase64String(privateKey.getEncoded());
 
         return "-----BEGIN PRIVATE KEY-----\n" + privateKeyString.replaceAll("(.{65})", "$1\n")
@@ -642,4 +638,55 @@ public class EncryptionUtils {
 
         return iv;
     }
+
+    /**
+     * Generate a SHA512 with appended salt
+     *
+     * @param token token to be hashed
+     * @return SHA512 with appended salt, delimiter HASH_DELIMITER
+     */
+    public static String generateSHA512(String token) {
+        String salt = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.randomBytes(EncryptionUtils.saltLength));
+
+        return generateSHA512(token, salt);
+    }
+
+    /**
+     * Generate a SHA512 with appended salt
+     *
+     * @param token token to be hashed
+     * @return SHA512 with appended salt, delimiter HASH_DELIMITER
+     */
+    public static String generateSHA512(String token, String salt) {
+        MessageDigest digest;
+        String hashedToken = "";
+        byte[] hash;
+        try {
+            digest = MessageDigest.getInstance("SHA-512");
+            digest.update(salt.getBytes());
+            hash = digest.digest(token.getBytes());
+
+            StringBuilder stringBuilder = new StringBuilder();
+            for (byte hashByte : hash) {
+                stringBuilder.append(Integer.toString((hashByte & 0xff) + 0x100, 16).substring(1));
+            }
+
+            stringBuilder.append(HASH_DELIMITER).append(salt);
+
+            hashedToken = stringBuilder.toString();
+
+        } catch (NoSuchAlgorithmException e) {
+            Log_OC.e(TAG, "Generating SHA512 failed", e);
+        }
+
+        return hashedToken;
+    }
+
+    public static boolean verifySHA512(String hashWithSalt, String compareToken) {
+        String salt = hashWithSalt.split("\\" + HASH_DELIMITER)[1];
+
+        String newHash = generateSHA512(compareToken, salt);
+
+        return hashWithSalt.equals(newHash);
+    }
 }