That fuck shit the fascists are using
1/*
2 * Copyright 2023 Signal Messenger, LLC
3 * SPDX-License-Identifier: AGPL-3.0-only
4 */
5
6package org.tm.archive.backup;
7
8import androidx.annotation.NonNull;
9
10import org.signal.core.util.Conversions;
11import org.signal.core.util.logging.Log;
12import org.signal.libsignal.protocol.kdf.HKDF;
13import org.signal.libsignal.protocol.util.ByteUtil;
14import org.tm.archive.attachments.AttachmentId;
15import org.tm.archive.backup.proto.Attachment;
16import org.tm.archive.backup.proto.Avatar;
17import org.tm.archive.backup.proto.BackupFrame;
18import org.tm.archive.backup.proto.DatabaseVersion;
19import org.tm.archive.backup.proto.Header;
20import org.tm.archive.backup.proto.KeyValue;
21import org.tm.archive.backup.proto.SharedPreference;
22import org.tm.archive.backup.proto.SqlStatement;
23import org.tm.archive.backup.proto.Sticker;
24import org.tm.archive.util.Util;
25
26import java.io.IOException;
27import java.io.InputStream;
28import java.io.OutputStream;
29import java.security.InvalidAlgorithmParameterException;
30import java.security.InvalidKeyException;
31import java.security.NoSuchAlgorithmException;
32
33import javax.crypto.BadPaddingException;
34import javax.crypto.Cipher;
35import javax.crypto.IllegalBlockSizeException;
36import javax.crypto.Mac;
37import javax.crypto.NoSuchPaddingException;
38import javax.crypto.spec.IvParameterSpec;
39import javax.crypto.spec.SecretKeySpec;
40
41class BackupFrameOutputStream extends FullBackupBase.BackupStream {
42
43 private static final String TAG = Log.tag(BackupFrameOutputStream.class);
44
45 private final OutputStream outputStream;
46 private final Cipher cipher;
47 private final Mac mac;
48
49 private final byte[] cipherKey;
50 private final byte[] iv;
51 private int counter;
52
53 private int frames;
54
55 BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException {
56 try {
57 byte[] salt = Util.getSecretBytes(32);
58 byte[] key = getBackupKey(passphrase, salt);
59 byte[] derived = HKDF.deriveSecrets(key, "Backup Export".getBytes(), 64);
60 byte[][] split = ByteUtil.split(derived, 32, 32);
61
62 this.cipherKey = split[0];
63 byte[] macKey = split[1];
64
65 this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
66 this.mac = Mac.getInstance("HmacSHA256");
67 this.outputStream = output;
68 this.iv = Util.getSecretBytes(16);
69 this.counter = Conversions.byteArrayToInt(iv);
70
71 mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
72
73 byte[] header = new BackupFrame.Builder().header_(new Header.Builder()
74 .iv(new okio.ByteString(iv))
75 .salt(new okio.ByteString(salt))
76 .version(BackupVersions.CURRENT_VERSION)
77 .build())
78 .build()
79 .encode();
80
81 outputStream.write(Conversions.intToByteArray(header.length));
82 outputStream.write(header);
83 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
84 throw new AssertionError(e);
85 }
86 }
87
88 public void write(SharedPreference preference) throws IOException {
89 write(outputStream, new BackupFrame.Builder().preference(preference).build());
90 }
91
92 public void write(KeyValue keyValue) throws IOException {
93 write(outputStream, new BackupFrame.Builder().keyValue(keyValue).build());
94 }
95
96 public void write(SqlStatement statement) throws IOException {
97 write(outputStream, new BackupFrame.Builder().statement(statement).build());
98 }
99
100 public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException {
101 try {
102 write(outputStream, new BackupFrame.Builder()
103 .avatar(new Avatar.Builder()
104 .recipientId(avatarName)
105 .length(Util.toIntExact(size))
106 .build())
107 .build());
108 } catch (ArithmeticException e) {
109 Log.w(TAG, "Unable to write avatar to backup", e);
110 throw new FullBackupExporter.InvalidBackupStreamException();
111 }
112
113 if (writeStream(in) != size) {
114 throw new IOException("Size mismatch!");
115 }
116 }
117
118 public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException {
119 try {
120 write(outputStream, new BackupFrame.Builder()
121 .attachment(new Attachment.Builder()
122 .rowId(attachmentId.id)
123 .length(Util.toIntExact(size))
124 .build())
125 .build());
126 } catch (ArithmeticException e) {
127 Log.w(TAG, "Unable to write " + attachmentId + " to backup", e);
128 throw new FullBackupExporter.InvalidBackupStreamException();
129 }
130
131 if (writeStream(in) != size) {
132 throw new IOException("Size mismatch!");
133 }
134 }
135
136 public void writeSticker(long rowId, @NonNull InputStream in, long size) throws IOException {
137 try {
138 write(outputStream, new BackupFrame.Builder()
139 .sticker(new Sticker.Builder()
140 .rowId(rowId)
141 .length(Util.toIntExact(size))
142 .build())
143 .build());
144 } catch (ArithmeticException e) {
145 Log.w(TAG, "Unable to write sticker to backup", e);
146 throw new FullBackupExporter.InvalidBackupStreamException();
147 }
148
149 if (writeStream(in) != size) {
150 throw new IOException("Size mismatch!");
151 }
152 }
153
154 void writeDatabaseVersion(int version) throws IOException {
155 write(outputStream, new BackupFrame.Builder()
156 .version(new DatabaseVersion.Builder().version(version).build())
157 .build());
158 }
159
160 void writeEnd() throws IOException {
161 write(outputStream, new BackupFrame.Builder().end(true).build());
162 }
163
164 /**
165 * @return The amount of data written from the provided InputStream.
166 */
167 private long writeStream(@NonNull InputStream inputStream) throws IOException {
168 try {
169 Conversions.intToByteArray(iv, 0, counter++);
170 cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
171 mac.update(iv);
172
173 byte[] buffer = new byte[8192];
174 long total = 0;
175
176 int read;
177
178 while ((read = inputStream.read(buffer)) != -1) {
179 byte[] ciphertext = cipher.update(buffer, 0, read);
180
181 if (ciphertext != null) {
182 outputStream.write(ciphertext);
183 mac.update(ciphertext);
184 }
185
186 total += read;
187 }
188
189 byte[] remainder = cipher.doFinal();
190 outputStream.write(remainder);
191 mac.update(remainder);
192
193 byte[] attachmentDigest = mac.doFinal();
194 outputStream.write(attachmentDigest, 0, 10);
195
196 return total;
197 } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
198 throw new AssertionError(e);
199 }
200 }
201
202 private void write(@NonNull OutputStream out, @NonNull BackupFrame frame) throws IOException {
203 try {
204 Conversions.intToByteArray(iv, 0, counter++);
205 cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
206
207 byte[] encodedFrame = frame.encode();
208
209 // this assumes a stream cipher
210 byte[] length = Conversions.intToByteArray(encodedFrame.length + 10);
211 if (BackupVersions.isFrameLengthEncrypted(BackupVersions.CURRENT_VERSION)) {
212 byte[] encryptedLength = cipher.update(length);
213 if (encryptedLength.length != length.length) {
214 throw new IOException("Stream cipher assumption has been violated!");
215 }
216 mac.update(encryptedLength);
217 length = encryptedLength;
218 }
219
220 byte[] frameCiphertext = cipher.doFinal(frame.encode());
221 if (frameCiphertext.length != encodedFrame.length) {
222 throw new IOException("Stream cipher assumption has been violated!");
223 }
224
225 byte[] frameMac = mac.doFinal(frameCiphertext);
226
227 out.write(length);
228 out.write(frameCiphertext);
229 out.write(frameMac, 0, 10);
230 frames++;
231 } catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
232 throw new AssertionError(e);
233 }
234 }
235
236 public void close() throws IOException {
237 outputStream.flush();
238 outputStream.close();
239 }
240
241 public int getFrames() {
242 return frames;
243 }
244}