1package eu.siacs.conversations.services;
2
3import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
4
5import android.os.PowerManager;
6import android.os.SystemClock;
7import android.util.Log;
8import androidx.annotation.Nullable;
9import androidx.core.content.ContextCompat;
10import eu.siacs.conversations.Config;
11import eu.siacs.conversations.R;
12import eu.siacs.conversations.entities.DownloadableFile;
13import eu.siacs.conversations.utils.Compatibility;
14import java.io.FileInputStream;
15import java.io.FileNotFoundException;
16import java.io.FileOutputStream;
17import java.io.IOException;
18import java.io.InputStream;
19import java.io.OutputStream;
20import java.util.concurrent.atomic.AtomicLong;
21import okhttp3.MediaType;
22import okhttp3.RequestBody;
23import okio.BufferedSink;
24import okio.Okio;
25import okio.Source;
26import org.bouncycastle.crypto.engines.AESEngine;
27import org.bouncycastle.crypto.io.CipherInputStream;
28import org.bouncycastle.crypto.io.CipherOutputStream;
29import org.bouncycastle.crypto.modes.AEADBlockCipher;
30import org.bouncycastle.crypto.modes.GCMBlockCipher;
31import org.bouncycastle.crypto.params.AEADParameters;
32import org.bouncycastle.crypto.params.KeyParameter;
33
34public class AbstractConnectionManager {
35
36 private static final int UI_REFRESH_THRESHOLD = 250;
37 private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0);
38 protected XmppConnectionService mXmppConnectionService;
39
40 public AbstractConnectionManager(XmppConnectionService service) {
41 this.mXmppConnectionService = service;
42 }
43
44 public static InputStream upgrade(DownloadableFile file, InputStream is) {
45 if (file.getKey() != null && file.getIv() != null) {
46 AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
47 cipher.init(
48 true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
49 return new CipherInputStream(is, cipher);
50 } else {
51 return is;
52 }
53 }
54
55 // For progress tracking see:
56 // https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
57
58 public static RequestBody requestBody(
59 final DownloadableFile file, final ProgressListener progressListener) {
60 return new RequestBody() {
61
62 @Override
63 public long contentLength() {
64 return file.getSize() + (file.getKey() != null ? 16 : 0);
65 }
66
67 @Nullable
68 @Override
69 public MediaType contentType() {
70 return MediaType.parse(file.getMimeType());
71 }
72
73 @Override
74 public void writeTo(final BufferedSink sink) throws IOException {
75 long transmitted = 0;
76 try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) {
77 long read;
78 while ((read = source.read(sink.buffer(), 8196)) != -1) {
79 transmitted += read;
80 sink.flush();
81 progressListener.onProgress(transmitted);
82 }
83 }
84 }
85 };
86 }
87
88 public interface ProgressListener {
89 void onProgress(long progress);
90 }
91
92 public static OutputStream createOutputStream(
93 DownloadableFile file, boolean append, boolean decrypt) {
94 FileOutputStream os;
95 try {
96 os = new FileOutputStream(file, append);
97 if (file.getKey() == null || !decrypt) {
98 return os;
99 }
100 } catch (FileNotFoundException e) {
101 Log.d(Config.LOGTAG, "unable to create output stream", e);
102 return null;
103 }
104 try {
105 AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
106 cipher.init(
107 false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
108 return new CipherOutputStream(os, cipher);
109 } catch (Exception e) {
110 Log.d(Config.LOGTAG, "unable to create cipher output stream", e);
111 return null;
112 }
113 }
114
115 public XmppConnectionService getXmppConnectionService() {
116 return this.mXmppConnectionService;
117 }
118
119 public long getAutoAcceptFileSize() {
120 final long autoAcceptFileSize =
121 this.mXmppConnectionService.getLongPreference(
122 "auto_accept_file_size", R.integer.auto_accept_filesize);
123 return autoAcceptFileSize <= 0 ? -1 : autoAcceptFileSize;
124 }
125
126 public boolean hasStoragePermission() {
127 return Compatibility.hasStoragePermission(mXmppConnectionService);
128 }
129
130 public void updateConversationUi(boolean force) {
131 synchronized (LAST_UI_UPDATE_CALL) {
132 if (force
133 || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get()
134 >= UI_REFRESH_THRESHOLD) {
135 LAST_UI_UPDATE_CALL.set(SystemClock.elapsedRealtime());
136 mXmppConnectionService.updateConversationUi();
137 }
138 }
139 }
140
141 public PowerManager.WakeLock createWakeLock(final String name) {
142 final PowerManager powerManager =
143 ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class);
144 return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
145 }
146
147 public static class Extension {
148 public final String main;
149 public final String secondary;
150
151 private Extension(String main, String secondary) {
152 this.main = main;
153 this.secondary = secondary;
154 }
155
156 public String getExtension() {
157 if (VALID_CRYPTO_EXTENSIONS.contains(main)) {
158 return secondary;
159 } else {
160 return main;
161 }
162 }
163
164 public static Extension of(String path) {
165 // TODO accept List<String> pathSegments
166 final int pos = path.lastIndexOf('/');
167 final String filename = path.substring(pos + 1).toLowerCase();
168 final String[] parts = filename.split("\\.");
169 final String main = parts.length >= 2 ? parts[parts.length - 1] : null;
170 final String secondary = parts.length >= 3 ? parts[parts.length - 2] : null;
171 return new Extension(main, secondary);
172 }
173 }
174}