Merge remote-tracking branch 'origin/master' into encrypted-export
This commit is contained in:
@@ -91,14 +91,16 @@ public class KeychainApplication extends Application {
|
||||
}
|
||||
|
||||
brandGlowEffect(getApplicationContext(),
|
||||
FormattingUtils.getColorFromAttr(getApplicationContext(), R.attr.colorPrimary));
|
||||
FormattingUtils.getColorFromAttr(getApplicationContext(), R.attr.colorPrimary));
|
||||
|
||||
setupAccountAsNeeded(this);
|
||||
|
||||
// Update keyserver list as needed
|
||||
Preferences.getPreferences(this).upgradePreferences(this);
|
||||
|
||||
TlsHelper.addStaticCA("pool.sks-keyservers.net", getAssets(), "sks-keyservers.netCA.cer");
|
||||
TlsHelper.addPinnedCertificate("hkps.pool.sks-keyservers.net", getAssets(), "hkps.pool.sks-keyservers.net.CA.cer");
|
||||
TlsHelper.addPinnedCertificate("pgp.mit.edu", getAssets(), "pgp.mit.edu.cer");
|
||||
TlsHelper.addPinnedCertificate("api.keybase.io", getAssets(), "api.keybase.io.CA.cer");
|
||||
|
||||
TemporaryStorageProvider.cleanUp(this);
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) Andreas Jakl
|
||||
*
|
||||
* 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.experimental;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* The BitInputStream allows reading individual bits from a
|
||||
* general Java InputStream.
|
||||
* Like the various Stream-classes from Java, the BitInputStream
|
||||
* has to be created based on another Input stream. It provides
|
||||
* a function to read the next bit from the sream, as well as to read multiple
|
||||
* bits at once and write the resulting data into an integer value.
|
||||
* <p/>
|
||||
* source: http://developer.nokia.com/Community/Wiki/Bit_Input/Output_Stream_utility_classes_for_efficient_data_transfer
|
||||
*/
|
||||
public class BitInputStream {
|
||||
/**
|
||||
* The Java InputStream this class is working on.
|
||||
*/
|
||||
private InputStream iIs;
|
||||
|
||||
/**
|
||||
* The buffer containing the currently processed
|
||||
* byte of the input stream.
|
||||
*/
|
||||
private int iBuffer;
|
||||
|
||||
/**
|
||||
* Next bit of the current byte value that the user will
|
||||
* get. If it's 8, the next bit will be read from the
|
||||
* next byte of the InputStream.
|
||||
*/
|
||||
private int iNextBit = 8;
|
||||
|
||||
/**
|
||||
* Create a new bit input stream based on an existing Java InputStream.
|
||||
*
|
||||
* @param aIs the input stream this class should read the bits from.
|
||||
*/
|
||||
public BitInputStream(InputStream aIs) {
|
||||
iIs = aIs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a specified number of bits and return them combined as
|
||||
* an integer value. The bits are written to the integer
|
||||
* starting at the highest bit ( << aNumberOfBits ), going down
|
||||
* to the lowest bit ( << 0 )
|
||||
*
|
||||
* @param aNumberOfBits defines how many bits to read from the stream.
|
||||
* @return integer value containing the bits read from the stream.
|
||||
* @throws IOException
|
||||
*/
|
||||
synchronized public int readBits(final int aNumberOfBits)
|
||||
throws IOException {
|
||||
int value = 0;
|
||||
for (int i = aNumberOfBits - 1; i >= 0; i--) {
|
||||
value |= (readBit() << i);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
synchronized public int available() {
|
||||
try {
|
||||
return (8 - iNextBit) + iIs.available() * 8; // bytestream to bitstream available
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the next bit from the stream.
|
||||
*
|
||||
* @return 0 if the bit is 0, 1 if the bit is 1.
|
||||
* @throws IOException
|
||||
*/
|
||||
synchronized public int readBit() throws IOException {
|
||||
if (iIs == null)
|
||||
throw new IOException("Already closed");
|
||||
|
||||
if (iNextBit == 8) {
|
||||
iBuffer = iIs.read();
|
||||
|
||||
if (iBuffer == -1)
|
||||
throw new EOFException();
|
||||
|
||||
iNextBit = 0;
|
||||
}
|
||||
|
||||
int bit = iBuffer & (1 << iNextBit);
|
||||
iNextBit++;
|
||||
|
||||
bit = (bit == 0) ? 0 : 1;
|
||||
|
||||
return bit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the underlying input stream.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
iIs.close();
|
||||
iIs = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2014 Jake McGinty (Open Whisper Systems)
|
||||
*
|
||||
* 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.experimental;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* From https://github.com/mcginty/TextSecure/tree/mnemonic-poem
|
||||
*/
|
||||
public class SentenceConfirm {
|
||||
Context context;
|
||||
List<String> n, vi, vt, adj, adv, p, art;
|
||||
|
||||
public SentenceConfirm(Context context) {
|
||||
this.context = context;
|
||||
try {
|
||||
n = readFile(R.raw.fp_sentence_nouns);
|
||||
vi = readFile(R.raw.fp_sentence_verbs_i);
|
||||
vt = readFile(R.raw.fp_sentence_verbs_t);
|
||||
adj = readFile(R.raw.fp_sentence_adjectives);
|
||||
adv = readFile(R.raw.fp_sentence_adverbs);
|
||||
p = readFile(R.raw.fp_sentence_prepositions);
|
||||
art = readFile(R.raw.fp_sentence_articles);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Reading sentence files failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> readFile(int resId) throws IOException {
|
||||
if (context.getApplicationContext() == null) {
|
||||
throw new AssertionError("app context can't be null");
|
||||
}
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(
|
||||
context.getApplicationContext()
|
||||
.getResources()
|
||||
.openRawResource(resId)));
|
||||
List<String> words = new ArrayList<>();
|
||||
String word = in.readLine();
|
||||
while (word != null) {
|
||||
words.add(word);
|
||||
word = in.readLine();
|
||||
}
|
||||
in.close();
|
||||
return words;
|
||||
}
|
||||
|
||||
public String fromBytes(final byte[] bytes, int desiredBytes) throws IOException {
|
||||
BitInputStream bin = new BitInputStream(new ByteArrayInputStream(bytes));
|
||||
EntropyString fingerprint = new EntropyString();
|
||||
|
||||
while (fingerprint.getBits() < (desiredBytes * 8)) {
|
||||
if (!fingerprint.isEmpty()) {
|
||||
fingerprint.append("\n\n");
|
||||
}
|
||||
try {
|
||||
fingerprint.append(getSentence(bin));
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "IOException when creating the sentence");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return fingerprint.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab a word for a list of them using the necessary bits to choose from a BitInputStream
|
||||
*
|
||||
* @param words the list of words to select from
|
||||
* @param bin the bit input stream to encode from
|
||||
* @return A Pair of the word and the number of bits consumed from the stream
|
||||
*/
|
||||
private EntropyString getWord(List<String> words, BitInputStream bin) throws IOException {
|
||||
final int neededBits = log(words.size(), 2);
|
||||
Log.d(Constants.TAG, "need " + neededBits + " bits of entropy");
|
||||
int bits = bin.readBits(neededBits);
|
||||
Log.d(Constants.TAG, "got word " + words.get(bits) + " with " + neededBits + " bits of entropy");
|
||||
return new EntropyString(words.get(bits), neededBits);
|
||||
}
|
||||
|
||||
private EntropyString getNounPhrase(BitInputStream bits) throws IOException {
|
||||
final EntropyString phrase = new EntropyString();
|
||||
phrase.append(getWord(art, bits)).append(" ");
|
||||
if (bits.readBit() != 0) {
|
||||
phrase.append(getWord(adj, bits)).append(" ");
|
||||
}
|
||||
phrase.incBits();
|
||||
|
||||
phrase.append(getWord(n, bits));
|
||||
Log.d(Constants.TAG, "got phrase " + phrase + " with " + phrase.getBits() + " bits of entropy");
|
||||
return phrase;
|
||||
}
|
||||
|
||||
EntropyString getSentence(BitInputStream bits) throws IOException {
|
||||
final EntropyString sentence = new EntropyString();
|
||||
sentence.append(getNounPhrase(bits)); // Subject
|
||||
if (bits.readBit() != 0) {
|
||||
sentence.append(" ").append(getWord(vt, bits)); // Transitive verb
|
||||
sentence.append(" ").append(getNounPhrase(bits)); // Object of transitive verb
|
||||
} else {
|
||||
sentence.append(" ").append(getWord(vi, bits)); // Intransitive verb
|
||||
}
|
||||
sentence.incBits();
|
||||
|
||||
if (bits.readBit() != 0) {
|
||||
sentence.append(" ").append(getWord(adv, bits)); // Adverb
|
||||
}
|
||||
|
||||
sentence.incBits();
|
||||
if (bits.readBit() != 0) {
|
||||
sentence.append(" ").append(getWord(p, bits)); // Preposition
|
||||
sentence.append(" ").append(getNounPhrase(bits)); // Object of preposition
|
||||
}
|
||||
sentence.incBits();
|
||||
Log.d(Constants.TAG, "got sentence " + sentence + " with " + sentence.getBits() + " bits of entropy");
|
||||
|
||||
// uppercase first character, end with dot (without increasing the bits)
|
||||
sentence.getBuilder().replace(0, 1,
|
||||
Character.toString(Character.toUpperCase(sentence.getBuilder().charAt(0))));
|
||||
sentence.getBuilder().append(".");
|
||||
|
||||
return sentence;
|
||||
}
|
||||
|
||||
public static class EntropyString {
|
||||
private StringBuilder builder;
|
||||
private int bits;
|
||||
|
||||
public EntropyString(String phrase, int bits) {
|
||||
this.builder = new StringBuilder(phrase);
|
||||
this.bits = bits;
|
||||
}
|
||||
|
||||
public EntropyString() {
|
||||
this("", 0);
|
||||
}
|
||||
|
||||
public StringBuilder getBuilder() {
|
||||
return builder;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return builder.length() == 0;
|
||||
}
|
||||
|
||||
public EntropyString append(EntropyString phrase) {
|
||||
builder.append(phrase);
|
||||
bits += phrase.getBits();
|
||||
return this;
|
||||
}
|
||||
|
||||
public EntropyString append(String string) {
|
||||
builder.append(string);
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getBits() {
|
||||
return bits;
|
||||
}
|
||||
|
||||
public void setBits(int bits) {
|
||||
this.bits = bits;
|
||||
}
|
||||
|
||||
public void incBits() {
|
||||
bits += 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static int log(int x, int base) {
|
||||
return (int) (Math.log(x) / Math.log(base));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,12 +15,13 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui.util;
|
||||
package org.sufficientlysecure.keychain.experimental;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.spongycastle.util.Arrays;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -29,7 +30,7 @@ import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
|
||||
public class ExperimentalWordConfirm {
|
||||
public class WordConfirm {
|
||||
|
||||
public static String getWords(Context context, byte[] fingerprintBlob) {
|
||||
ArrayList<String> words = new ArrayList<>();
|
||||
@@ -37,7 +38,7 @@ public class ExperimentalWordConfirm {
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
reader = new BufferedReader(new InputStreamReader(
|
||||
context.getAssets().open("word_confirm_list.txt"),
|
||||
context.getResources().openRawResource(R.raw.fp_word_list),
|
||||
"UTF-8"
|
||||
));
|
||||
|
||||
@@ -77,7 +77,7 @@ public class CloudSearch {
|
||||
// kill threads that haven't returned yet
|
||||
thread.interrupt();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.RequestBody;
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpHelper;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
@@ -196,19 +197,23 @@ public class HkpKeyserver extends Keyserver {
|
||||
/**
|
||||
* returns a client with pinned certificate if necessary
|
||||
*
|
||||
* @param url url to be queried by client
|
||||
* @param url url to be queried by client
|
||||
* @param proxy proxy to be used by client
|
||||
* @return client with a pinned certificate if necesary
|
||||
* @return client with a pinned certificate if necessary
|
||||
*/
|
||||
public static OkHttpClient getClient(URL url, Proxy proxy) throws IOException {
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
|
||||
try {
|
||||
TlsHelper.pinCertificateIfNecessary(client, url);
|
||||
TlsHelper.usePinnedCertificateIfAvailable(client, url);
|
||||
} catch (TlsHelper.TlsHelperException e) {
|
||||
Log.w(Constants.TAG, e);
|
||||
}
|
||||
|
||||
// don't follow any redirects
|
||||
client.setFollowRedirects(false);
|
||||
client.setFollowSslRedirects(false);
|
||||
|
||||
if (proxy != null) {
|
||||
client.setProxy(proxy);
|
||||
client.setConnectTimeout(30000, TimeUnit.MILLISECONDS);
|
||||
@@ -228,7 +233,7 @@ public class HkpKeyserver extends Keyserver {
|
||||
OkHttpClient client = getClient(url, proxy);
|
||||
Response response = client.newCall(new Request.Builder().url(url).build()).execute();
|
||||
|
||||
String responseBody = response.body().string();// contains body both in case of success or failure
|
||||
String responseBody = response.body().string(); // contains body both in case of success or failure
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
return responseBody;
|
||||
@@ -238,17 +243,12 @@ public class HkpKeyserver extends Keyserver {
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "IOException at HkpKeyserver", e);
|
||||
throw new QueryFailedException("Keyserver '" + mHost + "' is unavailable. Check your Internet connection!" +
|
||||
proxy == null?"":" Using proxy " + proxy);
|
||||
(proxy == null ? "" : " Using proxy " + proxy));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Results are sorted by creation date of key!
|
||||
*
|
||||
* @param query
|
||||
* @return
|
||||
* @throws QueryFailedException
|
||||
* @throws QueryNeedsRepairException
|
||||
*/
|
||||
@Override
|
||||
public ArrayList<ImportKeysListEntry> search(String query, Proxy proxy) throws QueryFailedException,
|
||||
@@ -299,30 +299,46 @@ public class HkpKeyserver extends Keyserver {
|
||||
entry.setQuery(query);
|
||||
entry.addOrigin(getUrlPrefix() + mHost + ":" + mPort);
|
||||
|
||||
int bitSize = Integer.parseInt(matcher.group(3));
|
||||
entry.setBitStrength(bitSize);
|
||||
int algorithmId = Integer.decode(matcher.group(2));
|
||||
entry.setAlgorithm(KeyFormattingUtils.getAlgorithmInfo(algorithmId, bitSize, null));
|
||||
|
||||
// group 1 contains the full fingerprint (v4) or the long key id if available
|
||||
// see https://bitbucket.org/skskeyserver/sks-keyserver/pull-request/12/fixes-for-machine-readable-indexes/diff
|
||||
String fingerprintOrKeyId = matcher.group(1).toLowerCase(Locale.ENGLISH);
|
||||
if (fingerprintOrKeyId.length() > 16) {
|
||||
if (fingerprintOrKeyId.length() == 40) {
|
||||
entry.setFingerprintHex(fingerprintOrKeyId);
|
||||
entry.setKeyIdHex("0x" + fingerprintOrKeyId.substring(fingerprintOrKeyId.length()
|
||||
- 16, fingerprintOrKeyId.length()));
|
||||
} else {
|
||||
} else if (fingerprintOrKeyId.length() == 16) {
|
||||
// set key id only
|
||||
entry.setKeyIdHex("0x" + fingerprintOrKeyId);
|
||||
} else {
|
||||
Log.e(Constants.TAG, "Wrong length for fingerprint/long key id.");
|
||||
// skip this key
|
||||
continue;
|
||||
}
|
||||
|
||||
final long creationDate = Long.parseLong(matcher.group(4));
|
||||
final GregorianCalendar tmpGreg = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
|
||||
tmpGreg.setTimeInMillis(creationDate * 1000);
|
||||
entry.setDate(tmpGreg.getTime());
|
||||
try {
|
||||
int bitSize = Integer.parseInt(matcher.group(3));
|
||||
entry.setBitStrength(bitSize);
|
||||
int algorithmId = Integer.decode(matcher.group(2));
|
||||
entry.setAlgorithm(KeyFormattingUtils.getAlgorithmInfo(algorithmId, bitSize, null));
|
||||
|
||||
entry.setRevoked(matcher.group(6).contains("r"));
|
||||
entry.setExpired(matcher.group(6).contains("e"));
|
||||
final long creationDate = Long.parseLong(matcher.group(4));
|
||||
final GregorianCalendar tmpGreg = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
|
||||
tmpGreg.setTimeInMillis(creationDate * 1000);
|
||||
entry.setDate(tmpGreg.getTime());
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(Constants.TAG, "Conversation for bit size, algorithm, or creation date failed.", e);
|
||||
// skip this key
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
entry.setRevoked(matcher.group(6).contains("r"));
|
||||
entry.setExpired(matcher.group(6).contains("e"));
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(Constants.TAG, "Check for revocation or expiry failed.", e);
|
||||
// skip this key
|
||||
continue;
|
||||
}
|
||||
|
||||
ArrayList<String> userIds = new ArrayList<>();
|
||||
final String uidLines = matcher.group(7);
|
||||
@@ -340,6 +356,10 @@ public class HkpKeyserver extends Keyserver {
|
||||
tmp = URLDecoder.decode(tmp, "UTF8");
|
||||
} catch (UnsupportedEncodingException ignored) {
|
||||
// will never happen, because "UTF8" is supported
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(Constants.TAG, "User ID encoding broken", e);
|
||||
// skip this user id
|
||||
continue;
|
||||
}
|
||||
}
|
||||
userIds.add(tmp);
|
||||
@@ -363,11 +383,14 @@ public class HkpKeyserver extends Keyserver {
|
||||
Log.d(Constants.TAG, "Failed to get key at HkpKeyserver", httpError);
|
||||
throw new QueryFailedException("not found");
|
||||
}
|
||||
if (data == null) {
|
||||
throw new QueryFailedException("data is null");
|
||||
}
|
||||
Matcher matcher = PgpHelper.PGP_PUBLIC_KEY.matcher(data);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return null;
|
||||
throw new QueryFailedException("data is null");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -418,7 +441,7 @@ public class HkpKeyserver extends Keyserver {
|
||||
* Tries to find a server responsible for a given domain
|
||||
*
|
||||
* @return A responsible Keyserver or null if not found.
|
||||
* TODO: PHILIP Add proxy functionality
|
||||
* TODO: Add proxy functionality
|
||||
*/
|
||||
public static HkpKeyserver resolve(String domain) {
|
||||
try {
|
||||
|
||||
@@ -19,12 +19,13 @@ package org.sufficientlysecure.keychain.keyimport;
|
||||
|
||||
import com.textuality.keybase.lib.KeybaseException;
|
||||
import com.textuality.keybase.lib.Match;
|
||||
import com.textuality.keybase.lib.Search;
|
||||
import com.textuality.keybase.lib.KeybaseQuery;
|
||||
import com.textuality.keybase.lib.User;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.OkHttpKeybaseClient;
|
||||
|
||||
import java.net.Proxy;
|
||||
import java.util.ArrayList;
|
||||
@@ -49,7 +50,9 @@ public class KeybaseKeyserver extends Keyserver {
|
||||
mQuery = query;
|
||||
|
||||
try {
|
||||
Iterable<Match> matches = Search.search(query, proxy);
|
||||
KeybaseQuery keybaseQuery = new KeybaseQuery(new OkHttpKeybaseClient());
|
||||
keybaseQuery.setProxy(proxy);
|
||||
Iterable<Match> matches = keybaseQuery.search(query);
|
||||
for (Match match : matches) {
|
||||
results.add(makeEntry(match));
|
||||
}
|
||||
@@ -101,7 +104,9 @@ public class KeybaseKeyserver extends Keyserver {
|
||||
@Override
|
||||
public String get(String id, Proxy proxy) throws QueryFailedException {
|
||||
try {
|
||||
return User.keyForUsername(id, proxy);
|
||||
KeybaseQuery keybaseQuery = new KeybaseQuery(new OkHttpKeybaseClient());
|
||||
keybaseQuery.setProxy(proxy);
|
||||
return User.keyForUsername(keybaseQuery, id);
|
||||
} catch (KeybaseException e) {
|
||||
throw new QueryFailedException(e.getMessage());
|
||||
}
|
||||
|
||||
@@ -62,15 +62,15 @@ public abstract class Keyserver {
|
||||
* query too short _or_ too many responses
|
||||
*/
|
||||
public static class QueryTooShortOrTooManyResponsesException extends QueryNeedsRepairException {
|
||||
private static final long serialVersionUID = 2703768928624654514L;
|
||||
private static final long serialVersionUID = 2703768928624654518L;
|
||||
}
|
||||
|
||||
public static class AddKeyException extends Exception {
|
||||
private static final long serialVersionUID = -507574859137295530L;
|
||||
}
|
||||
|
||||
public abstract List<ImportKeysListEntry> search(String query, Proxy proxy) throws QueryFailedException,
|
||||
QueryNeedsRepairException;
|
||||
public abstract List<ImportKeysListEntry> search(String query, Proxy proxy)
|
||||
throws QueryFailedException, QueryNeedsRepairException;
|
||||
|
||||
public abstract String get(String keyIdHex, Proxy proxy) throws QueryFailedException;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.ArrayList;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.apache.james.mime4j.MimeException;
|
||||
import org.apache.james.mime4j.codec.DecodeMonitor;
|
||||
@@ -86,6 +87,11 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
|
||||
DecryptVerifyResult decryptResult = null;
|
||||
|
||||
PgpDecryptVerifyInputParcel decryptInput = input.getDecryptInput();
|
||||
|
||||
if (!input.getMimeDecode() && decryptInput == null) {
|
||||
throw new AssertionError("no decryption or mime decoding, this is probably a bug");
|
||||
}
|
||||
|
||||
if (decryptInput != null) {
|
||||
|
||||
log.add(LogType.MSG_DATA_OPENPGP, 1);
|
||||
@@ -109,16 +115,33 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
|
||||
return new InputDataResult(InputDataResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
// inform the storage provider about the mime type for this uri
|
||||
if (decryptResult.getDecryptionMetadata() != null) {
|
||||
TemporaryStorageProvider.setMimeType(mContext, currentInputUri,
|
||||
decryptResult.getDecryptionMetadata().getMimeType());
|
||||
}
|
||||
|
||||
} else {
|
||||
currentInputUri = input.getInputUri();
|
||||
}
|
||||
|
||||
// If we aren't supposed to attempt mime decode, we are done here
|
||||
if (!input.getMimeDecode()) {
|
||||
|
||||
if (decryptInput == null) {
|
||||
throw new AssertionError("no decryption or mime decoding, this is probably a bug");
|
||||
// don't even attempt if we know the data isn't suitable for mime content, or if we have a filename
|
||||
boolean skipMimeParsing = false;
|
||||
if (decryptResult != null && decryptResult.getDecryptionMetadata() != null) {
|
||||
OpenPgpMetadata metadata = decryptResult.getDecryptionMetadata();
|
||||
String fileName = metadata.getFilename();
|
||||
String contentType = metadata.getMimeType();
|
||||
if (!TextUtils.isEmpty(fileName)
|
||||
|| contentType != null
|
||||
&& !contentType.startsWith("multipart/")
|
||||
&& !contentType.startsWith("text/")
|
||||
&& !contentType.startsWith("application/")) {
|
||||
skipMimeParsing = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we aren't supposed to attempt mime decode after decryption, we are done here
|
||||
if (skipMimeParsing || !input.getMimeDecode()) {
|
||||
|
||||
log.add(LogType.MSG_DATA_SKIP_MIME, 1);
|
||||
|
||||
@@ -309,25 +332,32 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
|
||||
|
||||
log.add(LogType.MSG_DATA_MIME, 1);
|
||||
|
||||
// open current uri for input
|
||||
InputStream in = mContext.getContentResolver().openInputStream(currentInputUri);
|
||||
parser.parse(in);
|
||||
try {
|
||||
|
||||
if (mSignedDataUri != null) {
|
||||
|
||||
if (decryptResult != null) {
|
||||
decryptResult.setSignatureResult(mSignedDataResult.getSignatureResult());
|
||||
} else {
|
||||
decryptResult = mSignedDataResult;
|
||||
}
|
||||
|
||||
// the actual content is the signed data now (and will be passed verbatim, if parsing fails)
|
||||
currentInputUri = mSignedDataUri;
|
||||
in = mContext.getContentResolver().openInputStream(currentInputUri);
|
||||
// reset signed data result, to indicate to the parser that it is in the inner part
|
||||
mSignedDataResult = null;
|
||||
// open current uri for input
|
||||
InputStream in = mContext.getContentResolver().openInputStream(currentInputUri);
|
||||
parser.parse(in);
|
||||
|
||||
if (mSignedDataUri != null) {
|
||||
|
||||
if (decryptResult != null) {
|
||||
decryptResult.setSignatureResult(mSignedDataResult.getSignatureResult());
|
||||
} else {
|
||||
decryptResult = mSignedDataResult;
|
||||
}
|
||||
|
||||
// the actual content is the signed data now (and will be passed verbatim, if parsing fails)
|
||||
currentInputUri = mSignedDataUri;
|
||||
in = mContext.getContentResolver().openInputStream(currentInputUri);
|
||||
// reset signed data result, to indicate to the parser that it is in the inner part
|
||||
mSignedDataResult = null;
|
||||
parser.parse(in);
|
||||
|
||||
}
|
||||
} catch (MimeException e) {
|
||||
// a mime error likely means that this wasn't mime data, after all
|
||||
e.printStackTrace();
|
||||
log.add(LogType.MSG_DATA_MIME_BAD, 2);
|
||||
}
|
||||
|
||||
// if we found data, return success
|
||||
@@ -363,10 +393,6 @@ public class InputDataOperation extends BaseOperation<InputDataParcel> {
|
||||
e.printStackTrace();
|
||||
log.add(LogType.MSG_DATA_ERROR_IO, 2);
|
||||
return new InputDataResult(InputDataResult.RESULT_ERROR, log);
|
||||
} catch (MimeException e) {
|
||||
e.printStackTrace();
|
||||
log.add(LogType.MSG_DATA_MIME_ERROR, 2);
|
||||
return new InputDataResult(InputDataResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,39 +20,43 @@
|
||||
package org.sufficientlysecure.keychain.operations;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.textuality.keybase.lib.KeybaseQuery;
|
||||
import com.textuality.keybase.lib.Proof;
|
||||
import com.textuality.keybase.lib.prover.Prover;
|
||||
import de.measite.minidns.Client;
|
||||
import de.measite.minidns.DNSMessage;
|
||||
import de.measite.minidns.Question;
|
||||
import de.measite.minidns.Record;
|
||||
import de.measite.minidns.record.Data;
|
||||
import de.measite.minidns.record.TXT;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.spongycastle.openpgp.PGPUtil;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.KeybaseVerificationResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyInputParcel;
|
||||
import org.sufficientlysecure.keychain.pgp.PgpDecryptVerifyOperation;
|
||||
import org.sufficientlysecure.keychain.pgp.Progressable;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.service.KeybaseVerificationParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.util.OkHttpKeybaseClient;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.measite.minidns.Client;
|
||||
import de.measite.minidns.DNSMessage;
|
||||
import de.measite.minidns.Question;
|
||||
import de.measite.minidns.Record;
|
||||
import de.measite.minidns.record.Data;
|
||||
import de.measite.minidns.record.TXT;
|
||||
|
||||
public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificationParcel> {
|
||||
|
||||
public KeybaseVerificationOperation(Context context, ProviderHelper providerHelper,
|
||||
@@ -83,6 +87,9 @@ public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificat
|
||||
log.add(OperationResult.LogType.MSG_KEYBASE_VERIFICATION, 0, requiredFingerprint);
|
||||
|
||||
try {
|
||||
KeybaseQuery keybaseQuery = new KeybaseQuery(new OkHttpKeybaseClient());
|
||||
keybaseQuery.setProxy(proxy);
|
||||
|
||||
String keybaseProof = keybaseInput.mKeybaseProof;
|
||||
Proof proof = new Proof(new JSONObject(keybaseProof));
|
||||
mProgressable.setProgress(R.string.keybase_message_fetching_data, 0, 100);
|
||||
@@ -95,7 +102,7 @@ public class KeybaseVerificationOperation extends BaseOperation<KeybaseVerificat
|
||||
return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
if (!prover.fetchProofData(proxy)) {
|
||||
if (!prover.fetchProofData(keybaseQuery)) {
|
||||
log.add(OperationResult.LogType.MSG_KEYBASE_ERROR_FETCH_PROOF, 1);
|
||||
return new KeybaseVerificationResult(OperationResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
@@ -474,6 +474,7 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_KC_UID_BAD (LogLevel.WARN, R.string.msg_kc_uid_bad),
|
||||
MSG_KC_UID_CERT_DUP (LogLevel.DEBUG, R.string.msg_kc_uid_cert_dup),
|
||||
MSG_KC_UID_DUP (LogLevel.DEBUG, R.string.msg_kc_uid_dup),
|
||||
MSG_KC_UID_TOO_MANY (LogLevel.DEBUG, R.string.msg_kc_uid_too_many),
|
||||
MSG_KC_UID_FOREIGN (LogLevel.DEBUG, R.string.msg_kc_uid_foreign),
|
||||
MSG_KC_UID_NO_CERT (LogLevel.DEBUG, R.string.msg_kc_uid_no_cert),
|
||||
MSG_KC_UID_REVOKE_DUP (LogLevel.DEBUG, R.string.msg_kc_uid_revoke_dup),
|
||||
@@ -832,7 +833,7 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_DATA_DETACHED_NESTED(LogLevel.WARN, R.string.msg_data_detached_nested),
|
||||
MSG_DATA_DETACHED_TRAILING (LogLevel.WARN, R.string.msg_data_detached_trailing),
|
||||
MSG_DATA_DETACHED_UNSUPPORTED (LogLevel.WARN, R.string.msg_data_detached_unsupported),
|
||||
MSG_DATA_MIME_ERROR (LogLevel.ERROR, R.string.msg_data_mime_error),
|
||||
MSG_DATA_MIME_BAD(LogLevel.INFO, R.string.msg_data_mime_bad),
|
||||
MSG_DATA_MIME_FILENAME (LogLevel.DEBUG, R.string.msg_data_mime_filename),
|
||||
MSG_DATA_MIME_LENGTH (LogLevel.DEBUG, R.string.msg_data_mime_length),
|
||||
MSG_DATA_MIME (LogLevel.DEBUG, R.string.msg_data_mime),
|
||||
|
||||
@@ -52,13 +52,13 @@ import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.Constants.key;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.BaseOperation;
|
||||
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.operations.results.DecryptVerifyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
|
||||
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
@@ -512,8 +512,9 @@ public class PgpDecryptVerifyOperation extends BaseOperation<PgpDecryptVerifyInp
|
||||
PGPLiteralData literalData = (PGPLiteralData) dataChunk;
|
||||
|
||||
String originalFilename = literalData.getFileName();
|
||||
// reject filenames with slashes completely (path traversal issue)
|
||||
if (originalFilename.contains("/")) {
|
||||
originalFilename = originalFilename.substring(originalFilename.lastIndexOf('/'));
|
||||
originalFilename = "";
|
||||
}
|
||||
String mimeType = null;
|
||||
if (literalData.getFormat() == PGPLiteralData.TEXT
|
||||
|
||||
@@ -456,11 +456,15 @@ public class UncachedKeyRing {
|
||||
|
||||
// check for duplicate user ids
|
||||
if (processedUserIds.contains(userId)) {
|
||||
log.add(LogType.MSG_KC_UID_DUP,
|
||||
indent, userId);
|
||||
log.add(LogType.MSG_KC_UID_DUP, indent, userId);
|
||||
// strip out the first found user id with this name
|
||||
modified = PGPPublicKey.removeCertification(modified, rawUserId);
|
||||
}
|
||||
if (processedUserIds.size() > 100) {
|
||||
log.add(LogType.MSG_KC_UID_TOO_MANY, indent, userId);
|
||||
// strip out the user id
|
||||
modified = PGPPublicKey.removeCertification(modified, rawUserId);
|
||||
}
|
||||
processedUserIds.add(userId);
|
||||
|
||||
PGPSignature selfCert = null;
|
||||
|
||||
@@ -54,7 +54,7 @@ import java.io.IOException;
|
||||
*/
|
||||
public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
private static final String DATABASE_NAME = "openkeychain.db";
|
||||
private static final int DATABASE_VERSION = 12;
|
||||
private static final int DATABASE_VERSION = 13;
|
||||
static Boolean apgHack = false;
|
||||
private Context mContext;
|
||||
|
||||
@@ -296,6 +296,8 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
// the api_accounts fix and the new update keys table
|
||||
return;
|
||||
}
|
||||
case 13:
|
||||
// do nothing here, just consolidate
|
||||
|
||||
}
|
||||
|
||||
@@ -306,6 +308,13 @@ public class KeychainDatabase extends SQLiteOpenHelper {
|
||||
mContext.getApplicationContext().startActivity(consolidateIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// NOTE: downgrading the database is explicitly not allowed to prevent
|
||||
// someone from exploiting old bugs to export the database
|
||||
throw new RuntimeException("Downgrading the database is not allowed!");
|
||||
}
|
||||
|
||||
/** This method tries to import data from a provided database.
|
||||
*
|
||||
* The sole assumptions made on this db are that there is a key_rings table
|
||||
|
||||
@@ -33,12 +33,14 @@ import android.widget.TextView;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
import org.sufficientlysecure.keychain.experimental.SentenceConfirm;
|
||||
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.ui.util.ExperimentalWordConfirm;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class CertifyFingerprintFragment extends LoaderFragment implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
@@ -46,24 +48,26 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
|
||||
static final int REQUEST_CERTIFY = 1;
|
||||
|
||||
public static final String ARG_DATA_URI = "uri";
|
||||
public static final String ARG_ENABLE_WORD_CONFIRM = "enable_word_confirm";
|
||||
public static final String ARG_ENABLE_PHRASES_CONFIRM = "enable_word_confirm";
|
||||
|
||||
private TextView mActionYes;
|
||||
private TextView mFingerprint;
|
||||
private TextView mIntro;
|
||||
private TextView mHeader;
|
||||
|
||||
private static final int LOADER_ID_UNIFIED = 0;
|
||||
|
||||
private Uri mDataUri;
|
||||
private boolean mEnableWordConfirm;
|
||||
private boolean mEnablePhrasesConfirm;
|
||||
|
||||
/**
|
||||
* Creates new instance of this fragment
|
||||
*/
|
||||
public static CertifyFingerprintFragment newInstance(Uri dataUri, boolean enableWordConfirm) {
|
||||
public static CertifyFingerprintFragment newInstance(Uri dataUri, boolean enablePhrasesConfirm) {
|
||||
CertifyFingerprintFragment frag = new CertifyFingerprintFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(ARG_DATA_URI, dataUri);
|
||||
args.putBoolean(ARG_ENABLE_WORD_CONFIRM, enableWordConfirm);
|
||||
args.putBoolean(ARG_ENABLE_PHRASES_CONFIRM, enablePhrasesConfirm);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
@@ -75,11 +79,12 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
|
||||
View root = super.onCreateView(inflater, superContainer, savedInstanceState);
|
||||
View view = inflater.inflate(R.layout.certify_fingerprint_fragment, getContainer());
|
||||
|
||||
View actionNo = view.findViewById(R.id.certify_fingerprint_button_no);
|
||||
View actionYes = view.findViewById(R.id.certify_fingerprint_button_yes);
|
||||
TextView actionNo = (TextView) view.findViewById(R.id.certify_fingerprint_button_no);
|
||||
mActionYes = (TextView) view.findViewById(R.id.certify_fingerprint_button_yes);
|
||||
|
||||
mFingerprint = (TextView) view.findViewById(R.id.certify_fingerprint_fingerprint);
|
||||
mIntro = (TextView) view.findViewById(R.id.certify_fingerprint_intro);
|
||||
mHeader = (TextView) view.findViewById(R.id.certify_fingerprint_fingerprint_header);
|
||||
|
||||
actionNo.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
@@ -87,7 +92,7 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
|
||||
getActivity().finish();
|
||||
}
|
||||
});
|
||||
actionYes.setOnClickListener(new View.OnClickListener() {
|
||||
mActionYes.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
certify(mDataUri);
|
||||
@@ -107,10 +112,12 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
|
||||
getActivity().finish();
|
||||
return;
|
||||
}
|
||||
mEnableWordConfirm = getArguments().getBoolean(ARG_ENABLE_WORD_CONFIRM);
|
||||
mEnablePhrasesConfirm = getArguments().getBoolean(ARG_ENABLE_PHRASES_CONFIRM);
|
||||
|
||||
if (mEnableWordConfirm) {
|
||||
mIntro.setText(R.string.certify_fingerprint_text_words);
|
||||
if (mEnablePhrasesConfirm) {
|
||||
mIntro.setText(R.string.certify_fingerprint_text_phrases);
|
||||
mHeader.setText(R.string.section_phrases);
|
||||
mActionYes.setText(R.string.btn_match_phrases);
|
||||
}
|
||||
|
||||
loadData(dataUri);
|
||||
@@ -160,7 +167,7 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
|
||||
if (data.moveToFirst()) {
|
||||
byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT);
|
||||
|
||||
if (mEnableWordConfirm) {
|
||||
if (mEnablePhrasesConfirm) {
|
||||
displayWordConfirm(fingerprintBlob);
|
||||
} else {
|
||||
displayHexConfirm(fingerprintBlob);
|
||||
@@ -180,9 +187,16 @@ public class CertifyFingerprintFragment extends LoaderFragment implements
|
||||
}
|
||||
|
||||
private void displayWordConfirm(byte[] fingerprintBlob) {
|
||||
String fingerprint = ExperimentalWordConfirm.getWords(getActivity(), fingerprintBlob);
|
||||
// String fingerprint = ExperimentalWordConfirm.getWords(getActivity(), fingerprintBlob);
|
||||
|
||||
mFingerprint.setTextSize(24);
|
||||
String fingerprint;
|
||||
try {
|
||||
fingerprint = new SentenceConfirm(getActivity()).fromBytes(fingerprintBlob, 16);
|
||||
} catch (IOException ioe) {
|
||||
fingerprint = "-";
|
||||
}
|
||||
|
||||
mFingerprint.setTextSize(18);
|
||||
mFingerprint.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
|
||||
mFingerprint.setText(fingerprint);
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ public class DecryptActivity extends BaseActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
uris.add(intent.getData());
|
||||
uris.add(uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -257,7 +257,6 @@ public class DecryptListFragment
|
||||
}
|
||||
|
||||
OpenPgpMetadata metadata = result.mMetadata.get(index);
|
||||
Uri saveUri = Uri.fromFile(activity.getExternalFilesDir(metadata.getMimeType()));
|
||||
mCurrentSaveFileUri = result.getOutputUris().get(index);
|
||||
|
||||
String filename = metadata.getFilename();
|
||||
@@ -266,8 +265,8 @@ public class DecryptListFragment
|
||||
filename = "decrypted" + (ext != null ? "."+ext : "");
|
||||
}
|
||||
|
||||
FileHelper.saveDocument(this, filename, saveUri, metadata.getMimeType(),
|
||||
R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to, REQUEST_CODE_OUTPUT);
|
||||
FileHelper.saveDocument(this, filename, metadata.getMimeType(),
|
||||
REQUEST_CODE_OUTPUT);
|
||||
}
|
||||
|
||||
private void saveFile(Uri saveUri) {
|
||||
@@ -376,10 +375,12 @@ public class DecryptListFragment
|
||||
// noinspection deprecation, this should be called from Context, but not available in minSdk
|
||||
icon = getResources().getDrawable(R.drawable.ic_chat_black_24dp);
|
||||
} else if (ClipDescription.compareMimeTypes(type, "image/*")) {
|
||||
int px = FormattingUtils.dpToPx(context, 48);
|
||||
int px = FormattingUtils.dpToPx(context, 32);
|
||||
Bitmap bitmap = FileHelper.getThumbnail(context, outputUri, new Point(px, px));
|
||||
icon = new BitmapDrawable(context.getResources(), bitmap);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (icon == null) {
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(outputUri, type);
|
||||
|
||||
@@ -445,6 +446,7 @@ public class DecryptListFragment
|
||||
displayWithViewIntent(result, index, true, true);
|
||||
break;
|
||||
case R.id.decrypt_save:
|
||||
// only inside the menu xml for Android >= 4.4
|
||||
saveFileDialog(result, index);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.sufficientlysecure.keychain.ui;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
@@ -224,9 +225,8 @@ public class EncryptFilesFragment
|
||||
String targetName =
|
||||
(mEncryptFilenames ? "1" : FileHelper.getFilename(getActivity(), model.inputUri))
|
||||
+ (mUseArmor ? Constants.FILE_EXTENSION_ASC : Constants.FILE_EXTENSION_PGP_MAIN);
|
||||
Uri inputUri = model.inputUri;
|
||||
FileHelper.saveDocument(this, targetName, inputUri,
|
||||
R.string.title_encrypt_to_file, R.string.specify_file_to_encrypt_to, REQUEST_CODE_OUTPUT);
|
||||
FileHelper.saveDocument(this, targetName,
|
||||
REQUEST_CODE_OUTPUT);
|
||||
}
|
||||
|
||||
public void addFile(Intent data) {
|
||||
@@ -308,6 +308,17 @@ public class EncryptFilesFragment
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
|
||||
// Show save only on Android >= 4.4 (Document Provider)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||
MenuItem save = menu.findItem(R.id.encrypt_save);
|
||||
save.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleUseArmor(MenuItem item, final boolean useArmor) {
|
||||
|
||||
mUseArmor = useArmor;
|
||||
@@ -441,9 +452,29 @@ public class EncryptFilesFragment
|
||||
|
||||
}
|
||||
|
||||
// prepares mOutputUris, either directly and returns false, or indirectly
|
||||
// which returns true and will call cryptoOperation after mOutputUris has
|
||||
// been set at a later point.
|
||||
/**
|
||||
* Checks that the input uris are not linked to our own internal storage.
|
||||
* This prevents the encryption of our own database (-> export of whole database)
|
||||
*/
|
||||
private void securityCheckInternalStorage() {
|
||||
for (FilesAdapter.ViewModel model : mFilesAdapter.mDataset) {
|
||||
File fileInput = new File(model.inputUri.getPath());
|
||||
try {
|
||||
// the canonical path of the file must not start with /data/data/org.sufficientlysecure.keychain/
|
||||
if (fileInput.getCanonicalPath().startsWith(getActivity().getApplicationInfo().dataDir)) {
|
||||
throw new RuntimeException("Encrypting OpenKeychain's private files is not allowed!");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Getting canonical path failed!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares mOutputUris, either directly and returns false, or indirectly
|
||||
* which returns true and will call cryptoOperation after mOutputUris has
|
||||
* been set at a later point.
|
||||
*/
|
||||
private boolean prepareOutputStreams() {
|
||||
|
||||
switch (mAfterEncryptAction) {
|
||||
@@ -519,6 +550,8 @@ public class EncryptFilesFragment
|
||||
|
||||
}
|
||||
|
||||
securityCheckInternalStorage();
|
||||
|
||||
return actionsParcel;
|
||||
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC
|
||||
data.getBoolean(AddEditKeyserverDialogFragment.MESSAGE_VERIFIED);
|
||||
if (verified) {
|
||||
Notify.create(getActivity(),
|
||||
R.string.add_keyserver_verified, Notify.Style.OK).show();
|
||||
R.string.add_keyserver_connection_verified, Notify.Style.OK).show();
|
||||
} else {
|
||||
Notify.create(getActivity(),
|
||||
R.string.add_keyserver_without_verification,
|
||||
@@ -177,26 +177,6 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AddEditKeyserverDialogFragment.MESSAGE_VERIFICATION_FAILED: {
|
||||
AddEditKeyserverDialogFragment.FailureReason failureReason =
|
||||
(AddEditKeyserverDialogFragment.FailureReason) data.getSerializable(
|
||||
AddEditKeyserverDialogFragment.MESSAGE_FAILURE_REASON);
|
||||
switch (failureReason) {
|
||||
case CONNECTION_FAILED: {
|
||||
Notify.create(getActivity(),
|
||||
R.string.add_keyserver_connection_failed,
|
||||
Notify.Style.ERROR).show();
|
||||
break;
|
||||
}
|
||||
case INVALID_URL: {
|
||||
Notify.create(getActivity(),
|
||||
R.string.add_keyserver_invalid_url,
|
||||
Notify.Style.ERROR).show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,7 +107,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
|
||||
View vFingerprintShareButton = view.findViewById(R.id.view_key_action_fingerprint_share);
|
||||
View vFingerprintClipboardButton = view.findViewById(R.id.view_key_action_fingerprint_clipboard);
|
||||
View vKeyShareButton = view.findViewById(R.id.view_key_action_key_share);
|
||||
View vKeySafeButton = view.findViewById(R.id.view_key_action_key_export);
|
||||
View vKeySaveButton = view.findViewById(R.id.view_key_action_key_export);
|
||||
View vKeyNfcButton = view.findViewById(R.id.view_key_action_key_nfc);
|
||||
View vKeyClipboardButton = view.findViewById(R.id.view_key_action_key_clipboard);
|
||||
ImageButton vKeySafeSlingerButton = (ImageButton) view.findViewById(R.id.view_key_action_key_safeslinger);
|
||||
@@ -133,7 +133,11 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
|
||||
share(false, false);
|
||||
}
|
||||
});
|
||||
vKeySafeButton.setOnClickListener(new View.OnClickListener() {
|
||||
// Show save only on Android >= 4.4 (Document Provider)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||
vKeySaveButton.setVisibility(View.GONE);
|
||||
}
|
||||
vKeySaveButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
exportToFile();
|
||||
|
||||
@@ -40,6 +40,7 @@ import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.textuality.keybase.lib.KeybaseException;
|
||||
import com.textuality.keybase.lib.KeybaseQuery;
|
||||
import com.textuality.keybase.lib.Proof;
|
||||
import com.textuality.keybase.lib.User;
|
||||
|
||||
@@ -51,6 +52,7 @@ import org.sufficientlysecure.keychain.service.KeybaseVerificationParcel;
|
||||
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.OkHttpKeybaseClient;
|
||||
import org.sufficientlysecure.keychain.util.ParcelableProxy;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
|
||||
@@ -224,8 +226,9 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements
|
||||
}
|
||||
}
|
||||
|
||||
// look for evidence from keybase in the background, make tabular version of result
|
||||
//
|
||||
/**
|
||||
* look for evidence from keybase in the background, make tabular version of result
|
||||
*/
|
||||
private class DescribeKey extends AsyncTask<String, Void, ResultPage> {
|
||||
ParcelableProxy mParcelableProxy;
|
||||
|
||||
@@ -240,7 +243,9 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements
|
||||
final ArrayList<CharSequence> proofList = new ArrayList<CharSequence>();
|
||||
final Hashtable<Integer, ArrayList<Proof>> proofs = new Hashtable<Integer, ArrayList<Proof>>();
|
||||
try {
|
||||
User keybaseUser = User.findByFingerprint(fingerprint, mParcelableProxy.getProxy());
|
||||
KeybaseQuery keybaseQuery = new KeybaseQuery(new OkHttpKeybaseClient());
|
||||
keybaseQuery.setProxy(mParcelableProxy.getProxy());
|
||||
User keybaseUser = User.findByFingerprint(keybaseQuery, fingerprint);
|
||||
for (Proof proof : keybaseUser.getProofs()) {
|
||||
Integer proofType = proof.getType();
|
||||
appendIfOK(proofs, proofType, proof);
|
||||
@@ -266,7 +271,12 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements
|
||||
} catch (KeybaseException ignored) {
|
||||
}
|
||||
|
||||
return new ResultPage(getString(R.string.key_trust_results_prefix), proofList);
|
||||
String prefix = "";
|
||||
if (isAdded()) {
|
||||
prefix = getString(R.string.key_trust_results_prefix);
|
||||
}
|
||||
|
||||
return new ResultPage(prefix, proofList);
|
||||
}
|
||||
|
||||
private SpannableStringBuilder formatSpannableString(SpannableStringBuilder proofLinks, String proofType) {
|
||||
@@ -291,7 +301,10 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements
|
||||
if (haveProofFor(proof.getType())) {
|
||||
ssb.append("\u00a0[");
|
||||
startAt = ssb.length();
|
||||
String verify = getString(R.string.keybase_verify);
|
||||
String verify = "";
|
||||
if (isAdded()) {
|
||||
verify = getString(R.string.keybase_verify);
|
||||
}
|
||||
ssb.append(verify);
|
||||
ClickableSpan clicker = new ClickableSpan() {
|
||||
@Override
|
||||
@@ -308,6 +321,11 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements
|
||||
@Override
|
||||
protected void onPostExecute(ResultPage result) {
|
||||
super.onPostExecute(result);
|
||||
// stop if fragment is no longer added to an activity
|
||||
if(!isAdded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.mProofs.isEmpty()) {
|
||||
result.mHeader = getActivity().getString(R.string.key_trust_no_cloud_evidence);
|
||||
}
|
||||
@@ -356,7 +374,12 @@ public class ViewKeyKeybaseFragment extends LoaderFragment implements
|
||||
default:
|
||||
stringIndex = R.string.keybase_narrative_unknown;
|
||||
}
|
||||
return getActivity().getString(stringIndex);
|
||||
|
||||
if (isAdded()) {
|
||||
return getString(stringIndex);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private void appendIfOK(Hashtable<Integer, ArrayList<Proof>> table, Integer proofType, Proof proof) throws KeybaseException {
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
@@ -44,6 +45,7 @@ import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
@@ -54,6 +56,7 @@ import com.squareup.okhttp.Request;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.Preferences;
|
||||
import org.sufficientlysecure.keychain.util.TlsHelper;
|
||||
@@ -68,11 +71,9 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
private static final String ARG_KEYSERVER = "arg_keyserver";
|
||||
|
||||
public static final int MESSAGE_OKAY = 1;
|
||||
public static final int MESSAGE_VERIFICATION_FAILED = 2;
|
||||
|
||||
public static final String MESSAGE_KEYSERVER = "new_keyserver";
|
||||
public static final String MESSAGE_VERIFIED = "verified";
|
||||
public static final String MESSAGE_FAILURE_REASON = "failure_reason";
|
||||
public static final String MESSAGE_KEYSERVER_DELETED = "keyserver_deleted";
|
||||
public static final String MESSAGE_DIALOG_ACTION = "message_dialog_action";
|
||||
public static final String MESSAGE_EDIT_POSITION = "keyserver_edited_position";
|
||||
@@ -82,7 +83,9 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
private int mPosition;
|
||||
|
||||
private EditText mKeyserverEditText;
|
||||
private TextInputLayout mKeyserverEditTextLayout;
|
||||
private CheckBox mVerifyKeyserverCheckBox;
|
||||
private CheckBox mOnlyTrustedKeyserverCheckBox;
|
||||
|
||||
public enum DialogAction {
|
||||
ADD,
|
||||
@@ -91,7 +94,8 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
|
||||
public enum FailureReason {
|
||||
INVALID_URL,
|
||||
CONNECTION_FAILED
|
||||
CONNECTION_FAILED,
|
||||
NO_PINNED_CERTIFICATE
|
||||
}
|
||||
|
||||
public static AddEditKeyserverDialogFragment newInstance(Messenger messenger,
|
||||
@@ -126,7 +130,15 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
alert.setView(view);
|
||||
|
||||
mKeyserverEditText = (EditText) view.findViewById(R.id.keyserver_url_edit_text);
|
||||
mVerifyKeyserverCheckBox = (CheckBox) view.findViewById(R.id.verify_keyserver_checkbox);
|
||||
mKeyserverEditTextLayout = (TextInputLayout) view.findViewById(R.id.keyserver_url_edit_text_layout);
|
||||
mVerifyKeyserverCheckBox = (CheckBox) view.findViewById(R.id.verify_connection_checkbox);
|
||||
mOnlyTrustedKeyserverCheckBox = (CheckBox) view.findViewById(R.id.only_trusted_keyserver_checkbox);
|
||||
mVerifyKeyserverCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
mOnlyTrustedKeyserverCheckBox.setEnabled(isChecked);
|
||||
}
|
||||
});
|
||||
|
||||
switch (mDialogAction) {
|
||||
case ADD: {
|
||||
@@ -212,6 +224,8 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
positiveButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mKeyserverEditTextLayout.setErrorEnabled(false);
|
||||
|
||||
// behaviour same for edit and add
|
||||
final String keyserverUrl = mKeyserverEditText.getText().toString();
|
||||
if (mVerifyKeyserverCheckBox.isChecked()) {
|
||||
@@ -220,13 +234,20 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
OrbotHelper.DialogActions dialogActions = new OrbotHelper.DialogActions() {
|
||||
@Override
|
||||
public void onOrbotStarted() {
|
||||
verifyConnection(keyserverUrl,
|
||||
proxyPrefs.parcelableProxy.getProxy());
|
||||
verifyConnection(
|
||||
keyserverUrl,
|
||||
proxyPrefs.parcelableProxy.getProxy(),
|
||||
mOnlyTrustedKeyserverCheckBox.isChecked()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNeutralButton() {
|
||||
verifyConnection(keyserverUrl, null);
|
||||
verifyConnection(
|
||||
keyserverUrl,
|
||||
null,
|
||||
mOnlyTrustedKeyserverCheckBox.isChecked()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -236,7 +257,11 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
};
|
||||
|
||||
if (OrbotHelper.putOrbotInRequiredState(dialogActions, getActivity())) {
|
||||
verifyConnection(keyserverUrl, proxyPrefs.parcelableProxy.getProxy());
|
||||
verifyConnection(
|
||||
keyserverUrl,
|
||||
proxyPrefs.parcelableProxy.getProxy(),
|
||||
mOnlyTrustedKeyserverCheckBox.isChecked()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dismiss();
|
||||
@@ -272,14 +297,28 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
sendMessageToHandler(MESSAGE_OKAY, data);
|
||||
}
|
||||
|
||||
public void verificationFailed(FailureReason reason) {
|
||||
Bundle data = new Bundle();
|
||||
data.putSerializable(MESSAGE_FAILURE_REASON, reason);
|
||||
public void verificationFailed(FailureReason failureReason) {
|
||||
switch (failureReason) {
|
||||
case CONNECTION_FAILED: {
|
||||
mKeyserverEditTextLayout.setError(
|
||||
getString(R.string.add_keyserver_connection_failed));
|
||||
break;
|
||||
}
|
||||
case INVALID_URL: {
|
||||
mKeyserverEditTextLayout.setError(
|
||||
getString(R.string.add_keyserver_invalid_url));
|
||||
break;
|
||||
}
|
||||
case NO_PINNED_CERTIFICATE: {
|
||||
mKeyserverEditTextLayout.setError(
|
||||
getString(R.string.add_keyserver_keyserver_not_trusted));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sendMessageToHandler(MESSAGE_VERIFICATION_FAILED, data);
|
||||
}
|
||||
|
||||
public void verifyConnection(String keyserver, final Proxy proxy) {
|
||||
public void verifyConnection(String keyserver, final Proxy proxy, final boolean onlyTrustedKeyserver) {
|
||||
|
||||
new AsyncTask<String, Void, FailureReason>() {
|
||||
ProgressDialog mProgressDialog;
|
||||
@@ -288,7 +327,7 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
mProgressDialog = new ProgressDialog(getActivity());
|
||||
mProgressDialog.setMessage(getString(R.string.progress_verifying_keyserver_url));
|
||||
mProgressDialog.setMessage(getString(R.string.progress_verifying_keyserver_connection));
|
||||
mProgressDialog.setCancelable(false);
|
||||
mProgressDialog.show();
|
||||
}
|
||||
@@ -316,7 +355,18 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
|
||||
Log.d("Converted URL", newKeyserver.toString());
|
||||
|
||||
OkHttpClient client = HkpKeyserver.getClient(newKeyserver.toURL(), proxy);
|
||||
TlsHelper.pinCertificateIfNecessary(client, newKeyserver.toURL());
|
||||
|
||||
// don't follow any redirects
|
||||
client.setFollowRedirects(false);
|
||||
client.setFollowSslRedirects(false);
|
||||
|
||||
if (onlyTrustedKeyserver
|
||||
&& !TlsHelper.usePinnedCertificateIfAvailable(client, newKeyserver.toURL())) {
|
||||
Log.w(Constants.TAG, "No pinned certificate for this host in OpenKeychain's assets.");
|
||||
reason = FailureReason.NO_PINNED_CERTIFICATE;
|
||||
return reason;
|
||||
}
|
||||
|
||||
client.newCall(new Request.Builder().url(newKeyserver.toURL()).build()).execute();
|
||||
} catch (TlsHelper.TlsHelperException e) {
|
||||
reason = FailureReason.CONNECTION_FAILED;
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012-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.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.util.FileHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* This is a file chooser dialog no longer used with KitKat
|
||||
*/
|
||||
public class FileDialogFragment extends DialogFragment {
|
||||
private static final String ARG_MESSENGER = "messenger";
|
||||
private static final String ARG_TITLE = "title";
|
||||
private static final String ARG_MESSAGE = "message";
|
||||
private static final String ARG_DEFAULT_FILE = "default_file";
|
||||
private static final String ARG_CHECKBOX_TEXT = "checkbox_text";
|
||||
|
||||
public static final int MESSAGE_OKAY = 1;
|
||||
|
||||
public static final String MESSAGE_DATA_FILE = "file";
|
||||
public static final String MESSAGE_DATA_CHECKED = "checked";
|
||||
|
||||
private Messenger mMessenger;
|
||||
|
||||
private EditText mFilename;
|
||||
private ImageButton mBrowse;
|
||||
private CheckBox mCheckBox;
|
||||
private TextView mMessageTextView;
|
||||
|
||||
private File mFile;
|
||||
|
||||
private static final int REQUEST_CODE = 0x00007004;
|
||||
|
||||
/**
|
||||
* Creates new instance of this file dialog fragment
|
||||
*/
|
||||
public static FileDialogFragment newInstance(Messenger messenger, String title, String message,
|
||||
File defaultFile, String checkboxText) {
|
||||
FileDialogFragment frag = new FileDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(ARG_MESSENGER, messenger);
|
||||
|
||||
args.putString(ARG_TITLE, title);
|
||||
args.putString(ARG_MESSAGE, message);
|
||||
args.putString(ARG_DEFAULT_FILE, defaultFile.getAbsolutePath());
|
||||
args.putString(ARG_CHECKBOX_TEXT, checkboxText);
|
||||
|
||||
frag.setArguments(args);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates dialog
|
||||
*/
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
mMessenger = getArguments().getParcelable(ARG_MESSENGER);
|
||||
|
||||
String title = getArguments().getString(ARG_TITLE);
|
||||
String message = getArguments().getString(ARG_MESSAGE);
|
||||
mFile = new File(getArguments().getString(ARG_DEFAULT_FILE));
|
||||
if (!mFile.isAbsolute()) {
|
||||
// We use OK dir by default
|
||||
mFile = new File(Constants.Path.APP_DIR.getAbsolutePath(), mFile.getName());
|
||||
}
|
||||
String checkboxText = getArguments().getString(ARG_CHECKBOX_TEXT);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) activity
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(activity);
|
||||
alert.setTitle(title);
|
||||
|
||||
View view = inflater.inflate(R.layout.file_dialog, null);
|
||||
|
||||
mMessageTextView = (TextView) view.findViewById(R.id.message);
|
||||
mMessageTextView.setText(message);
|
||||
|
||||
mFilename = (EditText) view.findViewById(R.id.input);
|
||||
mFilename.setText(mFile.getName());
|
||||
mBrowse = (ImageButton) view.findViewById(R.id.btn_browse);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||
mBrowse.setVisibility(View.GONE);
|
||||
} else {
|
||||
mBrowse.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
// only .asc or .gpg files
|
||||
// setting it to text/plain prevents Cynaogenmod's file manager from selecting asc
|
||||
// or gpg types!
|
||||
FileHelper.saveDocumentKitKat(
|
||||
FileDialogFragment.this, "*/*", mFile.getName(), REQUEST_CODE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mCheckBox = (CheckBox) view.findViewById(R.id.checkbox);
|
||||
if (checkboxText == null) {
|
||||
mCheckBox.setEnabled(false);
|
||||
mCheckBox.setVisibility(View.GONE);
|
||||
} else {
|
||||
mCheckBox.setEnabled(true);
|
||||
mCheckBox.setVisibility(View.VISIBLE);
|
||||
mCheckBox.setText(checkboxText);
|
||||
mCheckBox.setChecked(true);
|
||||
}
|
||||
|
||||
alert.setView(view);
|
||||
|
||||
alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
|
||||
String currentFilename = mFilename.getText().toString();
|
||||
if (currentFilename == null || currentFilename.isEmpty()) {
|
||||
// No file is like pressing cancel, UI: maybe disable positive button in this case?
|
||||
return;
|
||||
}
|
||||
|
||||
if (mFile == null || currentFilename.startsWith("/")) {
|
||||
mFile = new File(currentFilename);
|
||||
} else if (!mFile.getName().equals(currentFilename)) {
|
||||
// We update our File object if user changed name!
|
||||
mFile = new File(mFile.getParentFile(), currentFilename);
|
||||
}
|
||||
|
||||
boolean checked = mCheckBox.isEnabled() && mCheckBox.isChecked();
|
||||
|
||||
// return resulting data back to activity
|
||||
Bundle data = new Bundle();
|
||||
data.putString(MESSAGE_DATA_FILE, mFile.getAbsolutePath());
|
||||
data.putBoolean(MESSAGE_DATA_CHECKED, checked);
|
||||
|
||||
sendMessageToHandler(MESSAGE_OKAY, data);
|
||||
}
|
||||
});
|
||||
|
||||
alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
return alert.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode & 0xFFFF) {
|
||||
case REQUEST_CODE: {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
File file = new File(data.getData().getPath());
|
||||
if (file.getParentFile().exists()) {
|
||||
mFile = file;
|
||||
mFilename.setText(mFile.getName());
|
||||
} else {
|
||||
Notify.create(getActivity(), R.string.no_file_selected, Notify.Style.ERROR).show();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message back to handler which is initialized in a activity
|
||||
*
|
||||
* @param what Message integer you want to send
|
||||
*/
|
||||
private void sendMessageToHandler(Integer what, Bundle data) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = what;
|
||||
if (data != null) {
|
||||
msg.setData(data);
|
||||
}
|
||||
|
||||
try {
|
||||
mMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Constants.TAG, "Exception sending message, Is handler present?", e);
|
||||
} catch (NullPointerException e) {
|
||||
Log.w(Constants.TAG, "Messenger is null!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
|
||||
@@ -35,7 +34,6 @@ import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
import org.sufficientlysecure.keychain.util.FileHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.URI;
|
||||
@@ -134,9 +132,10 @@ public class LinkedIdCreateHttpsStep2Fragment extends LinkedIdCreateFinalFragmen
|
||||
|
||||
String targetName = "pgpkey.txt";
|
||||
|
||||
// TODO: not supported on Android < 4.4
|
||||
FileHelper.saveDocument(this,
|
||||
targetName, Uri.fromFile(new File(Constants.Path.APP_DIR, targetName)),
|
||||
"text/plain", R.string.title_decrypt_to_file, R.string.specify_file_to_decrypt_to,
|
||||
targetName,
|
||||
"text/plain",
|
||||
REQUEST_CODE_OUTPUT);
|
||||
}
|
||||
|
||||
|
||||
@@ -103,8 +103,7 @@ public class EmailKeyHelper {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Keyserver.QueryFailedException ignored) {
|
||||
} catch (Keyserver.QueryNeedsRepairException ignored) {
|
||||
} catch (Keyserver.CloudSearchFailureException ignored) {
|
||||
}
|
||||
return new ArrayList<>(keys);
|
||||
}
|
||||
|
||||
@@ -69,13 +69,14 @@ public class ExportHelper
|
||||
: R.string.specify_backup_dest_single);
|
||||
}
|
||||
|
||||
FileHelper.saveDocumentDialog(new FileHelper.FileDialogCallback() {
|
||||
@Override
|
||||
public void onFileSelected(File file, boolean checked) {
|
||||
mExportFile = file;
|
||||
exportKeys(masterKeyId == null ? null : new long[] { masterKeyId }, exportSecret);
|
||||
}
|
||||
}, mActivity.getSupportFragmentManager(), title, message, exportFile, null);
|
||||
// TODO: for valodim
|
||||
// FileHelper.saveDocumentDialog(new FileHelper.FileDialogCallback() {
|
||||
// @Override
|
||||
// public void onFileSelected(File file, boolean checked) {
|
||||
// mExportFile = file;
|
||||
// exportKeys(masterKeyId == null ? null : new long[] { masterKeyId }, exportSecret);
|
||||
// }
|
||||
// }, mActivity.getSupportFragmentManager(), title, message, exportFile, null);
|
||||
}
|
||||
|
||||
// TODO: If ExportHelper requires pending data (see CryptoOPerationHelper), activities using
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.sufficientlysecure.keychain.util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
@@ -30,20 +29,13 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.Messenger;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
|
||||
import org.sufficientlysecure.keychain.ui.dialog.FileDialogFragment;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
@@ -82,50 +74,24 @@ import java.text.DecimalFormat;
|
||||
public class FileHelper {
|
||||
|
||||
public static void openDocument(Fragment fragment, Uri last, String mimeType, boolean multiple, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||
openDocumentPreKitKat(fragment, last, mimeType, multiple, requestCode);
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
openDocumentKitKat(fragment, mimeType, multiple, requestCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveDocument(Fragment fragment, String targetName, Uri inputUri,
|
||||
@StringRes int title, @StringRes int message, int requestCode) {
|
||||
saveDocument(fragment, targetName, inputUri, "*/*", title, message, requestCode);
|
||||
}
|
||||
|
||||
public static void saveDocument(Fragment fragment, String targetName, Uri inputUri, String mimeType,
|
||||
@StringRes int title, @StringRes int message, int requestCode) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||
saveDocumentDialog(fragment, targetName, inputUri, title, message, requestCode);
|
||||
} else {
|
||||
saveDocumentKitKat(fragment, mimeType, targetName, requestCode);
|
||||
openDocumentPreKitKat(fragment, last, mimeType, multiple, requestCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveDocumentDialog(final Fragment fragment, String targetName, Uri inputUri,
|
||||
@StringRes int title, @StringRes int message, final int requestCode) {
|
||||
|
||||
saveDocumentDialog(fragment, targetName, inputUri, title, message, new FileDialogCallback() {
|
||||
// is this a good idea? seems hacky...
|
||||
@Override
|
||||
public void onFileSelected(File file, boolean checked) {
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.fromFile(file));
|
||||
fragment.onActivityResult(requestCode, Activity.RESULT_OK, intent);
|
||||
}
|
||||
});
|
||||
public static void saveDocument(Fragment fragment, String targetName, int requestCode) {
|
||||
saveDocument(fragment, targetName, "*/*", requestCode);
|
||||
}
|
||||
|
||||
public static void saveDocumentDialog(final Fragment fragment, String targetName, Uri inputUri,
|
||||
@StringRes int title, @StringRes int message, FileDialogCallback callback) {
|
||||
|
||||
File file = inputUri == null ? null : new File(inputUri.getPath());
|
||||
File parentDir = file != null && file.exists() ? file.getParentFile() : Constants.Path.APP_DIR;
|
||||
File targetFile = new File(parentDir, targetName);
|
||||
saveDocumentDialog(callback, fragment.getActivity().getSupportFragmentManager(),
|
||||
fragment.getString(title), fragment.getString(message), targetFile, null);
|
||||
|
||||
public static void saveDocument(Fragment fragment, String targetName, String mimeType,
|
||||
int requestCode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
saveDocumentKitKat(fragment, mimeType, targetName, requestCode);
|
||||
} else {
|
||||
throw new RuntimeException("saveDocument does not support Android < 4.4!");
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the preferred installed file manager on Android and shows a toast
|
||||
@@ -172,36 +138,6 @@ public class FileHelper {
|
||||
fragment.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
public static void saveDocumentDialog(
|
||||
final FileDialogCallback callback, final FragmentManager fragmentManager,
|
||||
final String title, final String message, final File defaultFile,
|
||||
final String checkMsg) {
|
||||
// Message is received after file is selected
|
||||
Handler returnHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (message.what == FileDialogFragment.MESSAGE_OKAY) {
|
||||
callback.onFileSelected(
|
||||
new File(message.getData().getString(FileDialogFragment.MESSAGE_DATA_FILE)),
|
||||
message.getData().getBoolean(FileDialogFragment.MESSAGE_DATA_CHECKED));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new Messenger for the communication back
|
||||
final Messenger messenger = new Messenger(returnHandler);
|
||||
|
||||
DialogFragmentWorkaround.INTERFACE.runnableRunDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
FileDialogFragment fileDialog = FileDialogFragment.newInstance(messenger, title, message,
|
||||
defaultFile, checkMsg);
|
||||
|
||||
fileDialog.show(fragmentManager, "fileDialog");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static String getFilename(Context context, Uri uri) {
|
||||
String filename = null;
|
||||
try {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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.util;
|
||||
|
||||
|
||||
|
||||
@@ -141,6 +141,10 @@ public class NfcHelper {
|
||||
}
|
||||
|
||||
protected void onPostExecute(Void unused) {
|
||||
if (mActivity.isFinishing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register callback to set NDEF message
|
||||
mNfcAdapter.setNdefPushMessageCallback(mNdefCallback,
|
||||
mActivity);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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.util;
|
||||
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.OkUrlFactory;
|
||||
import com.textuality.keybase.lib.KeybaseUrlConnectionClient;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Wrapper for Keybase Lib
|
||||
*/
|
||||
public class OkHttpKeybaseClient implements KeybaseUrlConnectionClient {
|
||||
|
||||
private final OkUrlFactory factory;
|
||||
|
||||
private static OkUrlFactory generateUrlFactory() {
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
return new OkUrlFactory(client);
|
||||
}
|
||||
|
||||
public OkHttpKeybaseClient() {
|
||||
factory = generateUrlFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URLConnection openConnection(URL url) throws IOException {
|
||||
return openConnection(url, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URLConnection openConnection(URL url, Proxy proxy) throws IOException {
|
||||
if (proxy != null) {
|
||||
factory.client().setProxy(proxy);
|
||||
factory.client().setConnectTimeout(30000, TimeUnit.MILLISECONDS);
|
||||
factory.client().setReadTimeout(40000, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
factory.client().setConnectTimeout(5000, TimeUnit.MILLISECONDS);
|
||||
factory.client().setReadTimeout(25000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
factory.client().setFollowSslRedirects(false);
|
||||
|
||||
// forced the usage of keybase.io pinned certificate
|
||||
try {
|
||||
if (!TlsHelper.usePinnedCertificateIfAvailable(factory.client(), url)) {
|
||||
throw new IOException("no pinned certificate found for URL!");
|
||||
}
|
||||
} catch (TlsHelper.TlsHelperException e) {
|
||||
Log.e(Constants.TAG, "TlsHelper failed", e);
|
||||
throw new IOException("TlsHelper failed");
|
||||
}
|
||||
|
||||
return factory.open(url);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
* Copyright (C) 2013-2015 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
|
||||
@@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.util;
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -37,7 +38,6 @@ import java.security.cert.CertificateFactory;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
@@ -49,15 +49,14 @@ public class TlsHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, byte[]> sStaticCA = new HashMap<>();
|
||||
private static Map<String, byte[]> sPinnedCertificates = new HashMap<>();
|
||||
|
||||
public static void addStaticCA(String domain, byte[] certificate) {
|
||||
sStaticCA.put(domain, certificate);
|
||||
}
|
||||
|
||||
public static void addStaticCA(String domain, AssetManager assetManager, String name) {
|
||||
/**
|
||||
* Add certificate from assets to pinned certificate map.
|
||||
*/
|
||||
public static void addPinnedCertificate(String host, AssetManager assetManager, String cerFilename) {
|
||||
try {
|
||||
InputStream is = assetManager.open(name);
|
||||
InputStream is = assetManager.open(cerFilename);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
int reads = is.read();
|
||||
|
||||
@@ -68,27 +67,36 @@ public class TlsHelper {
|
||||
|
||||
is.close();
|
||||
|
||||
addStaticCA(domain, baos.toByteArray());
|
||||
sPinnedCertificates.put(host, baos.toByteArray());
|
||||
} catch (IOException e) {
|
||||
Log.w(Constants.TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void pinCertificateIfNecessary(OkHttpClient client, URL url) throws TlsHelperException, IOException {
|
||||
/**
|
||||
* Use pinned certificate for OkHttpClient if we have one.
|
||||
*
|
||||
* @return true, if certificate is available, false if not
|
||||
* @throws TlsHelperException
|
||||
* @throws IOException
|
||||
*/
|
||||
public static boolean usePinnedCertificateIfAvailable(OkHttpClient client, URL url) throws TlsHelperException, IOException {
|
||||
if (url.getProtocol().equals("https")) {
|
||||
for (String domain : sStaticCA.keySet()) {
|
||||
if (url.getHost().endsWith(domain)) {
|
||||
pinCertificate(sStaticCA.get(domain), client);
|
||||
// use certificate PIN from assets if we have one
|
||||
for (String host : sPinnedCertificates.keySet()) {
|
||||
if (url.getHost().endsWith(host)) {
|
||||
pinCertificate(sPinnedCertificates.get(host), client);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the client to accept only requests with a given certificate. Applies to all URLs requested by the
|
||||
* client.
|
||||
* Therefore a client that is pinned this way should be used to only make requests to URLs with passed certificate.
|
||||
* TODO: Refactor - More like SSH StrictHostKeyChecking than pinning?
|
||||
*
|
||||
* @param certificate certificate to pin
|
||||
* @param client OkHttpClient to enforce pinning on
|
||||
@@ -97,8 +105,10 @@ public class TlsHelper {
|
||||
*/
|
||||
private static void pinCertificate(byte[] certificate, OkHttpClient client)
|
||||
throws TlsHelperException, IOException {
|
||||
// We don't use OkHttp's CertificatePinner since it depends on a TrustManager to verify it too. Refer to
|
||||
// note at end of description: http://square.github.io/okhttp/javadoc/com/squareup/okhttp/CertificatePinner.html
|
||||
// We don't use OkHttp's CertificatePinner since it can not be used to pin self-signed
|
||||
// certificate if such certificate is not accepted by TrustManager.
|
||||
// (Refer to note at end of description:
|
||||
// http://square.github.io/okhttp/javadoc/com/squareup/okhttp/CertificatePinner.html )
|
||||
// Creating our own TrustManager that trusts only our certificate eliminates the need for certificate pinning
|
||||
try {
|
||||
// Load CA
|
||||
@@ -126,42 +136,4 @@ public class TlsHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a Connection that will only accept certificates signed with a specific CA and skips common name check.
|
||||
* This is required for some distributed Keyserver networks like sks-keyservers.net
|
||||
*
|
||||
* @param certificate The X.509 certificate used to sign the servers certificate
|
||||
* @param url Connection target
|
||||
*/
|
||||
public static HttpsURLConnection openCAConnection(byte[] certificate, URL url)
|
||||
throws TlsHelperException, IOException {
|
||||
try {
|
||||
// Load CA
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
Certificate ca = cf.generateCertificate(new ByteArrayInputStream(certificate));
|
||||
|
||||
// Create a KeyStore containing our trusted CAs
|
||||
String keyStoreType = KeyStore.getDefaultType();
|
||||
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
|
||||
keyStore.load(null, null);
|
||||
keyStore.setCertificateEntry("ca", ca);
|
||||
|
||||
// Create a TrustManager that trusts the CAs in our KeyStore
|
||||
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
|
||||
tmf.init(keyStore);
|
||||
|
||||
// Create an SSLContext that uses our TrustManager
|
||||
SSLContext context = SSLContext.getInstance("TLS");
|
||||
context.init(null, tmf.getTrustManagers(), null);
|
||||
|
||||
// Tell the URLConnection to use a SocketFactory from our SSLContext
|
||||
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
|
||||
urlConnection.setSSLSocketFactory(context.getSocketFactory());
|
||||
|
||||
return urlConnection;
|
||||
} catch (CertificateException | KeyManagementException | KeyStoreException | NoSuchAlgorithmException e) {
|
||||
throw new TlsHelperException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user