Skip to content

Feature/uvf draft #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 27, 2025
Merged
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
41 changes: 22 additions & 19 deletions .github/workflows/publish-central.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
name: Publish to Maven Central
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag'
required: true
default: '0.0.0'
push:
release:
types: [published]
jobs:
publish:
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[release snapshot]')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: "refs/tags/${{ github.event.inputs.tag }}"
- uses: actions/setup-java@v4
with:
java-version: 23
distribution: 'temurin'
cache: 'maven'
server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml
server-username: MAVEN_USERNAME # env variable for username in deploy
server-password: MAVEN_PASSWORD # env variable for token in deploy
gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase
- name: Enforce project version ${{ github.event.inputs.tag }}
run: mvn versions:set -B -DnewVersion="$GIT_TAG"
env:
GIT_TAG: ${{ github.event.inputs.tag }}
server-id: central
server-username: MAVEN_CENTRAL_USERNAME
server-password: MAVEN_CENTRAL_PASSWORD
- name: Enforce project version ${{ github.event.release.tag_name }}
if: github.event_name == 'release'
run: mvn versions:set -B -DnewVersion=${{ github.event.release.tag_name }}
- name: Verify this is a SNAPSHOT
if: github.event_name == 'push' && contains(github.event.head_commit.message, '[release snapshot]')
run: |
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
if [[ "$VERSION" != *-SNAPSHOT ]]; then
echo "::error file=pom.xml,title=Not a SNAPSHOT::Project version ($VERSION) does not end with -SNAPSHOT"
exit 1
fi
- name: Deploy
run: mvn deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }}
28 changes: 13 additions & 15 deletions .github/workflows/publish-github.yml
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
name: Publish to GitHub Packages
on:
push:
release:
types: [published]
jobs:
publish:
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[release snapshot]')
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: 23
distribution: 'temurin'
cache: 'maven'
gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase
- name: Enforce project version ${{ github.event.release.tag_name }}
if: github.event_name == 'release'
run: mvn versions:set -B -DnewVersion=${{ github.event.release.tag_name }}
- name: Verify this is a SNAPSHOT
if: github.event_name == 'push' && contains(github.event.head_commit.message, '[release snapshot]')
run: |
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
if [[ "$VERSION" != *-SNAPSHOT ]]; then
echo "::error file=pom.xml,title=Not a SNAPSHOT::Project version ($VERSION) does not end with -SNAPSHOT"
exit 1
fi
- name: Deploy
run: mvn deploy -B -DskipTests -Psign,deploy-github --no-transfer-progress
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_USERNAME: 'Cryptobot'
SLACK_ICON:
SLACK_ICON_EMOJI: ':bot:'
SLACK_CHANNEL: 'cryptomator-desktop'
SLACK_TITLE: "Published ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}"
SLACK_MESSAGE: "Ready to <https://github.com/${{ github.repository }}/actions/workflows/publish-central.yml|deploy to Maven Central>."
SLACK_FOOTER:
MSG_MINIMAL: true
MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }}
MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }}
57 changes: 24 additions & 33 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,22 @@
<maven.compiler.target>8</maven.compiler.target>

<!-- dependencies -->
<gson.version>2.11.0</gson.version>
<guava.version>33.3.0-jre</guava.version>
<siv-mode.version>1.6.0</siv-mode.version>
<bouncycastle.version>1.78.1</bouncycastle.version>
<slf4j.version>2.0.16</slf4j.version>
<gson.version>2.12.1</gson.version>
<guava.version>33.4.0-jre</guava.version>
<siv-mode.version>1.6.1</siv-mode.version>
<bouncycastle.version>1.80</bouncycastle.version>
<slf4j.version>2.0.17</slf4j.version>

<!-- test dependencies -->
<junit.jupiter.version>5.11.0</junit.jupiter.version>
<junit.jupiter.version>5.12.0</junit.jupiter.version>
<mockito.version>5.15.2</mockito.version>
<hamcrest.version>3.0</hamcrest.version>
<jmh.version>1.37</jmh.version>

<!-- build plugin dependencies -->
<dependency-check.version>11.1.1</dependency-check.version>
<jacoco.version>0.8.12</jacoco.version>
<nexus-staging.version>1.7.0</nexus-staging.version>
<dependency-check.version>12.1.0</dependency-check.version>
<jacoco.version>0.8.13</jacoco.version>
<central-publishing.version>0.7.0</central-publishing.version>
</properties>

<licenses>
Expand Down Expand Up @@ -171,7 +171,7 @@
<rules>
<requireJavaVersion>
<message>You need at least JDK 22 to build this project.</message>
<version>[21,)</version>
<version>[22,)</version>
</requireJavaVersion>
</rules>
</configuration>
Expand All @@ -180,7 +180,7 @@
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<version>3.14.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
Expand Down Expand Up @@ -284,6 +284,9 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<argLine>@{argLine} -Dnet.bytebuddy.experimental=true</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down Expand Up @@ -412,7 +415,7 @@
</profile>

<profile>
<id>release</id>
<id>sign</id>
<build>
<plugins>
<plugin>
Expand All @@ -426,10 +429,7 @@
<goal>sign</goal>
</goals>
<configuration>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
<signer>bc</signer>
</configuration>
</execution>
</executions>
Expand All @@ -440,26 +440,17 @@

<profile>
<id>deploy-central</id>
<distributionManagement>
<repository>
<id>ossrh</id>
<name>Maven Central</name>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<build>
<plugins>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>${central-publishing.version}</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
</configuration>
</plugin>
</plugins>
</build>
Expand Down
4 changes: 1 addition & 3 deletions src/main/java/org/cryptomator/cryptolib/api/Cryptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ public interface Cryptor extends Destroyable, AutoCloseable {
* High-Level API for file name encryption and decryption
* @return utility for encryption and decryption of file names in the context of a directory
*/
default DirectoryContentCryptor directoryContentCryptor() {
throw new UnsupportedOperationException("not implemented");
}
DirectoryContentCryptor directoryContentCryptor();

@Override
void destroy();
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/org/cryptomator/cryptolib/api/UVFMasterkey.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,18 @@ public static UVFMasterkey fromDecryptedPayload(String json) {
Preconditions.checkArgument("HKDF-SHA512".equals(root.get("kdf").getAsString()));
Preconditions.checkArgument(root.get("seeds").isJsonObject());

Base64.Decoder base64 = Base64.getDecoder();
byte[] initialSeed = base64.decode(root.get("initialSeed").getAsString());
byte[] latestSeed = base64.decode(root.get("latestSeed").getAsString());
byte[] kdfSalt = base64.decode(root.get("kdfSalt").getAsString());
Base64.Decoder base64Url = Base64.getUrlDecoder();
byte[] initialSeed = base64Url.decode(root.get("initialSeed").getAsString());
byte[] latestSeed = base64Url.decode(root.get("latestSeed").getAsString());
byte[] kdfSalt = base64Url.decode(root.get("kdfSalt").getAsString());

Map<Integer, byte[]> seeds = new HashMap<>();
ByteBuffer intBuf = ByteBuffer.allocate(Integer.BYTES);
for (Map.Entry<String, JsonElement> entry : root.getAsJsonObject("seeds").asMap().entrySet()) {
intBuf.clear();
intBuf.put(base64.decode(entry.getKey()));
intBuf.put(base64Url.decode(entry.getKey()));
int seedNum = intBuf.getInt(0);
byte[] seedVal = base64.decode(entry.getValue().getAsString());
byte[] seedVal = base64Url.decode(entry.getValue().getAsString());
seeds.put(seedNum, seedVal);
}
return new UVFMasterkey(seeds, kdfSalt, ByteBuffer.wrap(initialSeed).getInt(), ByteBuffer.wrap(latestSeed).getInt());
Expand Down
10 changes: 2 additions & 8 deletions src/main/java/org/cryptomator/cryptolib/v1/Constants.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
/*******************************************************************************
* Copyright (c) 2016 Sebastian Stenzel and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE.txt.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
package org.cryptomator.cryptolib.v1;

final class Constants {

private Constants() {
}

static final String C9R_FILE_EXT = ".c9r";

static final String CONTENT_ENC_ALG = "AES";

static final int NONCE_SIZE = 16;
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cryptomator.cryptolib.v1;

import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
import org.cryptomator.cryptolib.api.FileHeaderCryptor;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
Expand Down Expand Up @@ -54,6 +55,11 @@ public FileNameCryptor fileNameCryptor(int revision) {
throw new UnsupportedOperationException();
}

@Override
public DirectoryContentCryptor directoryContentCryptor() {
return new DirectoryContentCryptorImpl(this);
}

@Override
public boolean isDestroyed() {
return masterkey.isDestroyed();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.cryptomator.cryptolib.v1;

import com.google.common.io.BaseEncoding;
import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
import org.cryptomator.cryptolib.api.DirectoryMetadata;

import java.nio.charset.StandardCharsets;
import java.util.UUID;

import static org.cryptomator.cryptolib.v1.Constants.C9R_FILE_EXT;

class DirectoryContentCryptorImpl implements DirectoryContentCryptor {

private final CryptorImpl cryptor;

public DirectoryContentCryptorImpl(CryptorImpl cryptor) {
this.cryptor = cryptor;
}

// DIRECTORY METADATA

@Override
public DirectoryMetadataImpl rootDirectoryMetadata() {
return new DirectoryMetadataImpl(new byte[0]);
}

@Override
public DirectoryMetadataImpl newDirectoryMetadata() {
byte[] dirId = UUID.randomUUID().toString().getBytes(StandardCharsets.US_ASCII);
return new DirectoryMetadataImpl(dirId);
}

@Override
public DirectoryMetadataImpl decryptDirectoryMetadata(byte[] ciphertext) {
// dirId is stored in plaintext
return new DirectoryMetadataImpl(ciphertext);
}

@Override
public byte[] encryptDirectoryMetadata(DirectoryMetadata directoryMetadata) {
// dirId is stored in plaintext
DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
return metadataImpl.dirId();
}

// DIR PATH

@Override
public String dirPath(DirectoryMetadata directoryMetadata) {
DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
String dirIdStr = cryptor.fileNameCryptor().hashDirectoryId(metadataImpl.dirId());
assert dirIdStr.length() == 32;
return "d/" + dirIdStr.substring(0, 2) + "/" + dirIdStr.substring(2);
}

// FILE NAMES

@Override
public Decrypting fileNameDecryptor(DirectoryMetadata directoryMetadata) {
DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
byte[] dirId = metadataImpl.dirId();
FileNameCryptorImpl fileNameCryptor = cryptor.fileNameCryptor();
return ciphertextAndExt -> {
String ciphertext = removeExtension(ciphertextAndExt);
return fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), ciphertext, dirId);
};
}

@Override
public Encrypting fileNameEncryptor(DirectoryMetadata directoryMetadata) {
DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
byte[] dirId = metadataImpl.dirId();
FileNameCryptorImpl fileNameCryptor = cryptor.fileNameCryptor();
return plaintext -> {
String ciphertext = fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), plaintext, dirId);
return ciphertext + C9R_FILE_EXT;
};
}

private static String removeExtension(String filename) {
if (filename.endsWith(C9R_FILE_EXT)) {
return filename.substring(0, filename.length() - C9R_FILE_EXT.length());
} else {
throw new IllegalArgumentException("Not a " + C9R_FILE_EXT + " file: " + filename);
}
}
}
Loading