1+ package org .cryptomator .macos .update ;
2+
3+ import org .cryptomator .integrations .common .LocalizedDisplayName ;
4+ import org .cryptomator .integrations .common .OperatingSystem ;
5+ import org .cryptomator .integrations .update .DownloadUpdateInfo ;
6+ import org .cryptomator .integrations .update .DownloadUpdateMechanism ;
7+ import org .cryptomator .integrations .update .UpdateFailedException ;
8+ import org .cryptomator .integrations .update .UpdateMechanism ;
9+ import org .cryptomator .integrations .update .UpdateStep ;
10+ import org .cryptomator .macos .common .Localization ;
11+ import org .slf4j .Logger ;
12+ import org .slf4j .LoggerFactory ;
13+
14+ import java .io .IOException ;
15+ import java .io .InterruptedIOException ;
16+ import java .nio .charset .StandardCharsets ;
17+ import java .nio .file .Files ;
18+ import java .nio .file .Path ;
19+ import java .nio .file .StandardOpenOption ;
20+ import java .util .List ;
21+ import java .util .UUID ;
22+
23+ @ OperatingSystem (OperatingSystem .Value .MAC )
24+ @ LocalizedDisplayName (bundle = "MacIntegrationsBundle" , key = "org.cryptomator.macos.update.dmg.displayName" )
25+ public class DmgUpdateMechanism extends DownloadUpdateMechanism {
26+
27+ private static final Logger LOG = LoggerFactory .getLogger (DmgUpdateMechanism .class );
28+
29+ @ Override
30+ protected DownloadUpdateInfo checkForUpdate (String currentVersion , LatestVersionResponse response ) {
31+ String suffix = switch (System .getProperty ("os.arch" )) {
32+ case "aarch64" , "arm64" -> "arm64.dmg" ;
33+ default -> "x64.dmg" ;
34+ };
35+ var updateVersion = response .latestVersion ().macVersion ();
36+ var asset = response .assets ().stream ().filter (a -> a .name ().endsWith (suffix )).findAny ().orElse (null );
37+ if (UpdateMechanism .isUpdateAvailable (updateVersion , currentVersion ) && asset != null ) {
38+ return new DownloadUpdateInfo (this , updateVersion , asset );
39+ } else {
40+ return null ;
41+ }
42+ }
43+
44+ @ Override
45+ public UpdateStep secondStep (Path workDir , Path assetPath , DownloadUpdateInfo updateInfo ) {
46+ return UpdateStep .of (Localization .get ().getString ("org.cryptomator.macos.update.dmg.unpacking" ), () -> this .unpack (workDir , assetPath ));
47+ }
48+
49+ private UpdateStep unpack (Path workDir , Path assetPath ) throws IOException {
50+ // Extract Cryptomator.app from the .dmg file
51+ var processBuilder = new ProcessBuilder (List .of ("/bin/zsh" , "-s" ));
52+ processBuilder .directory (workDir .toFile ());
53+ processBuilder .environment ().put ("DMG_PATH" , assetPath .toString ());
54+ processBuilder .environment ().put ("MOUNT_ID" , UUID .randomUUID ().toString ());
55+ Process p = processBuilder .start ();
56+ try {
57+ try (var stdin = p .outputWriter ()) {
58+ stdin .write ("""
59+ trap 'hdiutil detach "/Volumes/Cryptomator_${MOUNT_ID}" -quiet || true' EXIT
60+ hdiutil attach "${DMG_PATH}" -mountpoint "/Volumes/Cryptomator_${MOUNT_ID}" -nobrowse -quiet
61+ cp -R "/Volumes/Cryptomator_${MOUNT_ID}/Cryptomator.app" 'Cryptomator.app'
62+ """ );
63+ }
64+ if (p .waitFor () != 0 ) {
65+ LOG .error ("Failed to extract DMG, exit code: {}, output: {}" , p .exitValue (), new String (p .getErrorStream ().readAllBytes ()));
66+ throw new IOException ("Failed to extract DMG, exit code: " + p .exitValue ());
67+ }
68+ LOG .debug ("Unpacked app: {}" , workDir .resolve ("Cryptomator.app" ));
69+ } catch (InterruptedException e ) {
70+ Thread .currentThread ().interrupt ();
71+ throw new InterruptedIOException ("Failed to extract DMG, interrupted" );
72+ }
73+ return UpdateStep .of (Localization .get ().getString ("org.cryptomator.macos.update.dmg.verifying" ), () -> this .verify (workDir , assetPath ));
74+ }
75+
76+ private UpdateStep verify (Path workDir , Path assetPath ) throws IOException {
77+ // Verify code signature of the extracted .app
78+ var processBuilder = new ProcessBuilder (List .of ("/bin/zsh" , "-s" ));
79+ processBuilder .directory (workDir .toFile ());
80+ Process p = processBuilder .start ();
81+ try {
82+ try (var stdin = p .outputWriter ()) {
83+ stdin .write ("""
84+ codesign --verify --deep --strict 'Cryptomator.app'
85+ spctl --assess --type execute 'Cryptomator.app'
86+ """ );
87+ }
88+ if (p .waitFor () != 0 ) {
89+ LOG .error ("Checking code signature failed: {}, output: {}" , p .exitValue (), new String (p .getErrorStream ().readAllBytes ()));
90+ throw new UpdateFailedException ("Invalid Code Signature." );
91+ }
92+ LOG .debug ("Verified app: {}" , workDir .resolve ("Cryptomator.app" ));
93+ } catch (InterruptedException e ) {
94+ Thread .currentThread ().interrupt ();
95+ throw new InterruptedIOException ("Code signature verification interrupted" );
96+ }
97+ return UpdateStep .of (Localization .get ().getString ("org.cryptomator.macos.update.dmg.restarting" ), () -> this .restart (workDir ));
98+ }
99+
100+ public UpdateStep restart (Path workDir ) throws IllegalStateException , IOException {
101+ String selfPath = ProcessHandle .current ().info ().command ().orElse ("" );
102+ String installPath ;
103+ if (selfPath .startsWith ("/Applications/Cryptomator.app" )) {
104+ installPath = "/Applications/Cryptomator.app" ;
105+ } else if (selfPath .contains ("/Cryptomator.app/" )) {
106+ installPath = selfPath .substring (0 , selfPath .indexOf ("/Cryptomator.app/" )) + "/Cryptomator.app" ;
107+ } else {
108+ throw new UpdateFailedException ("Cannot determine destination path for Cryptomator.app, current path: " + selfPath );
109+ }
110+ LOG .info ("Restarting to apply Update in {} now..." , workDir );
111+ String script = """
112+ while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.2; done;
113+ if [ -d "${CRYPTOMATOR_INSTALL_PATH}" ]; then
114+ echo "Removing old installation at ${CRYPTOMATOR_INSTALL_PATH}";
115+ rm -rf "${CRYPTOMATOR_INSTALL_PATH}"
116+ fi
117+ mv 'Cryptomator.app' "${CRYPTOMATOR_INSTALL_PATH}";
118+ open "${CRYPTOMATOR_INSTALL_PATH}";
119+ """ ;
120+ Files .writeString (workDir .resolve ("install.sh" ), script , StandardCharsets .US_ASCII , StandardOpenOption .WRITE , StandardOpenOption .CREATE_NEW );
121+ var command = List .of ("/bin/zsh" , "-c" , "/usr/bin/nohup zsh install.sh >install.log 2>&1 &" );
122+ var processBuilder = new ProcessBuilder (command );
123+ processBuilder .directory (workDir .toFile ());
124+ processBuilder .environment ().put ("CRYPTOMATOR_PID" , String .valueOf (ProcessHandle .current ().pid ()));
125+ processBuilder .environment ().put ("CRYPTOMATOR_INSTALL_PATH" , installPath );
126+ processBuilder .start ();
127+
128+ return UpdateStep .EXIT ;
129+ }
130+
131+ }
0 commit comments