Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<project.jdk.version>25</project.jdk.version>

<!-- runtime dependencies -->
<api.version>1.7.0</api.version>
<api.version>1.8.0-SNAPSHOT</api.version>
<slf4j.version>2.0.17</slf4j.version>

<!-- test dependencies -->
Expand Down Expand Up @@ -63,6 +63,20 @@
</license>
</licenses>

<repositories>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.integrations.update.UpdateMechanism;
import org.cryptomator.macos.autostart.MacAutoStartProvider;
import org.cryptomator.macos.keychain.MacSystemKeychainAccess;
import org.cryptomator.macos.keychain.TouchIdKeychainAccess;
import org.cryptomator.macos.revealpath.OpenCmdRevealPathService;
import org.cryptomator.macos.tray.MacTrayIntegrationProvider;
import org.cryptomator.macos.uiappearance.MacUiAppearanceProvider;
import org.cryptomator.macos.update.DmgUpdateMechanism;

module org.cryptomator.integrations.mac {
requires org.cryptomator.integrations.api;
Expand All @@ -19,4 +21,5 @@
provides RevealPathService with OpenCmdRevealPathService;
provides TrayIntegrationProvider with MacTrayIntegrationProvider;
provides UiAppearanceProvider with MacUiAppearanceProvider;
provides UpdateMechanism with DmgUpdateMechanism;
}
131 changes: 131 additions & 0 deletions src/main/java/org/cryptomator/macos/update/DmgUpdateMechanism.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.cryptomator.macos.update;

import org.cryptomator.integrations.common.LocalizedDisplayName;
import org.cryptomator.integrations.common.OperatingSystem;
import org.cryptomator.integrations.update.DownloadUpdateInfo;
import org.cryptomator.integrations.update.DownloadUpdateMechanism;
import org.cryptomator.integrations.update.UpdateFailedException;
import org.cryptomator.integrations.update.UpdateMechanism;
import org.cryptomator.integrations.update.UpdateStep;
import org.cryptomator.macos.common.Localization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.UUID;

@OperatingSystem(OperatingSystem.Value.MAC)
@LocalizedDisplayName(bundle = "MacIntegrationsBundle", key = "org.cryptomator.macos.update.dmg.displayName")
public class DmgUpdateMechanism extends DownloadUpdateMechanism {

private static final Logger LOG = LoggerFactory.getLogger(DmgUpdateMechanism.class);

@Override
protected DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) {
String suffix = switch (System.getProperty("os.arch")) {
case "aarch64", "arm64" -> "arm64.dmg";
default -> "x64.dmg";
};
var updateVersion = response.latestVersion().macVersion();
var asset = response.assets().stream().filter(a -> a.name().endsWith(suffix)).findAny().orElse(null);
if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion) && asset != null) {
return new DownloadUpdateInfo(this, updateVersion, asset);
} else {
return null;
}
}

@Override
public UpdateStep secondStep(Path workDir, Path assetPath, DownloadUpdateInfo updateInfo) {
return UpdateStep.of(Localization.get().getString("org.cryptomator.macos.update.dmg.unpacking"), () -> this.unpack(workDir, assetPath));
}

private UpdateStep unpack(Path workDir, Path assetPath) throws IOException {
// Extract Cryptomator.app from the .dmg file
var processBuilder = new ProcessBuilder(List.of("/bin/zsh", "-s"));
processBuilder.directory(workDir.toFile());
processBuilder.environment().put("DMG_PATH", assetPath.toString());
processBuilder.environment().put("MOUNT_ID", UUID.randomUUID().toString());
Process p = processBuilder.start();
try {
try (var stdin = p.outputWriter()) {
stdin.write("""
trap 'hdiutil detach "/Volumes/Cryptomator_${MOUNT_ID}" -quiet || true' EXIT
hdiutil attach "${DMG_PATH}" -mountpoint "/Volumes/Cryptomator_${MOUNT_ID}" -nobrowse -quiet
cp -R "/Volumes/Cryptomator_${MOUNT_ID}/Cryptomator.app" 'Cryptomator.app'
""");
}
if (p.waitFor() != 0) {
LOG.error("Failed to extract DMG, exit code: {}, output: {}", p.exitValue(), new String(p.getErrorStream().readAllBytes()));
throw new IOException("Failed to extract DMG, exit code: " + p.exitValue());
}
LOG.debug("Unpacked app: {}", workDir.resolve("Cryptomator.app"));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new InterruptedIOException("Failed to extract DMG, interrupted");
}
return UpdateStep.of(Localization.get().getString("org.cryptomator.macos.update.dmg.verifying"), () -> this.verify(workDir, assetPath));
}

private UpdateStep verify(Path workDir, Path assetPath) throws IOException {
// Verify code signature of the extracted .app
var processBuilder = new ProcessBuilder(List.of("/bin/zsh", "-s"));
processBuilder.directory(workDir.toFile());
Process p = processBuilder.start();
try {
try (var stdin = p.outputWriter()) {
stdin.write("""
codesign --verify --deep --strict 'Cryptomator.app'
spctl --assess --type execute 'Cryptomator.app'
""");
}
if (p.waitFor() != 0) {
LOG.error("Checking code signature failed: {}, output: {}", p.exitValue(), new String(p.getErrorStream().readAllBytes()));
throw new UpdateFailedException("Invalid Code Signature.");
}
LOG.debug("Verified app: {}", workDir.resolve("Cryptomator.app"));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new InterruptedIOException("Failed to extract DMG, interrupted");
}
Comment on lines +93 to +96
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the error message for verification context.

Line 95's error message says "Failed to extract DMG, interrupted" but this is in the verify method after the extraction has already succeeded. The message should reflect that verification was interrupted.

Apply this diff:

 	} catch (InterruptedException e) {
 		Thread.currentThread().interrupt();
-		throw new InterruptedIOException("Failed to extract DMG, interrupted");
+		throw new InterruptedIOException("Code signature verification interrupted");
 	}
🤖 Prompt for AI Agents
In src/main/java/org/cryptomator/macos/update/DmgUpdateMechanism.java around
lines 93 to 96, the InterruptedIOException message is inaccurate for the verify
method — it currently says "Failed to extract DMG, interrupted" although
extraction has already completed; change the thrown InterruptedIOException
message to reflect that verification was interrupted (e.g., "Verification
interrupted") while preserving the Thread.currentThread().interrupt() behavior.

return UpdateStep.of(Localization.get().getString("org.cryptomator.macos.update.dmg.restarting"), () -> this.restart(workDir));
}

public UpdateStep restart(Path workDir) throws IllegalStateException, IOException {
String selfPath = ProcessHandle.current().info().command().orElse("");
String installPath;
if (selfPath.startsWith("/Applications/Cryptomator.app")) {
installPath = "/Applications/Cryptomator.app";
} else if (selfPath.contains("/Cryptomator.app/")) {
installPath = selfPath.substring(0, selfPath.indexOf("/Cryptomator.app/")) + "/Cryptomator.app";
} else {
throw new UpdateFailedException("Cannot determine destination path for Cryptomator.app, current path: " + selfPath);
}
LOG.info("Restarting to apply Update in {} now...", workDir);
String script = """
while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.2; done;
if [ -d "${CRYPTOMATOR_INSTALL_PATH}" ]; then
echo "Removing old installation at ${CRYPTOMATOR_INSTALL_PATH}";
rm -rf "${CRYPTOMATOR_INSTALL_PATH}"
fi
mv 'Cryptomator.app' "${CRYPTOMATOR_INSTALL_PATH}";
open "${CRYPTOMATOR_INSTALL_PATH}";
""";
Files.writeString(workDir.resolve("install.sh"), script, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
var command = List.of("/bin/zsh", "-c", "/usr/bin/nohup zsh install.sh >install.log 2>&1 &");
var processBuilder = new ProcessBuilder(command);
processBuilder.directory(workDir.toFile());
processBuilder.environment().put("CRYPTOMATOR_PID", String.valueOf(ProcessHandle.current().pid()));
processBuilder.environment().put("CRYPTOMATOR_INSTALL_PATH", installPath);
processBuilder.start();

return UpdateStep.EXIT;
}

}
6 changes: 5 additions & 1 deletion src/main/resources/MacIntegrationsBundle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
org.cryptomator.macos.keychain.displayName=macOS Keychain
org.cryptomator.macos.keychain.touchIdDisplayName=Touch ID
org.cryptomator.macos.keychain.touchIdDisplayName=Touch ID
org.cryptomator.macos.update.dmg.displayName=Download .dmg file
org.cryptomator.macos.update.dmg.unpacking=Unpacking...
org.cryptomator.macos.update.dmg.verifying=Verifying...
org.cryptomator.macos.update.dmg.restarting=Restarting...