Merge pull request #603 from timbray/master

Adds first level of keybase support
This commit is contained in:
Dominik Schürmann 2014-05-05 10:10:47 +02:00
commit 6d10ca678a
19 changed files with 678 additions and 32 deletions

1
.gitignore vendored
View File

@ -31,3 +31,4 @@ pom.xml.*
#OS Specific
[Tt]humbs.db
.DS_Store

View File

@ -56,6 +56,7 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import org.sufficientlysecure.keychain.util.HkpKeyServer;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.KeybaseKeyServer;
import org.sufficientlysecure.keychain.util.KeychainServiceListener;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.ProgressScaler;
@ -103,6 +104,7 @@ public class KeychainIntentService extends IntentService
public static final String ACTION_UPLOAD_KEYRING = Constants.INTENT_PREFIX + "UPLOAD_KEYRING";
public static final String ACTION_DOWNLOAD_AND_IMPORT_KEYS = Constants.INTENT_PREFIX + "QUERY_KEYRING";
public static final String ACTION_IMPORT_KEYBASE_KEYS = Constants.INTENT_PREFIX + "DOWNLOAD_KEYBASE";
public static final String ACTION_CERTIFY_KEYRING = Constants.INTENT_PREFIX + "SIGN_KEYRING";
@ -739,6 +741,55 @@ public class KeychainIntentService extends IntentService
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_IMPORT_KEYBASE_KEYS.equals(action)) {
ArrayList<ImportKeysListEntry> entries = data.getParcelableArrayList(DOWNLOAD_KEY_LIST);
try {
KeybaseKeyServer server = new KeybaseKeyServer();
for (ImportKeysListEntry entry : entries) {
// the keybase handle is in userId(1)
String keybaseID = entry.getUserIds().get(1);
byte[] downloadedKeyBytes = server.get(keybaseID).getBytes();
// create PGPKeyRing object based on downloaded armored key
PGPKeyRing downloadedKey = null;
BufferedInputStream bufferedInput =
new BufferedInputStream(new ByteArrayInputStream(downloadedKeyBytes));
if (bufferedInput.available() > 0) {
InputStream in = PGPUtil.getDecoderStream(bufferedInput);
PGPObjectFactory objectFactory = new PGPObjectFactory(in);
// get first object in block
Object obj;
if ((obj = objectFactory.nextObject()) != null) {
if (obj instanceof PGPKeyRing) {
downloadedKey = (PGPKeyRing) obj;
} else {
throw new PgpGeneralException("Object not recognized as PGPKeyRing!");
}
}
}
// save key bytes in entry object for doing the
// actual import afterwards
entry.setBytes(downloadedKey.getEncoded());
}
Intent importIntent = new Intent(this, KeychainIntentService.class);
importIntent.setAction(ACTION_IMPORT_KEYRING);
Bundle importData = new Bundle();
importData.putParcelableArrayList(IMPORT_KEY_LIST, entries);
importIntent.putExtra(EXTRA_DATA, importData);
importIntent.putExtra(EXTRA_MESSENGER, mMessenger);
// now import it with this service
onHandleIntent(importIntent);
// result is handled in ACTION_IMPORT_KEYRING
} catch (Exception e) {
sendErrorToHandler(e);
}
} else if (ACTION_DOWNLOAD_AND_IMPORT_KEYS.equals(action)) {
try {
ArrayList<ImportKeysListEntry> entries = data.getParcelableArrayList(DOWNLOAD_KEY_LIST);
@ -767,7 +818,6 @@ public class KeychainIntentService extends IntentService
// get first object in block
Object obj;
if ((obj = objectFactory.nextObject()) != null) {
Log.d(Constants.TAG, "Found class: " + obj.getClass());
if (obj instanceof PGPKeyRing) {
downloadedKey = (PGPKeyRing) obj;

View File

@ -62,6 +62,8 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O
+ "IMPORT_KEY_FROM_KEYSERVER";
public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN = Constants.INTENT_PREFIX
+ "IMPORT_KEY_FROM_KEY_SERVER_AND_RETURN";
public static final String ACTION_IMPORT_KEY_FROM_KEYBASE = Constants.INTENT_PREFIX
+ "IMPORT_KEY_FROM_KEYBASE";
// Actions for internal use only:
public static final String ACTION_IMPORT_KEY_FROM_FILE = Constants.INTENT_PREFIX
@ -92,13 +94,15 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O
ImportKeysFileFragment.class,
ImportKeysQrCodeFragment.class,
ImportKeysClipboardFragment.class,
ImportKeysNFCFragment.class
ImportKeysNFCFragment.class,
ImportKeysKeybaseFragment.class
};
private static final int NAV_SERVER = 0;
private static final int NAV_FILE = 1;
private static final int NAV_QR_CODE = 2;
private static final int NAV_CLIPBOARD = 3;
private static final int NAV_NFC = 4;
private static final int NAV_KEYBASE = 5;
private int mCurrentNavPosition = -1;
@ -236,6 +240,12 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O
// NOTE: this only displays the appropriate fragment, no actions are taken
loadNavFragment(NAV_NFC, null);
// no immediate actions!
startListFragment(savedInstanceState, null, null, null);
} else if (ACTION_IMPORT_KEY_FROM_KEYBASE.equals(action)) {
// NOTE: this only displays the appropriate fragment, no actions are taken
loadNavFragment(NAV_KEYBASE, null);
// no immediate actions!
startListFragment(savedInstanceState, null, null, null);
} else {
@ -340,8 +350,8 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O
startListFragment(savedInstanceState, null, null, query);
}
public void loadCallback(byte[] importData, Uri dataUri, String serverQuery, String keyServer) {
mListFragment.loadNew(importData, dataUri, serverQuery, keyServer);
public void loadCallback(byte[] importData, Uri dataUri, String serverQuery, String keyServer, String keybaseQuery) {
mListFragment.loadNew(importData, dataUri, serverQuery, keyServer, keybaseQuery);
}
/**
@ -449,6 +459,31 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O
// start service with intent
startService(intent);
} else if (mListFragment.getKeybaseQuery() != null) {
// Send all information needed to service to query keys in other thread
Intent intent = new Intent(this, KeychainIntentService.class);
intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYBASE_KEYS);
// fill values for this action
Bundle data = new Bundle();
// get selected key entries
ArrayList<ImportKeysListEntry> selectedEntries = mListFragment.getSelectedData();
data.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST, selectedEntries);
intent.putExtra(KeychainIntentService.EXTRA_DATA, data);
// Create a new Messenger for the communication back
Messenger messenger = new Messenger(saveHandler);
intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger);
// show progress dialog
saveHandler.showProgressDialog(this);
// start service with intent
startService(intent);
} else {
AppMsg.makeText(this, R.string.error_nothing_import, AppMsg.STYLE_ALERT).show();
}

View File

@ -71,7 +71,7 @@ public class ImportKeysClipboardFragment extends Fragment {
return;
}
}
mImportActivity.loadCallback(sendText.getBytes(), null, null, null);
mImportActivity.loadCallback(sendText.getBytes(), null, null, null, null);
}
});

View File

@ -85,7 +85,7 @@ public class ImportKeysFileFragment extends Fragment {
if (resultCode == Activity.RESULT_OK && data != null) {
// load data
mImportActivity.loadCallback(null, data.getData(), null, null);
mImportActivity.loadCallback(null, data.getData(), null, null, null);
}
break;

View File

@ -0,0 +1,120 @@
/*
* 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;
import android.content.Context;
import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import com.beardedhen.androidbootstrap.BootstrapButton;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.util.Log;
/**
* Import public keys from the Keybase.io directory. First cut: just raw search.
* TODO: make a pick list of the people youre following on keybase
*/
public class ImportKeysKeybaseFragment extends Fragment {
private ImportKeysActivity mImportActivity;
private BootstrapButton mSearchButton;
private EditText mQueryEditText;
public static final String ARG_QUERY = "query";
/**
* Creates new instance of this fragment
*/
public static ImportKeysKeybaseFragment newInstance() {
ImportKeysKeybaseFragment frag = new ImportKeysKeybaseFragment();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
/**
* Inflate the layout for this fragment
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.import_keys_keybase_fragment, container, false);
mQueryEditText = (EditText) view.findViewById(R.id.import_keybase_query);
mSearchButton = (BootstrapButton) view.findViewById(R.id.import_keybase_search);
mSearchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String query = mQueryEditText.getText().toString();
search(query);
// close keyboard after pressing search
InputMethodManager imm =
(InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mQueryEditText.getWindowToken(), 0);
}
});
mQueryEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
String query = mQueryEditText.getText().toString();
search(query);
// Don't return true to let the keyboard close itself after pressing search
return false;
}
return false;
}
});
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mImportActivity = (ImportKeysActivity) getActivity();
// set displayed values
if (getArguments() != null) {
if (getArguments().containsKey(ARG_QUERY)) {
String query = getArguments().getString(ARG_QUERY);
mQueryEditText.setText(query, TextView.BufferType.EDITABLE);
}
}
}
private void search(String query) {
mImportActivity.loadCallback(null, null, null, null, query);
}
}

View File

@ -34,6 +34,7 @@ import org.sufficientlysecure.keychain.helper.Preferences;
import org.sufficientlysecure.keychain.ui.adapter.AsyncTaskResultWrapper;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListKeybaseLoader;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListLoader;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListServerLoader;
import org.sufficientlysecure.keychain.util.InputData;
@ -60,9 +61,11 @@ public class ImportKeysListFragment extends ListFragment implements
private Uri mDataUri;
private String mServerQuery;
private String mKeyServer;
private String mKeybaseQuery;
private static final int LOADER_ID_BYTES = 0;
private static final int LOADER_ID_SERVER_QUERY = 1;
private static final int LOADER_ID_KEYBASE = 2;
public byte[] getKeyBytes() {
return mKeyBytes;
@ -76,6 +79,10 @@ public class ImportKeysListFragment extends ListFragment implements
return mServerQuery;
}
public String getKeybaseQuery() {
return mKeybaseQuery;
}
public String getKeyServer() {
return mKeyServer;
}
@ -148,6 +155,16 @@ public class ImportKeysListFragment extends ListFragment implements
// give arguments to onCreateLoader()
getLoaderManager().initLoader(LOADER_ID_SERVER_QUERY, null, this);
}
if (mKeybaseQuery != null) {
// Start out with a progress indicator.
setListShown(false);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
// give arguments to onCreateLoader()
getLoaderManager().initLoader(LOADER_ID_KEYBASE, null, this);
}
}
@Override
@ -157,16 +174,18 @@ public class ImportKeysListFragment extends ListFragment implements
// Select checkbox!
// Update underlying data and notify adapter of change. The adapter will
// update the view automatically.
ImportKeysListEntry entry = mAdapter.getItem(position);
entry.setSelected(!entry.isSelected());
mAdapter.notifyDataSetChanged();
}
public void loadNew(byte[] keyBytes, Uri dataUri, String serverQuery, String keyServer) {
public void loadNew(byte[] keyBytes, Uri dataUri, String serverQuery, String keyServer, String keybaseQuery) {
mKeyBytes = keyBytes;
mDataUri = dataUri;
mServerQuery = serverQuery;
mKeyServer = keyServer;
mKeybaseQuery = keybaseQuery;
if (mKeyBytes != null || mDataUri != null) {
// Start out with a progress indicator.
@ -181,6 +200,13 @@ public class ImportKeysListFragment extends ListFragment implements
getLoaderManager().restartLoader(LOADER_ID_SERVER_QUERY, null, this);
}
if (mKeybaseQuery != null) {
// Start out with a progress indicator.
setListShown(false);
getLoaderManager().restartLoader(LOADER_ID_KEYBASE, null, this);
}
}
@Override
@ -194,6 +220,9 @@ public class ImportKeysListFragment extends ListFragment implements
case LOADER_ID_SERVER_QUERY: {
return new ImportKeysListServerLoader(getActivity(), mServerQuery, mKeyServer);
}
case LOADER_ID_KEYBASE: {
return new ImportKeysListKeybaseLoader(getActivity(), mKeybaseQuery);
}
default:
return null;
@ -248,7 +277,7 @@ public class ImportKeysListFragment extends ListFragment implements
if (error == null) {
AppMsg.makeText(
getActivity(), getResources().getQuantityString(R.plurals.keys_found,
mAdapter.getCount(), mAdapter.getCount()),
mAdapter.getCount(), mAdapter.getCount()),
AppMsg.STYLE_INFO
).show();
} else if (error instanceof KeyServer.InsufficientQuery) {
@ -263,6 +292,19 @@ public class ImportKeysListFragment extends ListFragment implements
}
break;
case LOADER_ID_KEYBASE:
if (error == null) {
AppMsg.makeText(
getActivity(), getResources().getQuantityString(R.plurals.keys_found,
mAdapter.getCount(), mAdapter.getCount()),
AppMsg.STYLE_INFO
).show();
} else if (error instanceof KeyServer.QueryException) {
AppMsg.makeText(getActivity(), R.string.error_keyserver_query,
AppMsg.STYLE_ALERT).show();
}
default:
break;
}
@ -279,6 +321,10 @@ public class ImportKeysListFragment extends ListFragment implements
// Clear the data in the adapter.
mAdapter.clear();
break;
case LOADER_ID_KEYBASE:
// Clear the data in the adapter.
mAdapter.clear();
break;
default:
break;
}

View File

@ -117,7 +117,7 @@ public class ImportKeysQrCodeFragment extends Fragment {
// is this a full key encoded as qr code?
if (scannedContent.startsWith("-----BEGIN PGP")) {
mImportActivity.loadCallback(scannedContent.getBytes(), null, null, null);
mImportActivity.loadCallback(scannedContent.getBytes(), null, null, null, null);
return;
}
@ -197,7 +197,7 @@ public class ImportKeysQrCodeFragment extends Fragment {
for (String in : mScannedContent) {
result += in;
}
mImportActivity.loadCallback(result.getBytes(), null, null, null);
mImportActivity.loadCallback(result.getBytes(), null, null, null, null);
}
}

View File

@ -151,7 +151,7 @@ public class ImportKeysServerFragment extends Fragment {
}
private void search(String query, String keyServer) {
mImportActivity.loadCallback(null, null, query, keyServer);
mImportActivity.loadCallback(null, null, query, keyServer, null);
}
}

View File

@ -203,7 +203,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable {
* Constructor for later querying from keyserver
*/
public ImportKeysListEntry() {
// keys from keyserver are always public keys
// keys from keyserver are always public keys; from keybase too
secretKey = false;
// do not select by default
mSelected = false;

View File

@ -0,0 +1,106 @@
/*
* 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.adapter;
import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.KeyServer;
import org.sufficientlysecure.keychain.util.KeybaseKeyServer;
import org.sufficientlysecure.keychain.util.Log;
import java.util.ArrayList;
public class ImportKeysListKeybaseLoader
extends AsyncTaskLoader<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>> {
Context mContext;
String mKeybaseQuery;
private ArrayList<ImportKeysListEntry> mEntryList = new ArrayList<ImportKeysListEntry>();
private AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> mEntryListWrapper;
public ImportKeysListKeybaseLoader(Context context, String keybaseQuery) {
super(context);
mContext = context;
mKeybaseQuery = keybaseQuery;
}
@Override
public AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> loadInBackground() {
mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, null);
if (mKeybaseQuery == null) {
Log.e(Constants.TAG, "mKeybaseQery is null!");
return mEntryListWrapper;
}
queryServer(mKeybaseQuery);
return mEntryListWrapper;
}
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
}
@Override
protected void onStartLoading() {
forceLoad();
}
@Override
protected void onStopLoading() {
cancelLoad();
}
@Override
public void deliverResult(AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>> data) {
super.deliverResult(data);
}
/**
* Query keybase
*/
private void queryServer(String query) {
KeybaseKeyServer server = new KeybaseKeyServer();
try {
ArrayList<ImportKeysListEntry> searchResult = server.search(query);
mEntryList.clear();
mEntryList.addAll(searchResult);
mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, null);
} catch (KeyServer.InsufficientQuery e) {
mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, e);
} catch (KeyServer.QueryException e) {
mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, e);
} catch (KeyServer.TooManyResponses e) {
mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, e);
}
}
}

View File

@ -116,13 +116,10 @@ public class ImportKeysListServerLoader
}
mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, null);
} catch (KeyServer.InsufficientQuery e) {
Log.e(Constants.TAG, "InsufficientQuery", e);
mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, e);
} catch (KeyServer.QueryException e) {
Log.e(Constants.TAG, "QueryException", e);
mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, e);
} catch (KeyServer.TooManyResponses e) {
Log.e(Constants.TAG, "TooManyResponses", e);
mEntryListWrapper = new AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>(mEntryList, e);
}
}

View File

@ -34,7 +34,6 @@ import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.pgp.PgpKeyHelper;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@ -167,21 +166,6 @@ public class HkpKeyServer extends KeyServer {
mPort = port;
}
private static String readAll(InputStream in, String encoding) throws IOException {
ByteArrayOutputStream raw = new ByteArrayOutputStream();
byte buffer[] = new byte[1 << 16];
int n = 0;
while ((n = in.read(buffer)) != -1) {
raw.write(buffer, 0, n);
}
if (encoding == null) {
encoding = "utf8";
}
return raw.toString(encoding);
}
private String query(String request) throws QueryException, HttpError {
InetAddress ips[];
try {

View File

@ -0,0 +1,73 @@
/*
* Copyright (C) 2014 Tim Bray <tbray@textuality.com>
*
* 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.util;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* Minimal hierarchy selector
*/
public class JWalk {
public static int getInt(JSONObject json, String... path) throws JSONException {
json = walk(json, path);
return json.getInt(path[path.length - 1]);
}
public static long getLong(JSONObject json, String... path) throws JSONException {
json = walk(json, path);
return json.getLong(path[path.length - 1]);
}
public static String getString(JSONObject json, String... path) throws JSONException {
json = walk(json, path);
return json.getString(path[path.length - 1]);
}
public static JSONArray getArray(JSONObject json, String... path) throws JSONException {
json = walk(json, path);
return json.getJSONArray(path[path.length - 1]);
}
public static JSONObject optObject(JSONObject json, String... path) throws JSONException {
json = walk(json, path);
return json.optJSONObject(path[path.length - 1]);
}
private static JSONObject walk(JSONObject json, String... path) throws JSONException {
int len = path.length - 1;
int pathIndex = 0;
try {
while (pathIndex < len) {
json = json.getJSONObject(path[pathIndex]);
pathIndex++;
}
} catch (JSONException e) {
// try to give em a nice-looking error
StringBuilder sb = new StringBuilder();
for (int i = 0; i < len; i++) {
sb.append(path[i]).append('.');
}
sb.append(path[len]);
throw new JSONException("JWalk error at step " + pathIndex + " of " + sb);
}
return json;
}
}

View File

@ -20,6 +20,9 @@ package org.sufficientlysecure.keychain.util;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public abstract class KeyServer {
@ -49,4 +52,19 @@ public abstract class KeyServer {
abstract String get(String keyIdHex) throws QueryException;
abstract void add(String armoredKey) throws AddKeyException;
public static String readAll(InputStream in, String encoding) throws IOException {
ByteArrayOutputStream raw = new ByteArrayOutputStream();
byte buffer[] = new byte[1 << 16];
int n = 0;
while ((n = in.read(buffer)) != -1) {
raw.write(buffer, 0, n);
}
if (encoding == null) {
encoding = "utf8";
}
return raw.toString(encoding);
}
}

View File

@ -0,0 +1,161 @@
/*
* Copyright (C) 2012-2014 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2011-2014 Thialfihar <thi@thialfihar.org>
* Copyright (C) 2011 Senecaso
*
* 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.
*/
package org.sufficientlysecure.keychain.util;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.WeakHashMap;
public class KeybaseKeyServer extends KeyServer {
private WeakHashMap<String, String> mKeyCache = new WeakHashMap<String, String>();
@Override
public ArrayList<ImportKeysListEntry> search(String query) throws QueryException, TooManyResponses,
InsufficientQuery {
ArrayList<ImportKeysListEntry> results = new ArrayList<ImportKeysListEntry>();
JSONObject fromQuery = getFromKeybase("_/api/1.0/user/autocomplete.json?q=", query);
try {
JSONArray matches = JWalk.getArray(fromQuery, "completions");
for (int i = 0; i < matches.length(); i++) {
JSONObject match = matches.getJSONObject(i);
// only list them if they have a key
if (JWalk.optObject(match, "components", "key_fingerprint") != null) {
results.add(makeEntry(match));
}
}
} catch (Exception e) {
throw new QueryException("Unexpected structure in keybase search result: " + e.getMessage());
}
return results;
}
private JSONObject getUser(String keybaseID) throws QueryException {
try {
return getFromKeybase("_/api/1.0/user/lookup.json?username=", keybaseID);
} catch (Exception e) {
String detail = "";
if (keybaseID != null) {
detail = ". Query was for user '" + keybaseID + "'";
}
throw new QueryException(e.getMessage() + detail);
}
}
private ImportKeysListEntry makeEntry(JSONObject match) throws QueryException, JSONException {
String keybaseID = JWalk.getString(match, "components", "username", "val");
String key_fingerprint = JWalk.getString(match, "components", "key_fingerprint", "val");
key_fingerprint = key_fingerprint.replace(" ", "").toUpperCase();
match = getUser(keybaseID);
final ImportKeysListEntry entry = new ImportKeysListEntry();
// TODO: Fix; have suggested keybase provide this value to avoid search-time crypto calls
entry.setBitStrength(4096);
entry.setAlgorithm("RSA");
entry.setKeyIdHex("0x" + key_fingerprint);
entry.setRevoked(false);
// ctime
final long creationDate = JWalk.getLong(match, "them", "public_keys", "primary", "ctime");
final GregorianCalendar tmpGreg = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
tmpGreg.setTimeInMillis(creationDate * 1000);
entry.setDate(tmpGreg.getTime());
// key bits
// we have to fetch the user object to construct the search-result list, so we might as
// well (weakly) remember the key, in case they try to import it
mKeyCache.put(keybaseID, JWalk.getString(match,"them", "public_keys", "primary", "bundle"));
// String displayName = JWalk.getString(match, "them", "profile", "full_name");
ArrayList<String> userIds = new ArrayList<String>();
String name = "keybase.io/" + keybaseID + " <" + keybaseID + "@keybase.io>";
userIds.add(name);
userIds.add(keybaseID);
entry.setUserIds(userIds);
entry.setPrimaryUserId(name);
return entry;
}
private JSONObject getFromKeybase(String path, String query) throws QueryException {
try {
String url = "https://keybase.io/" + path + URLEncoder.encode(query, "utf8");
Log.d(Constants.TAG, "keybase query: " + url);
URL realUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
conn.setConnectTimeout(5000); // TODO: Reasonable values for keybase
conn.setReadTimeout(25000);
conn.connect();
int response = conn.getResponseCode();
if (response >= 200 && response < 300) {
String text = readAll(conn.getInputStream(), conn.getContentEncoding());
try {
JSONObject json = new JSONObject(text);
if (JWalk.getInt(json, "status", "code") != 0) {
throw new QueryException("Keybase autocomplete search failed");
}
return json;
} catch (JSONException e) {
throw new QueryException("Keybase.io query returned broken JSON");
}
} else {
String message = readAll(conn.getErrorStream(), conn.getContentEncoding());
throw new QueryException("Keybase.io query error (status=" + response +
"): " + message);
}
} catch (Exception e) {
throw new QueryException("Keybase.io query error");
}
}
@Override
public String get(String id) throws QueryException {
String key = mKeyCache.get(id);
if (key == null) {
try {
JSONObject user = getUser(id);
key = JWalk.getString(user, "them", "public_keys", "primary", "bundle");
} catch (Exception e) {
throw new QueryException(e.getMessage());
}
}
return key;
}
@Override
public void add(String armoredKey) throws AddKeyException {
throw new AddKeyException();
}
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bootstrapbutton="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/import_keybase_query"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="top|left"
android:hint="@string/hint_keybase_search"
android:imeOptions="actionSearch"
android:inputType="textNoSuggestions"
android:singleLine="true"
android:lines="1"
android:maxLines="1"
android:minLines="1"
android:layout_gravity="center_vertical" />
<com.beardedhen.androidbootstrap.BootstrapButton
android:id="@+id/import_keybase_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
bootstrapbutton:bb_icon_left="fa-search"
bootstrapbutton:bb_roundedCorners="true"
bootstrapbutton:bb_size="default"
bootstrapbutton:bb_type="default" />
</LinearLayout>
<!--
<com.beardedhen.androidbootstrap.BootstrapButton
android:id="@+id/import_keybase_button"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_margin="10dp"
android:text="@string/import_keybase_button"
bootstrapbutton:bb_size="default"
bootstrapbutton:bb_type="default" />
-->
</LinearLayout>

View File

@ -54,6 +54,7 @@
<item>@string/menu_import_from_qr_code</item>
<item>@string/import_from_clipboard</item>
<item>@string/menu_import_from_nfc</item>
<item>@string/menu_import_from_keybase</item>
</string-array>
</resources>

View File

@ -79,6 +79,7 @@
<string name="menu_create_key_expert">Create key (expert)</string>
<string name="menu_search">Search</string>
<string name="menu_import_from_key_server">Keyserver</string>
<string name="menu_import_from_keybase">Import from Keybase.io</string>
<string name="menu_key_server">Keyserver…</string>
<string name="menu_update_key">Update from keyserver</string>
<string name="menu_export_key_to_server">Upload to key server</string>
@ -353,6 +354,7 @@
<string name="hint_public_keys">Search Public Keys</string>
<string name="hint_secret_keys">Search Secret Keys</string>
<string name="action_share_key_with">Share Key with…</string>
<string name="hint_keybase_search">Search Keybase.io</string>
<!-- key bit length selections -->
<string name="key_size_512">512</string>
@ -398,6 +400,7 @@
<string name="import_nfc_text">To receive keys via NFC, the device needs to be unlocked.</string>
<string name="import_nfc_help_button">Help</string>
<string name="import_clipboard_button">Get key from clipboard</string>
<string name="import_keybase_button">Get key from Keybase.io</string>
<!-- Intent labels -->
<string name="intent_decrypt_file">Decrypt File with OpenKeychain</string>