Detailed changes
@@ -20,6 +20,7 @@ repositories {
google()
mavenCentral()
jcenter()
+ maven { url 'https://jitpack.io' }
}
// https://stackoverflow.com/a/38105112/8611
@@ -103,6 +104,7 @@ dependencies {
implementation 'io.michaelrocks:libphonenumber-android:8.12.49'
implementation 'io.github.nishkarsh:android-permissions:2.1.6'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'com.github.ipld:java-cid:v1.3.1'
implementation urlFile('https://gateway.pinata.cloud/ipfs/QmeqMiLxHi8AAjXobxr3QTfa1bSSLyAu86YviAqQnjxCjM/libwebrtc.aar', 'libwebrtc.aar')
// INSERT
}
@@ -3,10 +3,15 @@ package com.cheogram.android;
import android.util.Base64;
import android.util.Log;
+import java.io.ByteArrayInputStream;
+import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
+
+import io.ipfs.cid.Cid;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ -26,29 +31,36 @@ public class BobTransfer implements Transferable {
protected Message message;
protected URI uri;
protected XmppConnectionService xmppConnectionService;
- protected DownloadableFile file;
+
+ public static Cid cid(URI uri) {
+ String bobCid = uri.getSchemeSpecificPart();
+ if (!bobCid.contains("@") || !bobCid.contains("+")) return null;
+ String[] cidParts = bobCid.split("@")[0].split("\\+");
+ try {
+ return CryptoHelper.cid(CryptoHelper.hexToBytes(cidParts[1]), cidParts[0]);
+ } catch (final NoSuchAlgorithmException e) {
+ return null;
+ }
+ }
public BobTransfer(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException {
this.message = message;
this.xmppConnectionService = xmppConnectionService;
this.uri = new URI(message.getFileParams().url);
- setupFile();
- }
-
- private void setupFile() {
- final String reference = uri.getFragment();
- if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) {
- this.file = new DownloadableFile(xmppConnectionService.getCacheDir(), message.getUuid());
- this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
- Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
- } else {
- this.file = xmppConnectionService.getFileBackend().getFile(message, false);
- }
}
@Override
public boolean start() {
if (status == Transferable.STATUS_DOWNLOADING) return true;
+ File f = xmppConnectionService.getFileForCid(cid(uri));
+
+ if (f != null && f.canRead()) {
+ message.setRelativeFilePath(f.getAbsolutePath());
+ finish();
+ message.setTransferable(null);
+ xmppConnectionService.updateConversationUi();
+ return true;
+ }
if (xmppConnectionService.hasInternetConnection()) {
changeStatus(Transferable.STATUS_DOWNLOADING);
@@ -64,29 +76,27 @@ public class BobTransfer implements Transferable {
xmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
} else {
final String contentType = data.getAttribute("type");
+ String fileExtension = "dat";
if (contentType != null) {
- final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
- if (fileExtension != null) {
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), fileExtension), contentType);
- Log.d(Config.LOGTAG, "rewriting name for bob based on content type");
- setupFile();
- }
+ fileExtension = MimeUtils.guessExtensionFromMimeType(contentType);
}
try {
+ final byte[] bytes = Base64.decode(data.getContent(), Base64.DEFAULT);
+
+ xmppConnectionService.getFileBackend().setupRelativeFilePath(message, new ByteArrayInputStream(bytes), fileExtension);
+ DownloadableFile file = xmppConnectionService.getFileBackend().getFile(message);
file.getParentFile().mkdirs();
if (!file.exists() && !file.createNewFile()) {
throw new IOException(file.getAbsolutePath());
}
+
final OutputStream outputStream = AbstractConnectionManager.createOutputStream(file, false, false);
- outputStream.write(Base64.decode(data.getContent(), Base64.DEFAULT));
+ outputStream.write(bytes);
outputStream.flush();
outputStream.close();
- final boolean privateMessage = message.isPrivateMessage();
- message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
- xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString());
- xmppConnectionService.updateMessage(message);
+ finish();
} catch (IOException e) {
xmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
}
@@ -126,4 +136,11 @@ public class BobTransfer implements Transferable {
status = newStatus;
xmppConnectionService.updateConversationUi();
}
+
+ protected void finish() {
+ final boolean privateMessage = message.isPrivateMessage();
+ message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
+ xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString(), false);
+ xmppConnectionService.updateMessage(message);
+ }
}
@@ -192,9 +192,18 @@ public class HttpDownloadConnection implements Transferable {
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
}
- mHttpConnectionManager.updateConversationUi(true);
+ DownloadableFile file;
+ final DownloadableFile tmp = mXmppConnectionService.getFileBackend().getFile(message);
+ final String extension = MimeUtils.extractRelevantExtension(tmp.getName());
+ try {
+ mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, new FileInputStream(tmp), extension);
+ file = mXmppConnectionService.getFileBackend().getFile(message);
+ tmp.renameTo(file);
+ } catch (final IOException e) {
+ file = tmp;
+ }
+ mXmppConnectionService.updateMessage(message);
final boolean notifyAfterScan = notify;
- final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message, true);
mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> {
if (notifyAfterScan) {
mXmppConnectionService.getNotificationService().push(message);
@@ -39,6 +39,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
+import io.ipfs.cid.Cid;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ -252,6 +254,16 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("PRAGMA cheogram.user_version = 3");
}
+ if(cheogramVersion < 4) {
+ db.execSQL(
+ "CREATE TABLE cheogram.cids (" +
+ "cid TEXT NOT NULL PRIMARY KEY," +
+ "path TEXT NOT NULL" +
+ ")"
+ );
+ db.execSQL("PRAGMA cheogram.user_version = 4");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
@@ -721,6 +733,25 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cursor.close();
}
+ public File getFileForCid(Cid cid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.query("cheogram.cids", new String[]{"path"}, "cid=?", new String[]{cid.toString()}, null, null, null);
+ File f = null;
+ if (cursor.moveToNext()) {
+ f = new File(cursor.getString(0));
+ }
+ cursor.close();
+ return f;
+ }
+
+ public void saveCid(Cid cid, File file) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues cv = new ContentValues();
+ cv.put("cid", cid.toString());
+ cv.put("path", file.getAbsolutePath());
+ db.insertWithOnConflict("cheogram.cids", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+
public void createConversation(Conversation conversation) {
SQLiteDatabase db = this.getWritableDatabase();
db.insert(Conversation.TABLENAME, null, conversation.getContentValues());
@@ -62,6 +62,8 @@ import java.util.List;
import java.util.Locale;
import java.util.UUID;
+import io.ipfs.cid.Cid;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
@@ -712,7 +714,8 @@ public class FileBackend {
if ("ogg".equals(extension) && type != null && type.startsWith("audio/")) {
extension = "oga";
}
- setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), extension));
+
+ setupRelativeFilePath(message, uri, extension);
copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri);
}
@@ -850,8 +853,46 @@ public class FileBackend {
throw new IllegalStateException("Unknown image format");
}
setupRelativeFilePath(message, filename);
- copyImageToPrivateStorage(getFile(message), image);
- updateFileParams(message);
+ final File tmp = getFile(message);
+ copyImageToPrivateStorage(tmp, image);
+ final String extension = MimeUtils.extractRelevantExtension(filename);
+ try {
+ setupRelativeFilePath(message, new FileInputStream(tmp), extension);
+ } catch (final FileNotFoundException e) {
+ throw new FileCopyException(R.string.error_file_not_found);
+ } catch (final IOException e) {
+ throw new FileCopyException(R.string.error_io_exception);
+ }
+ tmp.renameTo(getFile(message));
+ updateFileParams(message, null, false);
+ }
+
+ public void setupRelativeFilePath(final Message message, final Uri uri, final String extension) throws FileCopyException {
+ try {
+ setupRelativeFilePath(message, mXmppConnectionService.getContentResolver().openInputStream(uri), extension);
+ } catch (final FileNotFoundException e) {
+ throw new FileCopyException(R.string.error_file_not_found);
+ } catch (final IOException e) {
+ throw new FileCopyException(R.string.error_io_exception);
+ }
+ }
+
+ public Cid[] calculateCids(final InputStream is) throws IOException {
+ try {
+ return CryptoHelper.cid(is, new String[]{"SHA-256", "SHA-1", "SHA-512"});
+ } catch (final NoSuchAlgorithmException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public void setupRelativeFilePath(final Message message, final InputStream is, final String extension) throws IOException {
+ Cid[] cids = calculateCids(is);
+
+ setupRelativeFilePath(message, String.format("%s.%s", cids[0], extension));
+ File file = getFile(message);
+ for (int i = 0; i < cids.length; i++) {
+ mXmppConnectionService.saveCid(cids[i], file);
+ }
}
public void setupRelativeFilePath(final Message message, final String filename) {
@@ -1533,6 +1574,10 @@ public class FileBackend {
}
public void updateFileParams(final Message message, final String url) {
+ updateFileParams(message, url, true);
+ }
+
+ public void updateFileParams(final Message message, final String url, boolean updateCids) {
final boolean encrypted =
message.getEncryption() == Message.ENCRYPTION_PGP
|| message.getEncryption() == Message.ENCRYPTION_DECRYPTED;
@@ -1602,6 +1647,15 @@ public class FileBackend {
privateMessage
? Message.TYPE_PRIVATE_FILE
: (image ? Message.TYPE_IMAGE : Message.TYPE_FILE));
+
+ if (updateCids) {
+ try {
+ Cid[] cids = calculateCids(new FileInputStream(getFile(message)));
+ for (int i = 0; i < cids.length; i++) {
+ mXmppConnectionService.saveCid(cids[i], file);
+ }
+ } catch (final IOException e) { }
+ }
}
private int getMediaRuntime(final File file) {
@@ -86,6 +86,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
+import io.ipfs.cid.Cid;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.android.JabberIdContact;
@@ -547,6 +549,14 @@ public class XmppConnectionService extends Service {
return this.fileBackend;
}
+ public File getFileForCid(Cid cid) {
+ return this.databaseBackend.getFileForCid(cid);
+ }
+
+ public void saveCid(Cid cid, File file) {
+ this.databaseBackend.saveCid(cid, file);
+ }
+
public AvatarService getAvatarService() {
return this.mAvatarService;
}
@@ -9,6 +9,8 @@ import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
+import java.io.InputStream;
+import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -25,6 +27,9 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.regex.Pattern;
+import io.ipfs.cid.Cid;
+import io.ipfs.multihash.Multihash;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@@ -282,4 +287,41 @@ public final class CryptoHelper {
final String u = url.toLowerCase();
return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://")) && u.endsWith(".pgp");
}
+
+ public static Multihash.Type multihashType(String algo) throws NoSuchAlgorithmException {
+ if (algo.equals("SHA-1") || algo.equals("sha-1") || algo.equals("sha1")) {
+ return Multihash.Type.sha1;
+ } else if (algo.equals("SHA-256") || algo.equals("sha-256")) {
+ return Multihash.Type.sha2_256;
+ } else if (algo.equals("SHA-512") | algo.equals("sha-512")) {
+ return Multihash.Type.sha2_512;
+ } else {
+ throw new NoSuchAlgorithmException(algo);
+ }
+ }
+
+ public static Cid cid(byte[] digest, String algo) throws NoSuchAlgorithmException {
+ return Cid.buildCidV1(Cid.Codec.Raw, multihashType(algo), digest);
+ }
+
+ public static Cid[] cid(InputStream in, String[] algo) throws NoSuchAlgorithmException, IOException {
+ byte[] buf = new byte[4096];
+ int len;
+ MessageDigest[] md = new MessageDigest[algo.length];
+ for (int i = 0; i < md.length; i++) {
+ md[i] = MessageDigest.getInstance(algo[i]);
+ }
+ while ((len = in.read(buf)) != -1) {
+ for (int i = 0; i < md.length; i++) {
+ md[i].update(buf, 0, len);
+ }
+ }
+
+ Cid[] cid = new Cid[md.length];
+ for (int i = 0; i < cid.length; i++) {
+ cid[i] = cid(md[i].digest(), algo[i]);
+ }
+
+ return cid;
+ }
}
@@ -39,6 +39,7 @@ import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
@@ -114,6 +115,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
@Override
public void onFileTransmitted(DownloadableFile file) {
+ DownloadableFile finalFile;
+
if (responding()) {
if (expectedHash.length > 0) {
if (Arrays.equals(expectedHash, file.getSha1Sum())) {
@@ -125,7 +128,17 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party did not include file hash in file transfer");
}
sendSuccess();
- xmppConnectionService.getFileBackend().updateFileParams(message);
+
+ final String extension = MimeUtils.extractRelevantExtension(file.getName());
+ try {
+ xmppConnectionService.getFileBackend().setupRelativeFilePath(message, new FileInputStream(file), extension);
+ finalFile = xmppConnectionService.getFileBackend().getFile(message);
+ file.renameTo(finalFile);
+ } catch (final IOException e) {
+ finalFile = file;
+ }
+
+ xmppConnectionService.getFileBackend().updateFileParams(message, null, false);
xmppConnectionService.databaseBackend.createMessage(message);
xmppConnectionService.markMessage(message, Message.STATUS_RECEIVED);
if (acceptedAutomatically) {
@@ -142,6 +155,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
id.account.getPgpDecryptionService().decrypt(message, true);
}
} else {
+ finalFile = file;
+
if (description.getVersion() == FileTransferDescription.Version.FT_5) { //older Conversations will break when receiving a session-info
sendHash();
}
@@ -149,13 +164,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
id.account.getPgpDecryptionService().decrypt(message, false);
}
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
- file.delete();
+ finalFile.delete();
}
disconnectSocks5Connections();
}
- Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
+ Log.d(Config.LOGTAG, "successfully transmitted file:" + finalFile.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
if (message.getEncryption() != Message.ENCRYPTION_PGP) {
- xmppConnectionService.getFileBackend().updateMediaScanner(file);
+ xmppConnectionService.getFileBackend().updateMediaScanner(finalFile);
}
}