That fuck shit the fascists are using
at master 244 lines 8.5 kB view raw
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}