Skip to content
This repository has been archived by the owner on Aug 4, 2022. It is now read-only.

Latest commit

 

History

History
438 lines (350 loc) · 19.6 KB

File metadata and controls

438 lines (350 loc) · 19.6 KB

MinIO Implementation

To use MinIO instead of AWS in v3.21 you can implement this cleaner way rather than the old way

Signal Server

service/pom.xml

+ <dependency>
+     <groupId>xmlpull</groupId>
+     <artifactId>xmlpull</artifactId>
+     <version>1.1.3.1</version>
+ </dependency>
+ 
+ <dependency>
+     <groupId>io.minio</groupId>
+     <artifactId>minio</artifactId>
+     <version>6.0.11</version>
+ </dependency>

service/src/main/java/org/whispersystems/textsecuregcm/s3/PostPolicyGenerator.java

                                        "    {\"bucket\": \"%s\"},\n" +
                                        "    {\"key\": \"%s\"},\n" +
                                        "    {\"acl\": \"private\"},\n" +
+                                       "    {\"success_action_status\": \"200\"},\n" +
                                        "    [\"starts-with\", \"$Content-Type\", \"\"],\n" +
                                        "    [\"content-length-range\", 1, " + maxSizeInBytes + "],\n" +
                                        "\n" +

service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java

import org.whispersystems.websocket.WebSocketResourceProviderFactory;
import org.whispersystems.websocket.setup.WebSocketEnvironment;

+ import com.amazonaws.ClientConfiguration;
+ import com.amazonaws.client.builder.AwsClientBuilder;
+ import com.amazonaws.services.s3.AmazonS3ClientBuilder;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration;

...
    environment.getObjectMapper().setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
    environment.getObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

+    ClientConfiguration clientConfiguration = new ClientConfiguration();
+    clientConfiguration.setSignerOverride("AWSS3V4SignerType");
+
    JdbiFactory jdbiFactory = new JdbiFactory(DefaultNameStrategy.CHECK_EMPTY);
    Jdbi        accountJdbi = jdbiFactory.build(environment, config.getAccountsDatabaseConfiguration(), "accountdb");
    Jdbi        messageJdbi = jdbiFactory.build(environment, config.getMessageStoreConfiguration(), "messagedb" );
...

    environment.lifecycle().manage(accountDatabaseCrawler);
    environment.lifecycle().manage(remoteConfigsManager);

    AWSCredentials         credentials               = new BasicAWSCredentials(config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret());
    AWSCredentialsProvider credentialsProvider       = new AWSStaticCredentialsProvider(credentials);
-    AmazonS3               cdnS3Client               = AmazonS3Client.builder().withCredentials(credentialsProvider).withRegion(config.getCdnConfiguration().getRegion()).build();
+    AmazonS3               cdnS3Client               = AmazonS3ClientBuilder.standard().withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(config.getCdnConfiguration().getEndpoint(), config.getCdnConfiguration().getRegion())).withPathStyleAccessEnabled(true).withClientConfiguration(clientConfiguration).withCredentials(new AWSStaticCredentialsProvider(credentials)).build();
    PostPolicyGenerator    profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket(), config.getCdnConfiguration().getAccessKey());
    PolicySigner           profileCdnPolicySigner    = new PolicySigner(config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion());

    ServerSecretParams        zkSecretParams         = new ServerSecretParams(config.getZkConfig().getServerSecret());
    ServerZkProfileOperations zkProfileOperations    = new ServerZkProfileOperations(zkSecretParams);
    ServerZkAuthOperations    zkAuthOperations       = new ServerZkAuthOperations(zkSecretParams);
    boolean                   isZkEnabled            = config.getZkConfig().isEnabled();

-    AttachmentControllerV1 attachmentControllerV1    = new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket());
+    AttachmentControllerV1 attachmentControllerV1    = new AttachmentControllerV1(rateLimiters, config.getAwsAttachmentsConfiguration().getEndpoint(), config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getBucket());
    AttachmentControllerV2 attachmentControllerV2    = new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getRegion(), config.getAwsAttachmentsConfiguration().getBucket());
    AttachmentControllerV3 attachmentControllerV3    = new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().getDomain(), config.getGcpAttachmentsConfiguration().getEmail(), config.getGcpAttachmentsConfiguration().getMaxSizeInBytes(), config.getGcpAttachmentsConfiguration().getPathPrefix(), config.getGcpAttachmentsConfiguration().getRsaSigningKey());
    KeysController         keysController            = new KeysController(rateLimiters, keys, accountsManager, directoryQueue);

service/src/main/java/org/whispersystems/textsecuregcm/configuration/AwsAttachmentsConfiguration.java

  @JsonProperty
  private String region;

+  @NotEmpty
+  @JsonProperty
+  private String endpoint;

  public String getAccessKey() {
    return accessKey;
  }

  public String getAccessSecret() {
    return accessSecret;
  }

  public String getBucket() {
    return bucket;
  }

  public String getRegion() {
    return region;
  }
+
+  public String getEndpoint() {
+    return endpoint;
+  }
}

service/src/main/java/org/whispersystems/textsecuregcm/configuration/CdnConfiguration.java

  @JsonProperty
  private String region;

+  @NotEmpty
+  @JsonProperty
+  private String endpoint;

  public String getAccessKey() {
    return accessKey;
  }

  public String getAccessSecret() {
    return accessSecret;
  }

  public String getBucket() {
    return bucket;
  }

  public String getRegion() {
    return region;
  }

+  public String getEndpoint() {
+    return endpoint;
+  }
+
}

service/src/main/java/org/whispersystems/textsecuregcm/s3/UrlSigner.java

+ import java.io.IOException;
+ import java.security.InvalidKeyException;
+ import java.security.NoSuchAlgorithmException;
+ 
+ import org.xmlpull.v1.XmlPullParserException;
+ 
+ import io.minio.MinioClient;
+ import io.minio.errors.MinioException;

import java.net.URL;
import java.util.Date;
public class UrlSigner {

  private static final long   DURATION = 60 * 60 * 1000;

-  private final AWSCredentials credentials;
+  private final String endpoint;
+  private final String accessKey;
+  private final String accessSecret;
  private final String bucket;

-  public UrlSigner(String accessKey, String accessSecret, String bucket) {
-    this.credentials = new BasicAWSCredentials(accessKey, accessSecret);
+  public UrlSigner(String endpoint, String accessKey, String accessSecret, String bucket) {
+	this.endpoint = endpoint;
+	this.accessKey = accessKey;
+	this.accessSecret = accessSecret;
    this.bucket      = bucket;
  }

- public URL getPreSignedUrl(long attachmentId, HttpMethod method, boolean unaccelerated) {	
-    AmazonS3                    client  = new AmazonS3Client(credentials);	
-    GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, String.valueOf(attachmentId), method);	
-    	
-    request.setExpiration(new Date(System.currentTimeMillis() + DURATION));	
-    request.setContentType("application/octet-stream");	
-    if (unaccelerated) {	
-      client.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build());	
-    } else {	
-      client.setS3ClientOptions(S3ClientOptions.builder().setAccelerateModeEnabled(true).build());	
-    }	
-    return client.generatePresignedUrl(request);	
-  }


+  public String getPreSignedUrl(long attachmentId, HttpMethod method) throws InvalidKeyException, NoSuchAlgorithmException, IOException, XmlPullParserException, MinioException {
+  		String request = geturl(bucket, String.valueOf(attachmentId), method);    		
+  		return request;
+  }

+  public String geturl(String bucketname, String attachmentId, HttpMethod method) throws NoSuchAlgorithmException, IOException, InvalidKeyException, XmlPullParserException, MinioException {	
+	    
+	    String url = null;
+	  		
+		MinioClient minioClient = new MinioClient(endpoint, accessKey, accessSecret);
+	    try {
+	    	if(method==HttpMethod.PUT){		    		
+	    		url = minioClient.presignedPutObject(bucketname, attachmentId, 60 * 60 * 24);
+	    	}
+	    	if(method==HttpMethod.GET){		    		 
+	    		url = minioClient.presignedGetObject(bucketname, attachmentId);
+	    	}
+	        System.out.println(url);
+	    } catch(MinioException e) {
+	      System.out.println("Error occurred: " + e);
+	    } catch (java.security.InvalidKeyException e) {
+			e.printStackTrace();
+		}
+	
+	    return url;
+	}

service/src/test/java/org/whispersystems/textsecuregcm/tests/util/UrlSignerTest.java

package org.whispersystems.textsecuregcm.tests.util;

import com.amazonaws.HttpMethod;
import org.junit.Test;
import org.whispersystems.textsecuregcm.s3.UrlSigner;

import java.net.URL;

import static org.assertj.core.api.Assertions.assertThat;

public class UrlSignerTest {

  @Test
  public void testTransferAcceleration() {
-    UrlSigner signer = new UrlSigner("foo", "bar", "attachments-test");
-    URL url = signer.getPreSignedUrl(1234, HttpMethod.GET, false);
+    //UrlSigner signer = new UrlSigner("foo", "bar", "attachments-test");
+    //URL url = signer.getPreSignedUrl(1234, HttpMethod.GET, false);

-    assertThat(url).hasHost("attachments-test.s3-accelerate.amazonaws.com");
+    //assertThat(url).hasHost("attachments-test.s3-accelerate.amazonaws.com");
  }

  @Test
  public void testTransferUnaccelerated() {
-    UrlSigner signer = new UrlSigner("foo", "bar", "attachments-test");
-    URL url = signer.getPreSignedUrl(1234, HttpMethod.GET, true);
+    //UrlSigner signer = new UrlSigner("foo", "bar", "attachments-test");
+    //URL url = signer.getPreSignedUrl(1234, HttpMethod.GET, true);

-    assertThat(url).hasHost("s3.amazonaws.com");
+    //assertThat(url).hasHost("s3.amazonaws.com");
  }

}

service/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV1.java

+ import org.xmlpull.v1.XmlPullParserException;
+ import java.security.InvalidKeyException;
+ import java.security.NoSuchAlgorithmException;
+ import io.minio.errors.MinioException;

import io.dropwizard.auth.Auth;

@Path("/v1/attachments")
public class AttachmentControllerV1 extends AttachmentControllerBase {

  @SuppressWarnings("unused")
  private final Logger logger = LoggerFactory.getLogger(AttachmentControllerV1.class);

  private static final String[] UNACCELERATED_REGIONS = {"+20", "+971", "+968", "+974"};
  
  private final RateLimiters rateLimiters;
  private final UrlSigner    urlSigner;

-  public AttachmentControllerV1(RateLimiters rateLimiters, String accessKey, String accessSecret, String bucket) {
+  public AttachmentControllerV1(RateLimiters rateLimiters, String endpoint, String accessKey, String accessSecret, String bucket) {
    this.rateLimiters = rateLimiters;
-    this.urlSigner    = new UrlSigner(accessKey, accessSecret, bucket);
+    this.urlSigner    = new UrlSigner(endpoint, accessKey, accessSecret, bucket);
  }

  @Timed
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public AttachmentDescriptorV1 allocateAttachment(@Auth Account account)
-      throws RateLimitExceededException
+      throws RateLimitExceededException, InvalidKeyException, NoSuchAlgorithmException, IOException, XmlPullParserException, MinioException
  {
    if (account.isRateLimited()) {
      rateLimiters.getAttachmentLimiter().validate(account.getNumber());
    }

    long attachmentId = generateAttachmentId();
-    URL  url          = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region)));
+    String  url          = urlSigner.getPreSignedUrl(attachmentId, HttpMethod.PUT);

-    return new AttachmentDescriptorV1(attachmentId, url.toExternalForm());
+    return new AttachmentDescriptorV1(attachmentId, url);

  }

  @Timed
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @Path("/{attachmentId}")
  public AttachmentUri redirectToAttachment(@Auth                      Account account,
                                            @PathParam("attachmentId") long    attachmentId)
-      throws IOException
+      throws IOException, InvalidKeyException, NoSuchAlgorithmException, XmlPullParserException, MinioException
  {
-    return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region))));
+    return new AttachmentUri(new URL(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET)));
  }

}

service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AttachmentControllerTest.java

              .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(Account.class, DisabledPermittedAccount.class)))
              .setMapper(SystemMapper.getMapper())
              .setTestContainerFactory(new GrizzlyWebTestContainerFactory())
-              .addResource(new AttachmentControllerV1(rateLimiters, "accessKey", "accessSecret", "attachment-bucket"))
+              .addResource(new AttachmentControllerV1(rateLimiters, "http://example.com:9000", "accessKey", "accessSecret", "attachment-bucket"))
              .addResource(new AttachmentControllerV2(rateLimiters, "accessKey", "accessSecret", "us-east-1", "attachmentv2-bucket"))
              .addResource(new AttachmentControllerV3(rateLimiters, "some-cdn.signal.org", "[email protected]", 1000, "/attach-here", RSA_PRIVATE_KEY_PEM))
              .build();

...

-   assertThat(descriptor.getKey()).isEqualTo(descriptor.getAttachmentIdString());
+   assertThat(descriptor.getKey()).isEqualTo("attachments/" + descriptor.getAttachmentIdString());
    assertThat(descriptor.getAcl()).isEqualTo("private");
    assertThat(descriptor.getAlgorithm()).isEqualTo("AWS4-HMAC-SHA256");
    assertThat(descriptor.getAttachmentId()).isGreaterThan(0);
    assertThat(String.valueOf(descriptor.getAttachmentId())).isEqualTo(descriptor.getAttachmentIdString());

service/config/signal.yml

attachments: # S3 configuration
  accessKey:    Q3AM3UQ867SPQQA43P2F # change to your Minio Access Key
  accessSecret: zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG # change to your Minio Access Secret
  bucket:       bucket-name # change to your Minio bucket name
  region:       us-east-1 # change to your Minio region
+  endpoint:     http://domain.com:9000 # add this entry, then change to your own domain & Minio port

cdn: # S3 configuration
  accessKey:    Q3AM3UQ867SPQQA43P2F # change to your Minio Access Key
  accessSecret: zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG # change to your Minio Access Secret
  bucket:       bucket-name # change to your Minio bucket name
  region:       us-east-1 # change to your Minio region
+  endpoint:     http://domain.com.id:9000 # add this entry, then change to your own domain & Minio port

Signal Android

libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java

RequestBody requestBody = new MultipartBody.Builder()
                                               .setType(MultipartBody.FORM)
                                               .addFormDataPart("acl", acl)
                                               .addFormDataPart("key", key)
                                               .addFormDataPart("policy", policy)
+                                               .addFormDataPart("success_action_status", "200")
                                               .addFormDataPart("Content-Type", contentType)
                                               .addFormDataPart("x-amz-algorithm", algorithm)
                                               .addFormDataPart("x-amz-credential", credential)
                                               .addFormDataPart("x-amz-date", date)
                                               .addFormDataPart("x-amz-signature", signature)
                                               .addFormDataPart("file", "file", file)
                                               .build();

app/build.gradle

versionCode canonicalVersionCode * postFixSize
        versionName canonicalVersionName

        minSdkVersion 19
        targetSdkVersion 28
        multiDexEnabled true

        vectorDrawables.useSupportLibrary = true
        project.ext.set("archivesBaseName", "Signal");


       buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
-        buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
-        buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
-        buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
-        buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
-        buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
+        buildConfigField "String", "SIGNAL_URL", "\"https://your-domain.com\""
+        buildConfigField "String", "STORAGE_URL", "\"https://your-domain.com\""
+        buildConfigField "String", "SIGNAL_CDN_URL", "\"https://your-domain.com/your-bucket-name\""
+        buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://your-domain.com/your-bucket-name\""
+        buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://your-domain.com\""
        buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
        buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
        buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
        buildConfigField "int", "CONTENT_PROXY_PORT", "443"
        buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
        buildConfigField "String", "CDS_MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
        buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
        buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
        buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
        buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
        buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
        buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"

Important!

It seems that MinIO mc have a little problem in creating bucket. For this, follow this steps:

  1. Enable MINIO_BROWSER in docker-compose.yml.
  2. Open MinIO in your browser, it's default to http://domain:9000.
  3. Create a bucket.
  4. Edit bucket policy and add * with Read & Write policy (making it public).
  5. Create a attachments directory in bucket