mirror of
https://github.com/moparisthebest/open-keychain
synced 2024-11-27 03:02:15 -05:00
Merge branch 'development' into detached-sigs-api
Conflicts: OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
This commit is contained in:
commit
e8780b4410
@ -438,6 +438,60 @@ public class PgpEncryptDecryptTest {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForeignEncoding () throws Exception {
|
||||
String plaintext = "ウィキペディア";
|
||||
byte[] plaindata = plaintext.getBytes("iso-2022-jp");
|
||||
|
||||
{ // some quick sanity checks
|
||||
Assert.assertEquals(plaintext, new String(plaindata, "iso-2022-jp"));
|
||||
Assert.assertNotEquals(plaintext, new String(plaindata, "utf-8"));
|
||||
}
|
||||
|
||||
byte[] ciphertext;
|
||||
{ // encrypt data with a given passphrase
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(plaindata);
|
||||
|
||||
InputData data = new InputData(in, in.available());
|
||||
Builder b = new PgpSignEncrypt.Builder(
|
||||
Robolectric.application,
|
||||
new ProviderHelper(Robolectric.application),
|
||||
null, // new DummyPassphraseCache(mPassphrase, 0L),
|
||||
data, out);
|
||||
|
||||
b.setEncryptionMasterKeyIds(new long[]{ mStaticRing1.getMasterKeyId() });
|
||||
b.setSymmetricEncryptionAlgorithm(PGPEncryptedData.AES_128);
|
||||
// this only works with ascii armored output!
|
||||
b.setEnableAsciiArmorOutput(true);
|
||||
b.setCharset("iso-2022-jp");
|
||||
SignEncryptResult result = b.build().execute();
|
||||
Assert.assertTrue("encryption must succeed", result.success());
|
||||
|
||||
ciphertext = out.toByteArray();
|
||||
}
|
||||
|
||||
{ // decryption with provided passphrase should yield the same result
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
|
||||
InputData data = new InputData(in, in.available());
|
||||
|
||||
PgpDecryptVerify.Builder b = builderWithFakePassphraseCache(data, out, null, null, null);
|
||||
b.setPassphrase(mKeyPhrase1);
|
||||
DecryptVerifyResult result = b.build().execute();
|
||||
Assert.assertTrue("decryption with provided passphrase must succeed", result.success());
|
||||
Assert.assertArrayEquals("decrypted ciphertext should equal plaintext bytes",
|
||||
out.toByteArray(), plaindata);
|
||||
Assert.assertEquals("charset should be read correctly",
|
||||
"iso-2022-jp", result.getCharset());
|
||||
Assert.assertEquals("decrypted ciphertext should equal plaintext",
|
||||
new String(out.toByteArray(), result.getCharset()), plaintext);
|
||||
Assert.assertNull("signature be empty", result.getSignatureResult());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private PgpDecryptVerify.Builder builderWithFakePassphraseCache (
|
||||
InputData data, OutputStream out,
|
||||
final String passphrase, final Long checkMasterKeyId, final Long checkSubKeyId) {
|
||||
|
@ -30,6 +30,7 @@ import org.sufficientlysecure.keychain.keyimport.KeybaseKeyserver;
|
||||
import org.sufficientlysecure.keychain.keyimport.Keyserver;
|
||||
import org.sufficientlysecure.keychain.keyimport.Keyserver.AddKeyException;
|
||||
import org.sufficientlysecure.keychain.keyimport.ParcelableKeyRing;
|
||||
import org.sufficientlysecure.keychain.operations.results.ConsolidateResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.ExportResult;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedKeyRing;
|
||||
import org.sufficientlysecure.keychain.pgp.CanonicalizedPublicKeyRing;
|
||||
@ -44,9 +45,12 @@ import org.sufficientlysecure.keychain.operations.results.OperationResult.LogTyp
|
||||
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
|
||||
import org.sufficientlysecure.keychain.operations.results.ImportKeyResult;
|
||||
import org.sufficientlysecure.keychain.operations.results.SaveKeyringResult;
|
||||
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
|
||||
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
|
||||
import org.sufficientlysecure.keychain.util.FileHelper;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.ParcelableFileCache;
|
||||
import org.sufficientlysecure.keychain.util.ParcelableFileCache.IteratorWithSize;
|
||||
import org.sufficientlysecure.keychain.util.ProgressScaler;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
@ -58,6 +62,7 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/** An operation class which implements high level import and export
|
||||
@ -123,6 +128,35 @@ public class ImportExportOperation extends BaseOperation {
|
||||
}
|
||||
}
|
||||
|
||||
public ImportKeyResult importKeyRings(List<ParcelableKeyRing> entries, String keyServerUri) {
|
||||
|
||||
Iterator<ParcelableKeyRing> it = entries.iterator();
|
||||
int numEntries = entries.size();
|
||||
|
||||
return importKeyRings(it, numEntries, keyServerUri);
|
||||
|
||||
}
|
||||
|
||||
public ImportKeyResult importKeyRings(ParcelableFileCache<ParcelableKeyRing> cache, String keyServerUri) {
|
||||
|
||||
// get entries from cached file
|
||||
try {
|
||||
IteratorWithSize<ParcelableKeyRing> it = cache.readCache();
|
||||
int numEntries = it.getSize();
|
||||
|
||||
return importKeyRings(it, numEntries, keyServerUri);
|
||||
} catch (IOException e) {
|
||||
|
||||
// Special treatment here, we need a lot
|
||||
OperationLog log = new OperationLog();
|
||||
log.add(LogType.MSG_IMPORT, 0, 0);
|
||||
log.add(LogType.MSG_IMPORT_ERROR_IO, 0, 0);
|
||||
|
||||
return new ImportKeyResult(ImportKeyResult.RESULT_ERROR, log);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public ImportKeyResult importKeyRings(Iterator<ParcelableKeyRing> entries, int num, String keyServerUri) {
|
||||
updateProgress(R.string.progress_importing, 0, 100);
|
||||
|
||||
@ -131,9 +165,7 @@ public class ImportExportOperation extends BaseOperation {
|
||||
|
||||
// If there aren't even any keys, do nothing here.
|
||||
if (entries == null || !entries.hasNext()) {
|
||||
return new ImportKeyResult(
|
||||
ImportKeyResult.RESULT_FAIL_NOTHING, log, 0, 0, 0, 0,
|
||||
new long[]{});
|
||||
return new ImportKeyResult(ImportKeyResult.RESULT_FAIL_NOTHING, log);
|
||||
}
|
||||
|
||||
int newKeys = 0, oldKeys = 0, badKeys = 0, secret = 0;
|
||||
@ -293,6 +325,16 @@ public class ImportExportOperation extends BaseOperation {
|
||||
position++;
|
||||
}
|
||||
|
||||
// Special: consolidate on secret key import (cannot be cancelled!)
|
||||
if (secret > 0) {
|
||||
setPreventCancel();
|
||||
ConsolidateResult result = mProviderHelper.consolidateDatabaseStep1(mProgressable);
|
||||
log.add(result, 1);
|
||||
}
|
||||
|
||||
// Special: make sure new data is synced into contacts
|
||||
ContactSyncAdapterService.requestSync();
|
||||
|
||||
// convert to long array
|
||||
long[] importedMasterKeyIdsArray = new long[importedMasterKeyIds.size()];
|
||||
for (int i = 0; i < importedMasterKeyIds.size(); ++i) {
|
||||
|
@ -41,6 +41,9 @@ public class DecryptVerifyResult extends OperationResult {
|
||||
|
||||
OpenPgpSignatureResult mSignatureResult;
|
||||
OpenPgpMetadata mDecryptMetadata;
|
||||
// This holds the charset which was specified in the ascii armor, if specified
|
||||
// https://tools.ietf.org/html/rfc4880#page56
|
||||
String mCharset;
|
||||
|
||||
public long getKeyIdPassphraseNeeded() {
|
||||
return mKeyIdPassphraseNeeded;
|
||||
@ -84,6 +87,14 @@ public class DecryptVerifyResult extends OperationResult {
|
||||
mDecryptMetadata = decryptMetadata;
|
||||
}
|
||||
|
||||
public String getCharset () {
|
||||
return mCharset;
|
||||
}
|
||||
|
||||
public void setCharset(String charset) {
|
||||
mCharset = charset;
|
||||
}
|
||||
|
||||
public boolean isPending() {
|
||||
return (mResult & RESULT_PENDING) == RESULT_PENDING;
|
||||
}
|
||||
|
@ -83,6 +83,10 @@ public class ImportKeyResult extends OperationResult {
|
||||
mImportedMasterKeyIds = source.createLongArray();
|
||||
}
|
||||
|
||||
public ImportKeyResult(int result, OperationLog log) {
|
||||
this(result, log, 0, 0, 0, 0, new long[] { });
|
||||
}
|
||||
|
||||
public ImportKeyResult(int result, OperationLog log,
|
||||
int newKeys, int updatedKeys, int badKeys, int secret,
|
||||
long[] importedMasterKeyIds) {
|
||||
|
@ -564,6 +564,7 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_DC_ASKIP_NO_KEY (LogLevel.DEBUG, R.string.msg_dc_askip_no_key),
|
||||
MSG_DC_ASKIP_NOT_ALLOWED (LogLevel.DEBUG, R.string.msg_dc_askip_not_allowed),
|
||||
MSG_DC_ASYM (LogLevel.DEBUG, R.string.msg_dc_asym),
|
||||
MSG_DC_CHARSET (LogLevel.DEBUG, R.string.msg_dc_charset),
|
||||
MSG_DC_CLEAR_DATA (LogLevel.DEBUG, R.string.msg_dc_clear_data),
|
||||
MSG_DC_CLEAR_DECOMPRESS (LogLevel.DEBUG, R.string.msg_dc_clear_decompress),
|
||||
MSG_DC_CLEAR_META_FILE (LogLevel.DEBUG, R.string.msg_dc_clear_meta_file),
|
||||
@ -654,6 +655,7 @@ public abstract class OperationResult implements Parcelable {
|
||||
MSG_IMPORT_FINGERPRINT_ERROR (LogLevel.ERROR, R.string.msg_import_fingerprint_error),
|
||||
MSG_IMPORT_FINGERPRINT_OK (LogLevel.DEBUG, R.string.msg_import_fingerprint_ok),
|
||||
MSG_IMPORT_ERROR (LogLevel.ERROR, R.string.msg_import_error),
|
||||
MSG_IMPORT_ERROR_IO (LogLevel.ERROR, R.string.msg_import_error_io),
|
||||
MSG_IMPORT_PARTIAL (LogLevel.ERROR, R.string.msg_import_partial),
|
||||
MSG_IMPORT_SUCCESS (LogLevel.OK, R.string.msg_import_success),
|
||||
|
||||
|
@ -234,6 +234,22 @@ public class PgpDecryptVerify extends BaseOperation {
|
||||
boolean symmetricPacketFound = false;
|
||||
boolean anyPacketFound = false;
|
||||
|
||||
// If the input stream is armored, and there is a charset specified, take a note for later
|
||||
// https://tools.ietf.org/html/rfc4880#page56
|
||||
String charset = null;
|
||||
if (in instanceof ArmoredInputStream) {
|
||||
for (String header : ((ArmoredInputStream) in).getArmorHeaders()) {
|
||||
String[] pieces = header.split(":", 2);
|
||||
if (pieces.length == 2 && "charset".equalsIgnoreCase(pieces[0])) {
|
||||
charset = pieces[1].trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (charset != null) {
|
||||
log.add(LogType.MSG_DC_CHARSET, indent, charset);
|
||||
}
|
||||
}
|
||||
|
||||
// go through all objects and find one we can decrypt
|
||||
while (it.hasNext()) {
|
||||
Object obj = it.next();
|
||||
@ -550,6 +566,7 @@ public class PgpDecryptVerify extends BaseOperation {
|
||||
log.add(LogType.MSG_DC_OK_META_ONLY, indent);
|
||||
DecryptVerifyResult result =
|
||||
new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
|
||||
result.setCharset(charset);
|
||||
result.setDecryptMetadata(metadata);
|
||||
return result;
|
||||
}
|
||||
@ -647,6 +664,7 @@ public class PgpDecryptVerify extends BaseOperation {
|
||||
new DecryptVerifyResult(DecryptVerifyResult.RESULT_OK, log);
|
||||
result.setDecryptMetadata(metadata);
|
||||
result.setSignatureResult(signatureResultBuilder.build());
|
||||
result.setCharset(charset);
|
||||
return result;
|
||||
|
||||
}
|
||||
@ -807,7 +825,7 @@ public class PgpDecryptVerify extends BaseOperation {
|
||||
while ((ch = fIn.read()) >= 0) {
|
||||
bOut.write(ch);
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
lookAhead = readPassedEOL(bOut, ch, fIn);
|
||||
lookAhead = readPastEOL(bOut, ch, fIn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -824,7 +842,7 @@ public class PgpDecryptVerify extends BaseOperation {
|
||||
do {
|
||||
bOut.write(ch);
|
||||
if (ch == '\r' || ch == '\n') {
|
||||
lookAhead = readPassedEOL(bOut, ch, fIn);
|
||||
lookAhead = readPastEOL(bOut, ch, fIn);
|
||||
break;
|
||||
}
|
||||
} while ((ch = fIn.read()) >= 0);
|
||||
@ -836,7 +854,7 @@ public class PgpDecryptVerify extends BaseOperation {
|
||||
return lookAhead;
|
||||
}
|
||||
|
||||
private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn)
|
||||
private static int readPastEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn)
|
||||
throws IOException {
|
||||
int lookAhead = fIn.read();
|
||||
|
||||
|
@ -83,6 +83,7 @@ public class PgpSignEncrypt extends BaseOperation {
|
||||
private boolean mDetachedSignature;
|
||||
private String mOriginalFilename;
|
||||
private boolean mFailOnMissingEncryptionKeyIds;
|
||||
private String mCharset;
|
||||
|
||||
private byte[] mNfcSignedHash = null;
|
||||
private Date mNfcCreationTimestamp = null;
|
||||
@ -121,6 +122,7 @@ public class PgpSignEncrypt extends BaseOperation {
|
||||
this.mNfcCreationTimestamp = builder.mNfcCreationTimestamp;
|
||||
this.mOriginalFilename = builder.mOriginalFilename;
|
||||
this.mFailOnMissingEncryptionKeyIds = builder.mFailOnMissingEncryptionKeyIds;
|
||||
this.mCharset = builder.mCharset;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
@ -149,6 +151,7 @@ public class PgpSignEncrypt extends BaseOperation {
|
||||
private byte[] mNfcSignedHash = null;
|
||||
private Date mNfcCreationTimestamp = null;
|
||||
private boolean mFailOnMissingEncryptionKeyIds = false;
|
||||
private String mCharset = null;
|
||||
|
||||
public Builder(Context context, ProviderHelper providerHelper, Progressable progressable,
|
||||
InputData data, OutputStream outStream) {
|
||||
@ -215,6 +218,11 @@ public class PgpSignEncrypt extends BaseOperation {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCharset(String charset) {
|
||||
mCharset = charset;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Also encrypt with the signing keyring
|
||||
*
|
||||
@ -286,6 +294,10 @@ public class PgpSignEncrypt extends BaseOperation {
|
||||
if (mVersionHeader != null) {
|
||||
armorOut.setHeader("Version", mVersionHeader);
|
||||
}
|
||||
// if we have a charset, put it in the header
|
||||
if (mCharset != null) {
|
||||
armorOut.setHeader("Charset", mCharset);
|
||||
}
|
||||
out = armorOut;
|
||||
} else {
|
||||
out = mOutStream;
|
||||
|
@ -563,6 +563,11 @@ public class OpenPgpService extends RemoteService {
|
||||
}
|
||||
}
|
||||
|
||||
String charset = pgpResult.getCharset();
|
||||
if (charset != null) {
|
||||
result.putExtra(OpenPgpApi.RESULT_CHARSET, charset);
|
||||
}
|
||||
|
||||
result.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS);
|
||||
return result;
|
||||
} else {
|
||||
|
@ -403,49 +403,28 @@ public class KeychainIntentService extends IntentService implements Progressable
|
||||
|
||||
break;
|
||||
}
|
||||
case ACTION_IMPORT_KEYRING:
|
||||
case ACTION_IMPORT_KEYRING: {
|
||||
|
||||
try {
|
||||
// Input
|
||||
String keyServer = data.getString(IMPORT_KEY_SERVER);
|
||||
ArrayList<ParcelableKeyRing> list = data.getParcelableArrayList(IMPORT_KEY_LIST);
|
||||
ParcelableFileCache<ParcelableKeyRing> cache =
|
||||
new ParcelableFileCache<>(this, "key_import.pcl");
|
||||
|
||||
// Input
|
||||
String keyServer = data.getString(IMPORT_KEY_SERVER);
|
||||
Iterator<ParcelableKeyRing> entries;
|
||||
int numEntries;
|
||||
if (data.containsKey(IMPORT_KEY_LIST)) {
|
||||
// get entries from intent
|
||||
ArrayList<ParcelableKeyRing> list = data.getParcelableArrayList(IMPORT_KEY_LIST);
|
||||
entries = list.iterator();
|
||||
numEntries = list.size();
|
||||
} else {
|
||||
// get entries from cached file
|
||||
ParcelableFileCache<ParcelableKeyRing> cache =
|
||||
new ParcelableFileCache<>(this, "key_import.pcl");
|
||||
IteratorWithSize<ParcelableKeyRing> it = cache.readCache();
|
||||
entries = it;
|
||||
numEntries = it.getSize();
|
||||
}
|
||||
// Operation
|
||||
ImportExportOperation importExportOperation = new ImportExportOperation(
|
||||
this, providerHelper, this, mActionCanceled);
|
||||
// Either list or cache must be null, no guarantees otherwise.
|
||||
ImportKeyResult result = list != null
|
||||
? importExportOperation.importKeyRings(list, keyServer)
|
||||
: importExportOperation.importKeyRings(cache, keyServer);
|
||||
|
||||
// Operation
|
||||
ImportExportOperation importExportOperation = new ImportExportOperation(
|
||||
this, providerHelper, this, mActionCanceled);
|
||||
ImportKeyResult result = importExportOperation.importKeyRings(entries, numEntries, keyServer);
|
||||
|
||||
// Special: consolidate on secret key import (cannot be cancelled!)
|
||||
if (result.mSecret > 0) {
|
||||
// TODO move this into the import operation
|
||||
providerHelper.consolidateDatabaseStep1(this);
|
||||
}
|
||||
|
||||
// Special: make sure new data is synced into contacts
|
||||
ContactSyncAdapterService.requestSync();
|
||||
|
||||
// Result
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, result);
|
||||
} catch (Exception e) {
|
||||
sendErrorToHandler(e);
|
||||
}
|
||||
// Result
|
||||
sendMessageToHandler(KeychainIntentServiceHandler.MESSAGE_OKAY, result);
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
case ACTION_SIGN_ENCRYPT:
|
||||
|
||||
try {
|
||||
|
@ -41,6 +41,8 @@ import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
import org.sufficientlysecure.keychain.util.ShareHelper;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
public class DecryptTextFragment extends DecryptFragment {
|
||||
public static final String ARG_CIPHERTEXT = "ciphertext";
|
||||
|
||||
@ -194,7 +196,18 @@ public class DecryptTextFragment extends DecryptFragment {
|
||||
|
||||
byte[] decryptedMessage = returnData
|
||||
.getByteArray(KeychainIntentService.RESULT_DECRYPTED_BYTES);
|
||||
mText.setText(new String(decryptedMessage));
|
||||
String displayMessage;
|
||||
if (pgpResult.getCharset() != null) {
|
||||
try {
|
||||
displayMessage = new String(decryptedMessage, pgpResult.getCharset());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// if we can't decode properly, just fall back to utf-8
|
||||
displayMessage = new String(decryptedMessage);
|
||||
}
|
||||
} else {
|
||||
displayMessage = new String(decryptedMessage);
|
||||
}
|
||||
mText.setText(displayMessage);
|
||||
|
||||
pgpResult.createNotify(getActivity()).show();
|
||||
|
||||
|
@ -39,18 +39,6 @@ import java.util.ArrayList;
|
||||
public class ImportKeysListLoader
|
||||
extends AsyncTaskLoader<AsyncTaskResultWrapper<ArrayList<ImportKeysListEntry>>> {
|
||||
|
||||
public static class NonPgpPartException extends Exception {
|
||||
private int mCount;
|
||||
|
||||
public NonPgpPartException(int count) {
|
||||
this.mCount = count;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return mCount;
|
||||
}
|
||||
}
|
||||
|
||||
final Context mContext;
|
||||
final InputData mInputData;
|
||||
|
||||
|
@ -924,6 +924,7 @@
|
||||
<string name="msg_dc_askip_no_key">"Data not encrypted with known key, skipping…"</string>
|
||||
<string name="msg_dc_askip_not_allowed">"Data not encrypted with allowed key, skipping…"</string>
|
||||
<string name="msg_dc_asym">"Found block of asymmetrically encrypted data for key %s"</string>
|
||||
<string name="msg_dc_charset">"Found charset header: '%s'"</string>
|
||||
<string name="msg_dc_clear_data">"Processing literal data"</string>
|
||||
<string name="msg_dc_clear_decompress">"Unpacking compressed data"</string>
|
||||
<string name="msg_dc_clear_meta_file">"Filename: %s"</string>
|
||||
@ -1022,6 +1023,7 @@
|
||||
<string name="msg_import_fingerprint_ok">"Fingerprint check OK"</string>
|
||||
<string name="msg_import_merge">"Merging retrieved data"</string>
|
||||
<string name="msg_import_error">"Import operation failed!"</string>
|
||||
<string name="msg_import_error_io">"Import operation failed due to i/o error!"</string>
|
||||
<string name="msg_import_partial">"Import operation successful, with errors!"</string>
|
||||
<string name="msg_import_success">"Import operation successful!"</string>
|
||||
|
||||
|
2
extern/openpgp-api-lib
vendored
2
extern/openpgp-api-lib
vendored
@ -1 +1 @@
|
||||
Subproject commit 76d7b17f114ef180fdbe6852d8897e8b9a4bca8b
|
||||
Subproject commit edcc61970f24bb3601af26d89f49d384e7a3aff6
|
Loading…
Reference in New Issue
Block a user