1package eu.siacs.conversations.services;
2
3import android.app.Notification;
4import android.app.NotificationManager;
5import android.app.Service;
6import android.content.Context;
7import android.content.Intent;
8import android.database.Cursor;
9import android.database.DatabaseUtils;
10import android.database.sqlite.SQLiteDatabase;
11import android.os.IBinder;
12import android.support.v4.app.NotificationCompat;
13import android.util.Log;
14
15import java.io.DataOutputStream;
16import java.io.File;
17import java.io.FileOutputStream;
18import java.io.PrintWriter;
19import java.security.NoSuchAlgorithmException;
20import java.security.SecureRandom;
21import java.security.spec.InvalidKeySpecException;
22import java.util.Arrays;
23import java.util.List;
24import java.util.concurrent.atomic.AtomicBoolean;
25import java.util.zip.GZIPOutputStream;
26
27import javax.crypto.Cipher;
28import javax.crypto.CipherOutputStream;
29import javax.crypto.SecretKeyFactory;
30import javax.crypto.spec.IvParameterSpec;
31import javax.crypto.spec.PBEKeySpec;
32import javax.crypto.spec.SecretKeySpec;
33
34import eu.siacs.conversations.Config;
35import eu.siacs.conversations.R;
36import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
37import eu.siacs.conversations.entities.Account;
38import eu.siacs.conversations.entities.Conversation;
39import eu.siacs.conversations.entities.Message;
40import eu.siacs.conversations.persistance.DatabaseBackend;
41import eu.siacs.conversations.persistance.FileBackend;
42import eu.siacs.conversations.utils.BackupFileHeader;
43import eu.siacs.conversations.utils.Compatibility;
44
45public class ExportBackupService extends Service {
46
47 public static final String KEYTYPE = "AES";
48 public static final String CIPHERMODE = "AES/GCM/NoPadding";
49 public static final String PROVIDER = "BC";
50
51 private static final int NOTIFICATION_ID = 19;
52 private static AtomicBoolean running = new AtomicBoolean(false);
53 private DatabaseBackend mDatabaseBackend;
54 private List<Account> mAccounts;
55 private NotificationManager notificationManager;
56
57 @Override
58 public void onCreate() {
59 mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
60 mAccounts = mDatabaseBackend.getAccounts();
61 notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
62 }
63
64 @Override
65 public int onStartCommand(Intent intent, int flags, int startId) {
66 if (running.compareAndSet(false, true)) {
67 new Thread(() -> {
68 export();
69 stopForeground(true);
70 running.set(false);
71 stopSelf();
72 }).start();
73 }
74 return START_NOT_STICKY;
75 }
76
77 private static void accountExport(SQLiteDatabase db, String uuid, PrintWriter writer) {
78 StringBuilder builder = new StringBuilder();
79 final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null);
80 while (accountCursor != null && accountCursor.moveToNext()) {
81 builder.append("INSERT INTO ").append(Account.TABLENAME).append("(");
82 for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
83 if (i != 0) {
84 builder.append(',');
85 }
86 builder.append(accountCursor.getColumnName(i));
87 }
88 builder.append(") VALUES(");
89 for (int i = 0; i < accountCursor.getColumnCount(); ++i) {
90 if (i != 0) {
91 builder.append(',');
92 }
93 final String value = accountCursor.getString(i);
94 if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) {
95 builder.append("NULL");
96 } else if (value.matches("\\d+")) {
97 int intValue = Integer.parseInt(value);
98 Log.d(Config.LOGTAG,"reading int value. "+intValue);
99 if (Account.OPTIONS.equals(accountCursor.getColumnName(i))) {
100 intValue |= 1 << Account.OPTION_DISABLED;
101 Log.d(Config.LOGTAG,"modified int value "+intValue);
102 }
103 builder.append(intValue);
104 } else {
105 DatabaseUtils.appendEscapedSQLString(builder, value);
106 }
107 }
108 builder.append(")");
109 builder.append(';');
110 builder.append('\n');
111 }
112 Log.d(Config.LOGTAG,builder.toString());
113 if (accountCursor != null) {
114 accountCursor.close();
115 }
116 writer.append(builder.toString());
117 }
118
119 private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
120 Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
121 int size = cursor != null ? cursor.getCount() : 0;
122 Log.d(Config.LOGTAG, "exporting " + size + " messages");
123 int i = 0;
124 int p = 0;
125 while (cursor != null && cursor.moveToNext()) {
126 writer.write(cursorToString(Message.TABLENAME, cursor, 20));
127 if (i + 20 > size) {
128 i = size;
129 } else {
130 i += 20;
131 }
132 final int percentage = i * 100 / size;
133 if (p < percentage) {
134 p = percentage;
135 notificationManager.notify(NOTIFICATION_ID,progress.build(p));
136 Log.d(Config.LOGTAG, "percentage=" + p);
137 }
138 }
139 if (cursor != null) {
140 cursor.close();
141 }
142 }
143
144 private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) {
145 final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null);
146 while (cursor != null && cursor.moveToNext()) {
147 writer.write(cursorToString(table, cursor, 20));
148 }
149 if (cursor != null) {
150 cursor.close();
151 }
152 }
153
154 private void export() {
155 NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
156 mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
157 .setSmallIcon(R.drawable.ic_archive_white_24dp)
158 .setProgress(1, 0, false);
159 startForeground(NOTIFICATION_ID, mBuilder.build());
160 try {
161 int count = 0;
162 final int max = this.mAccounts.size();
163 final SecureRandom secureRandom = new SecureRandom();
164 for (Account account : this.mAccounts) {
165 final byte[] IV = new byte[12];
166 final byte[] salt = new byte[16];
167 secureRandom.nextBytes(IV);
168 secureRandom.nextBytes(salt);
169 final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name),account.getJid(),System.currentTimeMillis(),IV,salt);
170 final Progress progress = new Progress(mBuilder, max, count);
171 final File file = new File(FileBackend.getBackupDirectory(this)+account.getJid().asBareJid().toEscapedString()+".ceb");
172 if (file.getParentFile().mkdirs()) {
173 Log.d(Config.LOGTAG,"created backup directory "+file.getParentFile().getAbsolutePath());
174 }
175 final FileOutputStream fileOutputStream = new FileOutputStream(file);
176 final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
177 backupFileHeader.write(dataOutputStream);
178 dataOutputStream.flush();
179
180 final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
181 byte[] key = getKey(account.getPassword(), salt);
182 Log.d(Config.LOGTAG,backupFileHeader.toString());
183 SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
184 IvParameterSpec ivSpec = new IvParameterSpec(IV);
185 cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
186 CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
187
188 GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
189 PrintWriter writer = new PrintWriter(gzipOutputStream);
190 SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
191 final String uuid = account.getUuid();
192 accountExport(db, uuid, writer);
193 simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
194 messageExport(db, uuid, writer, progress);
195 for(String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
196 simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT,uuid,writer);
197 }
198 writer.flush();
199 writer.close();
200 Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
201 count++;
202 }
203 } catch (Exception e) {
204 Log.d(Config.LOGTAG, "unable to create backup ", e);
205 }
206 }
207
208 public static byte[] getKey(String password, byte[] salt) {
209 try {
210 SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
211 return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
212 } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
213 throw new AssertionError(e);
214 }
215 }
216
217 private static String cursorToString(String tablename, Cursor cursor, int max) {
218 StringBuilder builder = new StringBuilder();
219 builder.append("INSERT INTO ").append(tablename).append("(");
220 for (int i = 0; i < cursor.getColumnCount(); ++i) {
221 if (i != 0) {
222 builder.append(',');
223 }
224 builder.append(cursor.getColumnName(i));
225 }
226 builder.append(") VALUES");
227 for (int i = 0; i < max; ++i) {
228 if (i != 0) {
229 builder.append(',');
230 }
231 appendValues(cursor, builder);
232 if (!cursor.moveToNext()) {
233 break;
234 }
235 }
236 builder.append(';');
237 builder.append('\n');
238 return builder.toString();
239 }
240
241 private static void appendValues(Cursor cursor, StringBuilder builder) {
242 builder.append("(");
243 for (int i = 0; i < cursor.getColumnCount(); ++i) {
244 if (i != 0) {
245 builder.append(',');
246 }
247 final String value = cursor.getString(i);
248 if (value == null) {
249 builder.append("NULL");
250 } else if (value.matches("\\d+")) {
251 builder.append(value);
252 } else {
253 DatabaseUtils.appendEscapedSQLString(builder, value);
254 }
255 }
256 builder.append(")");
257
258 }
259
260 @Override
261 public IBinder onBind(Intent intent) {
262 return null;
263 }
264
265 private class Progress {
266 private final NotificationCompat.Builder builder;
267 private final int max;
268 private final int count;
269
270 private Progress(NotificationCompat.Builder builder, int max, int count) {
271 this.builder = builder;
272 this.max = max;
273 this.count = count;
274 }
275
276 private Notification build(int percentage) {
277 builder.setProgress(max * 100,count * 100 + percentage,false);
278 return builder.build();
279 }
280 }
281}