@capgo/capacitor-updater 8.51.7 → 8.51.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +62 -21
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +115 -20
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +142 -114
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +119 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +249 -160
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +41 -29
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +30 -18
- package/package.json +5 -5
|
@@ -15,8 +15,6 @@ import java.io.FileInputStream;
|
|
|
15
15
|
import java.net.HttpURLConnection;
|
|
16
16
|
import java.net.URL;
|
|
17
17
|
import java.nio.channels.FileChannel;
|
|
18
|
-
import java.nio.file.Files;
|
|
19
|
-
import java.nio.file.StandardCopyOption;
|
|
20
18
|
import java.security.MessageDigest;
|
|
21
19
|
import java.util.ArrayList;
|
|
22
20
|
import java.util.Arrays;
|
|
@@ -30,6 +28,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
30
28
|
import java.util.concurrent.atomic.AtomicLong;
|
|
31
29
|
import okhttp3.Call;
|
|
32
30
|
import okhttp3.Callback;
|
|
31
|
+
import okhttp3.Dispatcher;
|
|
33
32
|
import okhttp3.Interceptor;
|
|
34
33
|
import okhttp3.MediaType;
|
|
35
34
|
import okhttp3.OkHttpClient;
|
|
@@ -38,11 +37,6 @@ import okhttp3.Request;
|
|
|
38
37
|
import okhttp3.RequestBody;
|
|
39
38
|
import okhttp3.Response;
|
|
40
39
|
import okhttp3.ResponseBody;
|
|
41
|
-
import okio.Buffer;
|
|
42
|
-
import okio.BufferedSink;
|
|
43
|
-
import okio.BufferedSource;
|
|
44
|
-
import okio.Okio;
|
|
45
|
-
import okio.Source;
|
|
46
40
|
import org.brotli.dec.BrotliInputStream;
|
|
47
41
|
import org.json.JSONArray;
|
|
48
42
|
import org.json.JSONObject;
|
|
@@ -78,6 +72,8 @@ public class DownloadService extends Worker {
|
|
|
78
72
|
public static final String DEFAULT_CHANNEL = "default_channel";
|
|
79
73
|
public static final String IS_PROD = "is_prod";
|
|
80
74
|
public static final String IS_EMULATOR = "is_emulator";
|
|
75
|
+
// Match HTTP dispatcher so 64 workers actually fetch in parallel (HTTP/2 multiplexes).
|
|
76
|
+
private static final int MANIFEST_MAX_CONCURRENT_FILES = 64;
|
|
81
77
|
private static final String UPDATE_FILE = "update.dat";
|
|
82
78
|
|
|
83
79
|
// Shared OkHttpClient to prevent resource leaks
|
|
@@ -88,7 +84,11 @@ public class DownloadService extends Worker {
|
|
|
88
84
|
|
|
89
85
|
// Initialize shared client with User-Agent interceptor
|
|
90
86
|
static {
|
|
87
|
+
Dispatcher dispatcher = new Dispatcher();
|
|
88
|
+
dispatcher.setMaxRequests(MANIFEST_MAX_CONCURRENT_FILES);
|
|
89
|
+
dispatcher.setMaxRequestsPerHost(MANIFEST_MAX_CONCURRENT_FILES);
|
|
91
90
|
sharedClient = new OkHttpClient.Builder()
|
|
91
|
+
.dispatcher(dispatcher)
|
|
92
92
|
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
|
|
93
93
|
.addInterceptor((chain) -> {
|
|
94
94
|
Request originalRequest = chain.request();
|
|
@@ -203,7 +203,7 @@ public class DownloadService extends Worker {
|
|
|
203
203
|
if (isManifest) {
|
|
204
204
|
JSONArray manifest = DataManager.getInstance().getAndClearManifest();
|
|
205
205
|
if (manifest != null) {
|
|
206
|
-
handleManifestDownload(id, documentsDir, dest, version, sessionKey, publicKey, manifest
|
|
206
|
+
handleManifestDownload(id, documentsDir, dest, version, sessionKey, publicKey, manifest);
|
|
207
207
|
return createSuccessResult(dest, version, sessionKey, checksum, true);
|
|
208
208
|
} else {
|
|
209
209
|
logger.error("Manifest is null");
|
|
@@ -291,7 +291,7 @@ public class DownloadService extends Worker {
|
|
|
291
291
|
String version,
|
|
292
292
|
String sessionKey,
|
|
293
293
|
String publicKey,
|
|
294
|
-
|
|
294
|
+
JSONArray manifest
|
|
295
295
|
) {
|
|
296
296
|
try {
|
|
297
297
|
logger.debug("handleManifestDownload");
|
|
@@ -299,7 +299,6 @@ public class DownloadService extends Worker {
|
|
|
299
299
|
// Send stats for manifest download start
|
|
300
300
|
sendStatsAsync("download_manifest_start", version);
|
|
301
301
|
|
|
302
|
-
JSONArray manifest = new JSONArray(manifestString);
|
|
303
302
|
File destFolder = new File(documentsDir, dest);
|
|
304
303
|
File cacheFolder = new File(getApplicationContext().getCacheDir(), "capgo_downloads");
|
|
305
304
|
File builtinFolder = new File(getApplicationContext().getFilesDir(), "public");
|
|
@@ -316,9 +315,7 @@ public class DownloadService extends Worker {
|
|
|
316
315
|
final AtomicLong completedFiles = new AtomicLong(0);
|
|
317
316
|
final AtomicBoolean hasError = new AtomicBoolean(false);
|
|
318
317
|
|
|
319
|
-
|
|
320
|
-
int threadCount = Math.min(64, Math.max(32, totalFiles));
|
|
321
|
-
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
|
318
|
+
ExecutorService executor = Executors.newFixedThreadPool(Math.min(MANIFEST_MAX_CONCURRENT_FILES, Math.max(1, totalFiles)));
|
|
322
319
|
List<Future<?>> futures = new ArrayList<>();
|
|
323
320
|
|
|
324
321
|
for (int i = 0; i < totalFiles; i++) {
|
|
@@ -361,8 +358,11 @@ public class DownloadService extends Worker {
|
|
|
361
358
|
continue;
|
|
362
359
|
}
|
|
363
360
|
String cacheBaseName = new File(isBrotli ? targetFileName : fileName).getName();
|
|
364
|
-
File cacheFile =
|
|
365
|
-
|
|
361
|
+
final File cacheFile = CapgoUpdater.isSafeCacheHash(finalFileHash)
|
|
362
|
+
? new File(cacheFolder, finalFileHash + "_" + cacheBaseName)
|
|
363
|
+
: null;
|
|
364
|
+
final File legacyCacheFile =
|
|
365
|
+
isBrotli && cacheFile != null ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
|
|
366
366
|
|
|
367
367
|
// Ensure parent directories of the target file exist
|
|
368
368
|
if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
|
|
@@ -622,17 +622,13 @@ public class DownloadService extends Worker {
|
|
|
622
622
|
* This handles the race condition where OS can delete cache files between exists() check and copy.
|
|
623
623
|
*/
|
|
624
624
|
private boolean tryCopyFromCache(File source, File dest, String expectedHash) {
|
|
625
|
-
// First quick check - if file doesn't exist, don't bother
|
|
626
|
-
if (!
|
|
625
|
+
// First quick check - if file doesn't exist or was truncated, don't bother
|
|
626
|
+
if (!CapgoUpdater.isReusableCacheFile(source, expectedHash)) {
|
|
627
627
|
return false;
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
return false;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Try to copy - if it fails (file deleted by OS between check and copy), return false
|
|
630
|
+
// Hash is in the cache file name and was verified when written.
|
|
631
|
+
// Re-hashing here would re-read every reused file on low-RAM devices.
|
|
636
632
|
try {
|
|
637
633
|
copyFile(source, dest);
|
|
638
634
|
return true;
|
|
@@ -649,23 +645,29 @@ public class DownloadService extends Worker {
|
|
|
649
645
|
throw new IOException("Failed to create parent directory: " + parent.getAbsolutePath());
|
|
650
646
|
}
|
|
651
647
|
|
|
652
|
-
final File tempFile =
|
|
653
|
-
try (
|
|
654
|
-
FileInputStream inStream = new FileInputStream(source);
|
|
655
|
-
FileOutputStream outStream = new FileOutputStream(tempFile);
|
|
656
|
-
FileChannel inChannel = inStream.getChannel();
|
|
657
|
-
FileChannel outChannel = outStream.getChannel()
|
|
658
|
-
) {
|
|
659
|
-
inChannel.transferTo(0, inChannel.size(), outChannel);
|
|
660
|
-
}
|
|
661
|
-
|
|
648
|
+
final File tempFile = File.createTempFile("capgo-", ".tmp", parent);
|
|
662
649
|
try {
|
|
663
|
-
|
|
664
|
-
|
|
650
|
+
try (
|
|
651
|
+
FileInputStream inStream = new FileInputStream(source);
|
|
652
|
+
FileOutputStream outStream = new FileOutputStream(tempFile);
|
|
653
|
+
FileChannel inChannel = inStream.getChannel();
|
|
654
|
+
FileChannel outChannel = outStream.getChannel()
|
|
655
|
+
) {
|
|
656
|
+
long size = inChannel.size();
|
|
657
|
+
long pos = 0;
|
|
658
|
+
while (pos < size) {
|
|
659
|
+
long transferred = inChannel.transferTo(pos, size - pos, outChannel);
|
|
660
|
+
if (transferred <= 0) {
|
|
661
|
+
throw new IOException("Failed to copy file: " + source.getAbsolutePath());
|
|
662
|
+
}
|
|
663
|
+
pos += transferred;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
CryptoCipher.replaceFile(tempFile, dest);
|
|
667
|
+
} finally {
|
|
665
668
|
if (tempFile.exists()) {
|
|
666
669
|
tempFile.delete();
|
|
667
670
|
}
|
|
668
|
-
throw e;
|
|
669
671
|
}
|
|
670
672
|
}
|
|
671
673
|
|
|
@@ -715,35 +717,16 @@ public class DownloadService extends Worker {
|
|
|
715
717
|
|
|
716
718
|
// Only decompress if file has .br extension
|
|
717
719
|
if (isBrotli) {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
) {
|
|
727
|
-
offset += bytesRead;
|
|
728
|
-
}
|
|
729
|
-
byte[] decompressedData;
|
|
730
|
-
try {
|
|
731
|
-
decompressedData = decompressBrotli(compressedData, targetFile.getName());
|
|
732
|
-
} catch (IOException e) {
|
|
733
|
-
sendStatsAsync(
|
|
734
|
-
"download_manifest_brotli_fail",
|
|
735
|
-
getInputData().getString(VERSION) + ":" + finalTargetFile.getName()
|
|
736
|
-
);
|
|
737
|
-
throw e;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
// Write decompressed data atomically
|
|
741
|
-
try (java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(decompressedData)) {
|
|
742
|
-
writeFileAtomic(finalTargetFile, bais, null);
|
|
743
|
-
}
|
|
720
|
+
try {
|
|
721
|
+
decompressBrotli(compressedFile, finalTargetFile, targetFile.getName());
|
|
722
|
+
} catch (IOException e) {
|
|
723
|
+
sendStatsAsync(
|
|
724
|
+
"download_manifest_brotli_fail",
|
|
725
|
+
getInputData().getString(VERSION) + ":" + finalTargetFile.getName()
|
|
726
|
+
);
|
|
727
|
+
throw e;
|
|
744
728
|
}
|
|
745
729
|
} else {
|
|
746
|
-
// Just copy the file without decompression using atomic operation
|
|
747
730
|
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
748
731
|
writeFileAtomic(finalTargetFile, fis, null);
|
|
749
732
|
}
|
|
@@ -758,8 +741,10 @@ public class DownloadService extends Worker {
|
|
|
758
741
|
// Verify checksum
|
|
759
742
|
if (calculatedHash.equalsIgnoreCase(expectedHash)) {
|
|
760
743
|
// Only cache if checksum is correct - use atomic copy
|
|
761
|
-
|
|
762
|
-
|
|
744
|
+
if (cacheFile != null) {
|
|
745
|
+
try (FileInputStream fis = new FileInputStream(finalTargetFile)) {
|
|
746
|
+
writeFileAtomic(cacheFile, fis, null);
|
|
747
|
+
}
|
|
763
748
|
}
|
|
764
749
|
} else {
|
|
765
750
|
finalTargetFile.delete();
|
|
@@ -815,75 +800,118 @@ public class DownloadService extends Worker {
|
|
|
815
800
|
return sb.toString();
|
|
816
801
|
}
|
|
817
802
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (
|
|
821
|
-
|
|
822
|
-
throw new IOException("Null data received");
|
|
803
|
+
static void decompressBrotli(File input, File output, String fileName) throws IOException {
|
|
804
|
+
File parent = output.getParentFile();
|
|
805
|
+
if (parent != null) {
|
|
806
|
+
parent.mkdirs();
|
|
823
807
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
return
|
|
808
|
+
long length = input.length();
|
|
809
|
+
if (length == 0) {
|
|
810
|
+
writeFileAtomic(output, new ByteArrayInputStream(new byte[0]), null);
|
|
811
|
+
return;
|
|
828
812
|
}
|
|
829
813
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
814
|
+
byte[] head = new byte[(int) Math.min(3, length)];
|
|
815
|
+
byte last = 0;
|
|
816
|
+
try (RandomAccessFile raf = new RandomAccessFile(input, "r")) {
|
|
817
|
+
raf.readFully(head);
|
|
818
|
+
if (length >= 1) {
|
|
819
|
+
raf.seek(length - 1);
|
|
820
|
+
last = raf.readByte();
|
|
821
|
+
}
|
|
833
822
|
}
|
|
834
823
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
if (data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06 && data[data.length - 1] == 0x03) {
|
|
840
|
-
return Arrays.copyOfRange(data, 3, data.length - 1);
|
|
841
|
-
}
|
|
824
|
+
if (length == 3 && head[0] == 0x1B && head[1] == 0x00 && head[2] == 0x06) {
|
|
825
|
+
writeFileAtomic(output, new ByteArrayInputStream(new byte[0]), null);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
842
828
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
829
|
+
if (length > 3 && last == 0x03) {
|
|
830
|
+
boolean emptyWrapper = head[0] == 0x1B && head[1] == 0x00 && head[2] == 0x06;
|
|
831
|
+
boolean qualityZeroWrapper = head[0] == 0x0b && head[1] == 0x02 && head[2] == (byte) 0x80;
|
|
832
|
+
if (emptyWrapper || qualityZeroWrapper) {
|
|
833
|
+
try (FileInputStream fis = new FileInputStream(input)) {
|
|
834
|
+
long skipped = 0;
|
|
835
|
+
while (skipped < 3) {
|
|
836
|
+
long n = fis.skip(3 - skipped);
|
|
837
|
+
if (n <= 0) {
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
skipped += n;
|
|
841
|
+
}
|
|
842
|
+
writeFileAtomic(output, new BoundedInputStream(fis, length - 4), null);
|
|
846
843
|
}
|
|
847
|
-
|
|
848
|
-
logger.error("Error: Malformed data for " + fileName);
|
|
849
|
-
throw new IOException("Malformed data structure");
|
|
844
|
+
return;
|
|
850
845
|
}
|
|
851
846
|
}
|
|
852
847
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
ByteArrayInputStream bis = new ByteArrayInputStream(data);
|
|
856
|
-
BrotliInputStream brotliInputStream = new BrotliInputStream(bis);
|
|
857
|
-
ByteArrayOutputStream bos = new ByteArrayOutputStream()
|
|
858
|
-
) {
|
|
859
|
-
byte[] buffer = new byte[8192];
|
|
860
|
-
int len;
|
|
861
|
-
while ((len = brotliInputStream.read(buffer)) != -1) {
|
|
862
|
-
bos.write(buffer, 0, len);
|
|
863
|
-
}
|
|
864
|
-
return bos.toByteArray();
|
|
848
|
+
try (FileInputStream fis = new FileInputStream(input); BrotliInputStream brotliInputStream = new BrotliInputStream(fis)) {
|
|
849
|
+
writeFileAtomic(output, brotliInputStream, null);
|
|
865
850
|
} catch (IOException e) {
|
|
866
851
|
logger.error("Error: Brotli process failed for " + fileName + ". Status: " + e.getMessage());
|
|
867
|
-
// Add hex dump for debugging
|
|
868
852
|
StringBuilder hexDump = new StringBuilder();
|
|
869
|
-
|
|
870
|
-
|
|
853
|
+
try (FileInputStream peek = new FileInputStream(input)) {
|
|
854
|
+
byte[] prefix = new byte[(int) Math.min(32, length)];
|
|
855
|
+
int n = peek.read(prefix);
|
|
856
|
+
for (int i = 0; i < n; i++) {
|
|
857
|
+
hexDump.append(String.format("%02x ", prefix[i]));
|
|
858
|
+
}
|
|
871
859
|
}
|
|
872
|
-
logger.error("Error: Raw data (" + fileName + "): " + hexDump
|
|
860
|
+
logger.error("Error: Raw data (" + fileName + "): " + hexDump);
|
|
873
861
|
throw e;
|
|
874
862
|
}
|
|
875
863
|
}
|
|
876
864
|
|
|
865
|
+
private static final class BoundedInputStream extends FilterInputStream {
|
|
866
|
+
|
|
867
|
+
private long remaining;
|
|
868
|
+
|
|
869
|
+
BoundedInputStream(InputStream in, long remaining) {
|
|
870
|
+
super(in);
|
|
871
|
+
this.remaining = remaining;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
@Override
|
|
875
|
+
public int read() throws IOException {
|
|
876
|
+
if (remaining <= 0) {
|
|
877
|
+
return -1;
|
|
878
|
+
}
|
|
879
|
+
int value = super.read();
|
|
880
|
+
if (value >= 0) {
|
|
881
|
+
remaining--;
|
|
882
|
+
}
|
|
883
|
+
return value;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
@Override
|
|
887
|
+
public int read(byte[] b, int off, int len) throws IOException {
|
|
888
|
+
if (remaining <= 0) {
|
|
889
|
+
return -1;
|
|
890
|
+
}
|
|
891
|
+
int capped = (int) Math.min(len, remaining);
|
|
892
|
+
int n = super.read(b, off, capped);
|
|
893
|
+
if (n > 0) {
|
|
894
|
+
remaining -= n;
|
|
895
|
+
}
|
|
896
|
+
return n;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
877
900
|
/**
|
|
878
|
-
* Atomically write
|
|
901
|
+
* Atomically write a stream to a file using the RAM-ladder IO buffer.
|
|
879
902
|
*/
|
|
880
|
-
|
|
881
|
-
File tempFile =
|
|
903
|
+
static void writeFileAtomic(File targetFile, InputStream inputStream, String expectedChecksum) throws IOException {
|
|
904
|
+
File tempFile = File.createTempFile("capgo-", ".tmp", targetFile.getParentFile());
|
|
882
905
|
|
|
883
906
|
try {
|
|
884
|
-
//
|
|
885
|
-
|
|
886
|
-
|
|
907
|
+
// Okio's default segment is 8KB. Copy with the RAM-ladder buffer so
|
|
908
|
+
// 8MB wrapper unwraps are not 1000 tiny writes.
|
|
909
|
+
byte[] buffer = new byte[CryptoCipher.ioBufferBytes()];
|
|
910
|
+
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
|
|
911
|
+
int n;
|
|
912
|
+
while ((n = inputStream.read(buffer)) != -1) {
|
|
913
|
+
fos.write(buffer, 0, n);
|
|
914
|
+
}
|
|
887
915
|
}
|
|
888
916
|
|
|
889
917
|
// Verify checksum if provided
|
|
@@ -895,8 +923,8 @@ public class DownloadService extends Worker {
|
|
|
895
923
|
}
|
|
896
924
|
}
|
|
897
925
|
|
|
898
|
-
// Atomic rename (on same filesystem)
|
|
899
|
-
|
|
926
|
+
// Atomic rename (on same filesystem). renameTo works on API 24; Files.move does not.
|
|
927
|
+
CryptoCipher.replaceFile(tempFile, targetFile);
|
|
900
928
|
} catch (Exception e) {
|
|
901
929
|
// Clean up temp file on error
|
|
902
930
|
if (tempFile.exists()) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import CommonCrypto
|
|
3
3
|
import CryptoKit
|
|
4
|
+
import Darwin
|
|
4
5
|
|
|
5
6
|
///
|
|
6
7
|
/// Constants
|
|
@@ -83,4 +84,122 @@ public struct AES128Key {
|
|
|
83
84
|
return nil
|
|
84
85
|
}
|
|
85
86
|
}
|
|
87
|
+
|
|
88
|
+
/// AES-CBC file-to-file. Never holds the whole ciphertext in RAM.
|
|
89
|
+
func decrypt(from source: URL, to destination: URL) throws {
|
|
90
|
+
var cryptor: CCCryptorRef?
|
|
91
|
+
let createStatus: CCCryptorStatus = aes128Key.withUnsafeBytes { keyBytes in
|
|
92
|
+
initVector.withUnsafeBytes { ivBytes in
|
|
93
|
+
guard let keyPtr = keyBytes.baseAddress, let ivPtr = ivBytes.baseAddress else {
|
|
94
|
+
return CCCryptorStatus(kCCParamError)
|
|
95
|
+
}
|
|
96
|
+
return CCCryptorCreate(
|
|
97
|
+
CCOperation(kCCDecrypt),
|
|
98
|
+
AESConstants.aesAlgorithm,
|
|
99
|
+
AESConstants.aesOptions,
|
|
100
|
+
keyPtr,
|
|
101
|
+
keyBytes.count,
|
|
102
|
+
ivPtr,
|
|
103
|
+
&cryptor
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
guard createStatus == kCCSuccess, let cryptor else {
|
|
108
|
+
logger.error("Failed to create AES cryptor")
|
|
109
|
+
throw NSError(domain: "AESDecryptError", code: Int(createStatus), userInfo: nil)
|
|
110
|
+
}
|
|
111
|
+
defer {
|
|
112
|
+
CCCryptorRelease(cryptor)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
guard let input = InputStream(url: source) else {
|
|
116
|
+
throw NSError(domain: "AESDecryptError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to open AES source"])
|
|
117
|
+
}
|
|
118
|
+
input.open()
|
|
119
|
+
defer {
|
|
120
|
+
input.close()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let tempURL = destination.deletingLastPathComponent().appendingPathComponent("capgo-aes-\(UUID().uuidString).tmp")
|
|
124
|
+
let fileManager = FileManager.default
|
|
125
|
+
fileManager.createFile(atPath: tempURL.path, contents: nil)
|
|
126
|
+
let output = try FileHandle(forWritingTo: tempURL)
|
|
127
|
+
defer {
|
|
128
|
+
try? output.close()
|
|
129
|
+
try? fileManager.removeItem(at: tempURL)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let bufferSize = CryptoCipher.ioBufferBytes()
|
|
133
|
+
let outBufSize = bufferSize + kCCBlockSizeAES128
|
|
134
|
+
var inBuf = [UInt8](repeating: 0, count: bufferSize)
|
|
135
|
+
var outBuf = [UInt8](repeating: 0, count: outBufSize)
|
|
136
|
+
let outFd = output.fileDescriptor
|
|
137
|
+
|
|
138
|
+
while true {
|
|
139
|
+
let n = inBuf.withUnsafeMutableBufferPointer { ptr in
|
|
140
|
+
input.read(ptr.baseAddress!, maxLength: ptr.count)
|
|
141
|
+
}
|
|
142
|
+
if n == 0 {
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
if n < 0 {
|
|
146
|
+
throw input.streamError ?? NSError(domain: "AESDecryptError", code: 2, userInfo: [NSLocalizedDescriptionKey: "AES stream read failed"])
|
|
147
|
+
}
|
|
148
|
+
var moved: size_t = 0
|
|
149
|
+
let status: CCCryptorStatus = inBuf.withUnsafeBufferPointer { inRaw in
|
|
150
|
+
outBuf.withUnsafeMutableBytes { outRaw in
|
|
151
|
+
CCCryptorUpdate(
|
|
152
|
+
cryptor,
|
|
153
|
+
inRaw.baseAddress,
|
|
154
|
+
n,
|
|
155
|
+
outRaw.baseAddress,
|
|
156
|
+
outBufSize,
|
|
157
|
+
&moved
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
guard status == kCCSuccess else {
|
|
162
|
+
logger.error("AES stream update failed")
|
|
163
|
+
throw NSError(domain: "AESDecryptError", code: Int(status), userInfo: nil)
|
|
164
|
+
}
|
|
165
|
+
if moved > 0 {
|
|
166
|
+
try Self.writeAll(fd: outFd, buffer: &outBuf, count: moved)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
var moved: size_t = 0
|
|
171
|
+
let finalStatus: CCCryptorStatus = outBuf.withUnsafeMutableBytes { outRaw in
|
|
172
|
+
CCCryptorFinal(cryptor, outRaw.baseAddress, outBufSize, &moved)
|
|
173
|
+
}
|
|
174
|
+
guard finalStatus == kCCSuccess else {
|
|
175
|
+
logger.error("AES stream finalize failed")
|
|
176
|
+
throw NSError(domain: "AESDecryptError", code: Int(finalStatus), userInfo: nil)
|
|
177
|
+
}
|
|
178
|
+
if moved > 0 {
|
|
179
|
+
try Self.writeAll(fd: outFd, buffer: &outBuf, count: moved)
|
|
180
|
+
}
|
|
181
|
+
try output.close()
|
|
182
|
+
|
|
183
|
+
do {
|
|
184
|
+
_ = try fileManager.replaceItemAt(destination, withItemAt: tempURL)
|
|
185
|
+
} catch {
|
|
186
|
+
if fileManager.fileExists(atPath: destination.path) {
|
|
187
|
+
throw error
|
|
188
|
+
}
|
|
189
|
+
try fileManager.moveItem(at: tempURL, to: destination)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private static func writeAll(fd: Int32, buffer: inout [UInt8], count: Int) throws {
|
|
194
|
+
var offset = 0
|
|
195
|
+
while offset < count {
|
|
196
|
+
let written = buffer.withUnsafeBufferPointer { ptr -> Int in
|
|
197
|
+
Darwin.write(fd, ptr.baseAddress!.advanced(by: offset), count - offset)
|
|
198
|
+
}
|
|
199
|
+
if written <= 0 {
|
|
200
|
+
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno), userInfo: [NSLocalizedDescriptionKey: "AES stream write failed"])
|
|
201
|
+
}
|
|
202
|
+
offset += written
|
|
203
|
+
}
|
|
204
|
+
}
|
|
86
205
|
}
|
|
@@ -96,7 +96,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
96
96
|
deinit {
|
|
97
97
|
implementation.shutdown()
|
|
98
98
|
}
|
|
99
|
-
private let pluginVersion: String = "8.51.
|
|
99
|
+
private let pluginVersion: String = "8.51.9"
|
|
100
100
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
101
101
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
102
102
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|