linked: redesign github resource creation, implement ouath flow (WIP)

This commit is contained in:
Vincent Breitmoser 2015-09-01 06:41:47 +02:00
parent 9668abfe9e
commit b52a0303ca
8 changed files with 501 additions and 12 deletions

View file

@ -113,7 +113,17 @@
<activity
android:name=".ui.linked.LinkedIdWizard"
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
android:label="@string/title_linked_create" />
android:launchMode="singleInstance"
android:label="@string/title_linked_create">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="linked"
android:scheme="oauth-openkeychain" />
</intent-filter>
</activity>
<activity
android:name=".ui.QrCodeViewActivity"
android:label="@string/share_qr_code_dialog_title" />

View file

@ -5,6 +5,7 @@ import android.graphics.PorterDuff;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
@ -48,16 +49,19 @@ public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragmen
protected abstract View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
@Override
@Override @NonNull
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = newView(inflater, container, savedInstanceState);
view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
cryptoOperation();
}
});
View nextButton = view.findViewById(R.id.next_button);
if (nextButton != null) {
nextButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
cryptoOperation();
}
});
}
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
@Override

View file

@ -0,0 +1,246 @@
/*
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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 org.sufficientlysecure.keychain.ui.linked;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ViewAnimator;
import javax.net.ssl.HttpsURLConnection;
import org.json.JSONException;
import org.json.JSONObject;
import org.sufficientlysecure.keychain.BuildConfig;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.widget.StatusIndicator;
import org.sufficientlysecure.keychain.util.Log;
public class LinkedIdCreateGithubFragment extends Fragment {
ViewAnimator mProceedContainer;
EditText mGithubUsername, mGithubPassword;
StatusIndicator mStatus1, mStatus2, mStatus3;
public static LinkedIdCreateGithubFragment newInstance() {
return new LinkedIdCreateGithubFragment();
}
@Override @NonNull
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.linked_create_github_fragment, container, false);
mProceedContainer = (ViewAnimator) view.findViewById(R.id.proceed_container);
mGithubUsername = (EditText) view.findViewById(R.id.username);
mGithubPassword = (EditText) view.findViewById(R.id.password);
mStatus1 = (StatusIndicator) view.findViewById(R.id.linked_status_step1);
mStatus2 = (StatusIndicator) view.findViewById(R.id.linked_status_step2);
mStatus3 = (StatusIndicator) view.findViewById(R.id.linked_status_step3);
view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
step1GetOAuthToken();
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
LinkedIdWizard wizard = (LinkedIdWizard) getActivity();
final String oAuthCode = wizard.oAuthGetCode();
final String oAuthState = wizard.oAuthGetState();
if (oAuthCode == null) {
Log.d(Constants.TAG, "no code");
return;
}
Log.d(Constants.TAG, "got code: " + oAuthCode);
new AsyncTask<Void,Void,JSONObject>() {
@Override
protected JSONObject doInBackground(Void... dummy) {
try {
JSONObject params = new JSONObject();
params.put("client_id", "7a011b66275f244d3f21");
params.put("client_secret", "eaced8a6655719d8c6848396de97b3f5d7a89fec");
params.put("code", oAuthCode);
params.put("state", oAuthState);
return jsonHttpRequest("https://github.com/login/oauth/access_token", params, null);
} catch (IOException e) {
Log.e(Constants.TAG, "error in request", e);
} catch (JSONException e) {
throw new AssertionError("json error, this is a bug!");
}
return null;
}
@Override
protected void onPostExecute(JSONObject result) {
super.onPostExecute(result);
Log.d(Constants.TAG, "response: " + result);
if (result == null || !result.has("access_token")) {
mStatus1.setDisplayedChild(3);
return;
}
mStatus1.setDisplayedChild(2);
step2PostGist(result.optString("access_token"));
}
}.execute();
}
private void step1GetOAuthToken() {
mStatus1.setDisplayedChild(1);
mStatus2.setDisplayedChild(0);
mStatus3.setDisplayedChild(0);
mProceedContainer.setDisplayedChild(1);
LinkedIdWizard wizard = (LinkedIdWizard) getActivity();
wizard.oAuthRequest("github.com/login/oauth/authorize", "7a011b66275f244d3f21", "gist");
}
private void step2PostGist(final String accessToken) {
mStatus2.setDisplayedChild(1);
new AsyncTask<Void,Void,JSONObject>() {
@Override
protected JSONObject doInBackground(Void... dummy) {
try {
JSONObject file = new JSONObject();
file.put("content", "hello!");
JSONObject files = new JSONObject();
files.put("file1.txt", file);
JSONObject params = new JSONObject();
params.put("public", true);
params.put("description", "OpenKeychain API Tests");
params.put("files", files);
return jsonHttpRequest("https://api.github.com/gists", params, accessToken);
} catch (IOException e) {
Log.e(Constants.TAG, "error in request", e);
} catch (JSONException e) {
throw new AssertionError("json error, this is a bug!");
}
return null;
}
@Override
protected void onPostExecute(JSONObject result) {
super.onPostExecute(result);
Log.d(Constants.TAG, "response: " + result);
if (result == null) {
mStatus2.setDisplayedChild(3);
return;
}
mStatus2.setDisplayedChild(2);
}
}.execute();
}
private static JSONObject jsonHttpRequest(String url, JSONObject params, String accessToken)
throws IOException {
HttpsURLConnection nection = (HttpsURLConnection) new URL(url).openConnection();
nection.setDoInput(true);
nection.setDoOutput(true);
nection.setRequestProperty("Content-Type", "application/json");
nection.setRequestProperty("Accept", "application/json");
nection.setRequestProperty("User-Agent", "OpenKeychain " + BuildConfig.VERSION_NAME);
if (accessToken != null) {
nection.setRequestProperty("Authorization", "token " + accessToken);
}
OutputStream os = nection.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
writer.write(params.toString());
writer.flush();
writer.close();
os.close();
try {
nection.connect();
InputStream in = new BufferedInputStream(nection.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
response.append(line);
}
try {
return new JSONObject(response.toString());
} catch (JSONException e) {
throw new IOException(e);
}
} finally {
nection.disconnect();
}
}
}

View file

@ -84,8 +84,8 @@ public class LinkedIdSelectFragment extends Fragment {
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LinkedIdCreateGithubStep1Fragment frag =
LinkedIdCreateGithubStep1Fragment.newInstance();
LinkedIdCreateGithubFragment frag =
LinkedIdCreateGithubFragment.newInstance();
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
}

View file

@ -17,7 +17,10 @@
package org.sufficientlysecure.keychain.ui.linked;
import java.util.Random;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
@ -25,6 +28,7 @@ import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import org.spongycastle.util.encoders.Hex;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
@ -32,6 +36,8 @@ import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.Log;
public class LinkedIdWizard extends BaseActivity {
@ -125,4 +131,55 @@ public class LinkedIdWizard extends BaseActivity {
inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
private String mOAuthCode, mOAuthState;
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Uri uri = intent.getData();
if (uri != null) {
Log.d(Constants.TAG, "received oauth uri: " + uri);
String state = uri.getQueryParameter("state");
if (!mOAuthState.equalsIgnoreCase(state)) {
Notify.create(this, "Authentication Error!", Style.ERROR).show();
return;
}
mOAuthCode = uri.getQueryParameter("code");
} else {
Log.d(Constants.TAG, "received oauth uri: null");
}
}
public String oAuthGetCode() {
try {
return mOAuthCode;
} finally {
mOAuthCode = null;
}
}
public String oAuthGetState() {
return mOAuthState;
}
public void oAuthRequest(String hostAndPath, String clientId, String scope) {
byte[] buf = new byte[16];
new Random().nextBytes(buf);
mOAuthState = new String(Hex.encode(buf));
Intent intent = new Intent(
Intent.ACTION_VIEW,
Uri.parse("https://" + hostAndPath +
"?client_id=" + clientId +
"&scope=" + scope +
"&redirect_uri=oauth-openkeychain://linked/" +
"&state=" + mOAuthState));
startActivity(intent);
}
}

View file

@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="false"
android:layout_above="@+id/create_key_button_divider">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:orientation="vertical">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="8dp"
android:src="@drawable/linked_github" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="This login data will be used to post a gist, verifying this account and key belongs to you!"
style="?android:textAppearanceSmall"/>
<org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/proceed_container"
custom:initialView="1">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
style="?android:buttonBarButtonStyle"
android:drawableLeft="@drawable/ic_mode_edit_grey_24dp"
android:drawableStart="@drawable/ic_mode_edit_grey_24dp"
android:drawablePadding="12dp"
android:text="Post Gist &amp; Link Key"
android:id="@+id/button_send"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.sufficientlysecure.keychain.ui.widget.StatusIndicator
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/linked_status_step1"
android:layout_margin="4dp">
</org.sufficientlysecure.keychain.ui.widget.StatusIndicator>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Login at GitHub…"
style="?android:textAppearanceMedium"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.sufficientlysecure.keychain.ui.widget.StatusIndicator
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/linked_status_step2"
android:layout_margin="4dp">
</org.sufficientlysecure.keychain.ui.widget.StatusIndicator>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Post Gist…"
style="?android:textAppearanceMedium"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.sufficientlysecure.keychain.ui.widget.StatusIndicator
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/linked_status_step3"
android:layout_margin="4dp">
</org.sufficientlysecure.keychain.ui.widget.StatusIndicator>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Update Key…"
style="?android:textAppearanceMedium"
/>
</LinearLayout>
</LinearLayout>
</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
</LinearLayout>
</ScrollView>
<View
android:id="@+id/create_key_button_divider"
android:layout_width="match_parent"
android:layout_height="1dip"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:background="?android:attr/listDivider"
android:layout_alignTop="@+id/create_key_buttons"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:id="@+id/create_key_buttons">
<TextView
android:id="@+id/back_button"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/btn_cancel"
android:minHeight="?android:attr/listPreferredItemHeight"
android:textAllCaps="true"
style="?android:attr/borderlessButtonStyle"
android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:clickable="true"
android:layout_gravity="center_vertical" />
</LinearLayout>
</RelativeLayout>

View file

@ -4,8 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:showIn="@layout/linked_create_https_fragment_step2">
tools:showIn="@layout/linked_create_github_fragment_step2">
<ViewAnimator
android:id="@+id/verify_progress"

View file

@ -82,6 +82,7 @@
<string name="btn_export_to_server">"Upload To Keyserver"</string>
<string name="btn_next">"Next"</string>
<string name="btn_back">"Back"</string>
<string name="btn_cancel">"Cancel"</string>
<string name="btn_no">"No"</string>
<string name="btn_match">"Fingerprints match"</string>
<string name="btn_share_encrypted_signed">"Encrypt/sign and share text"</string>