]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/api/UsernameLinkUrl.java
Implement username links
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / api / UsernameLinkUrl.java
diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UsernameLinkUrl.java b/lib/src/main/java/org/asamk/signal/manager/api/UsernameLinkUrl.java
new file mode 100644 (file)
index 0000000..f8fff6e
--- /dev/null
@@ -0,0 +1,82 @@
+package org.asamk.signal.manager.api;
+
+import org.signal.core.util.Base64;
+import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
+import org.whispersystems.signalservice.api.util.UuidUtil;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
+public final class UsernameLinkUrl {
+
+    private static final Pattern URL_REGEX = Pattern.compile("(https://)?signal.me/?#eu/([a-zA-Z0-9+\\-_/]+)");
+
+    private static final String BASE_URL = "https://signal.me/#eu/";
+
+    private final String url;
+    private final UsernameLinkComponents usernameLinkComponents;
+
+    public static UsernameLinkUrl fromUri(String url) throws InvalidUsernameLinkException {
+        final var matcher = URL_REGEX.matcher(url);
+        if (!matcher.matches()) {
+            throw new InvalidUsernameLinkException("Invalid username link");
+        }
+        final var path = matcher.group(2);
+        final byte[] allBytes;
+        try {
+            allBytes = Base64.decode(path);
+        } catch (IOException e) {
+            throw new InvalidUsernameLinkException("Invalid base64 encoding");
+        }
+
+        if (allBytes.length != 48) {
+            throw new InvalidUsernameLinkException("Invalid username link");
+        }
+
+        final var entropy = Arrays.copyOfRange(allBytes, 0, 32);
+        final var serverId = Arrays.copyOfRange(allBytes, 32, allBytes.length);
+        final var serverIdUuid = UuidUtil.parseOrNull(serverId);
+        if (serverIdUuid == null) {
+            throw new InvalidUsernameLinkException("Invalid serverId");
+        }
+
+        return new UsernameLinkUrl(new UsernameLinkComponents(entropy, serverIdUuid));
+    }
+
+    public UsernameLinkUrl(UsernameLinkComponents usernameLinkComponents) {
+        this.usernameLinkComponents = usernameLinkComponents;
+        this.url = createUrl(usernameLinkComponents);
+    }
+
+    private static String createUrl(UsernameLinkComponents usernameLinkComponents) {
+        final var entropy = usernameLinkComponents.getEntropy();
+        final var serverId = UuidUtil.toByteArray(usernameLinkComponents.getServerId());
+
+        final var combined = new byte[entropy.length + serverId.length];
+        System.arraycopy(entropy, 0, combined, 0, entropy.length);
+        System.arraycopy(serverId, 0, combined, entropy.length, serverId.length);
+
+        final var base64 = Base64.encodeUrlSafeWithoutPadding(combined);
+        return BASE_URL + base64;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public UsernameLinkComponents getComponents() {
+        return usernameLinkComponents;
+    }
+
+    public static final class InvalidUsernameLinkException extends Exception {
+
+        public InvalidUsernameLinkException(String message) {
+            super(message);
+        }
+
+        public InvalidUsernameLinkException(Throwable cause) {
+            super(cause);
+        }
+    }
+}