]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2084 use RSA to encrypt settings
authorSimon Brandhof <simon.brandhof@gmail.com>
Tue, 13 Mar 2012 15:06:51 +0000 (16:06 +0100)
committerSimon Brandhof <simon.brandhof@gmail.com>
Tue, 13 Mar 2012 15:06:51 +0000 (16:06 +0100)
sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
sonar-plugin-api/src/main/java/org/sonar/api/config/Base64Cipher.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/config/Cipher.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/config/Encryption.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/config/RsaCipher.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java
sonar-plugin-api/src/test/java/org/sonar/api/config/RsaCipherTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt [new file with mode: 0644]
sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_public_key.txt [new file with mode: 0644]

index 75d3620545af33e2ece07ac15e65f938e67e63fa..32a67d2b5294d58055c872d8bf1989aeb216a5f4 100644 (file)
@@ -22,11 +22,22 @@ package org.sonar.api;
 /**
  * CoreProperties is used to group various properties of Sonar as well
  * as default values of configuration in a single place
- * 
+ *
  * @since 1.11
  */
 public interface CoreProperties {
 
+  /**
+   * @since 2.15
+   */
+  String ENCRYPTION_PATH_TO_PRIVATE_KEY = "sonar.encryption.privateKeyPath";
+
+  /**
+   * @since 2.15
+   */
+  String ENCRYPTION_PUBLIC_KEY = "sonar.encryption.publicKey";
+
+
   /**
    * @since 2.11
    */
@@ -85,7 +96,7 @@ public interface CoreProperties {
 
   /**
    * To determine value of this property use {@link org.sonar.api.resources.ProjectFileSystem#getSourceCharset()}.
-   * 
+   *
    * @since 2.6
    */
   String ENCODING_PROPERTY = "sonar.sourceEncoding";
@@ -148,8 +159,8 @@ public interface CoreProperties {
   String SERVER_BASE_URL = "sonar.core.serverBaseURL";
 
   /**
-   * @since 2.10
    * @see #SERVER_BASE_URL
+   * @since 2.10
    */
   String SERVER_BASE_URL_DEFAULT_VALUE = "http://localhost:9000";
 
@@ -169,8 +180,8 @@ public interface CoreProperties {
   String CPD_ENGINE = "sonar.cpd.engine";
 
   /**
-   * @since 2.11
    * @see #CPD_ENGINE
+   * @since 2.11
    */
   String CPD_ENGINE_DEFAULT_VALUE = "sonar";
 
@@ -180,8 +191,8 @@ public interface CoreProperties {
   String CPD_CROSS_RPOJECT = "sonar.cpd.cross_project";
 
   /**
-   * @since 2.11
    * @see #CPD_CROSS_RPOJECT
+   * @since 2.11
    */
   boolean CPD_CROSS_RPOJECT_DEFAULT_VALUE = false;
 
@@ -189,7 +200,7 @@ public interface CoreProperties {
 
   /**
    * Indicates whether Java bytecode analysis should be skipped.
-   * 
+   *
    * @since 2.0
    */
   String DESIGN_SKIP_DESIGN_PROPERTY = "sonar.skipDesign";
@@ -197,7 +208,7 @@ public interface CoreProperties {
 
   /**
    * Indicates whether Package Design Analysis should be skipped.
-   * 
+   *
    * @since 2.9
    */
   String DESIGN_SKIP_PACKAGE_DESIGN_PROPERTY = "sonar.skipPackageDesign";
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/Base64Cipher.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Base64Cipher.java
new file mode 100644 (file)
index 0000000..a04b40e
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.config;
+
+import org.apache.commons.codec.binary.Base64;
+
+final class Base64Cipher extends Cipher {
+  String encrypt(String clearText) {
+    return new String(Base64.encodeBase64(clearText.getBytes()));
+  }
+
+  String decrypt(String encryptedText) {
+    return new String(Base64.decodeBase64(encryptedText));
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/Cipher.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Cipher.java
new file mode 100644 (file)
index 0000000..7a39c7c
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.config;
+
+abstract class Cipher {
+  abstract String encrypt(String clearText);
+  abstract String decrypt(String encryptedText);
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/Encryption.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Encryption.java
new file mode 100644 (file)
index 0000000..edc7dfa
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.config;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @since 2.15
+ */
+public final class Encryption {
+
+  private static final String BASE64_ALGORITHM = "b64";
+  private final Base64Cipher base64Encryption;
+
+  private static final String RSA_ALGORITHM = "rsa";
+  private final RsaCipher rsaEncryption;
+
+  private final Map<String, Cipher> encryptions;
+  private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("\\{(.*?)\\}(.*)");
+
+  Encryption(Settings settings) {
+    base64Encryption = new Base64Cipher();
+    rsaEncryption = new RsaCipher(settings);
+    encryptions = ImmutableMap.of(
+        BASE64_ALGORITHM, base64Encryption,
+        RSA_ALGORITHM, rsaEncryption
+    );
+  }
+
+  public boolean isEncrypted(String value) {
+    return value.startsWith("{") && value.indexOf("}") > 1;
+  }
+
+  public String encrypt(String clearText) {
+    return encrypt(RSA_ALGORITHM, clearText);
+  }
+
+  public String scramble(String clearText) {
+    return encrypt(BASE64_ALGORITHM, clearText);
+  }
+
+  /**
+   * @return an array of 2 strings: {public key, private key}
+   */
+  public String[] generateRandomKeys() {
+    return rsaEncryption.generateRandomKeys();
+  }
+  
+  public String decrypt(String encryptedText) {
+    Matcher matcher = ENCRYPTED_PATTERN.matcher(encryptedText);
+    if (matcher.matches()) {
+      Cipher cipher = encryptions.get(matcher.group(0).toLowerCase(Locale.ENGLISH));
+      if (cipher != null) {
+        return cipher.decrypt(matcher.group(1));
+      }
+    }
+    return encryptedText;
+  }
+
+  private String encrypt(String algorithm, String clearText) {
+    Cipher cipher = encryptions.get(algorithm);
+    if (cipher == null) {
+      throw new IllegalArgumentException("Unknown cipher algorithm: " + algorithm);
+    }
+    return String.format("{%s}%s", algorithm, cipher.encrypt(clearText));
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/RsaCipher.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/RsaCipher.java
new file mode 100644 (file)
index 0000000..3e96c09
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.CoreProperties;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.RSAPrivateKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+
+final class RsaCipher extends Cipher {
+
+  private final Settings settings;
+
+  RsaCipher(Settings settings) {
+    this.settings = settings;
+  }
+
+  String encrypt(String clearText) {
+    String publicKey = settings.getClearString(CoreProperties.ENCRYPTION_PUBLIC_KEY);
+    if (StringUtils.isBlank(publicKey)) {
+      throw new IllegalStateException("RSA public key is missing. Please generate one.");
+    }
+    return encrypt(clearText, publicKey);
+  }
+
+  private String encrypt(String clearText, String publicKey) {
+    try {
+      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("RSA");
+      cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, toPublicKey(publicKey));
+      return new String(Base64.encodeBase64(cipher.doFinal(clearText.getBytes())));
+    } catch (Exception e) {
+      throw Throwables.propagate(e);
+    }
+  }
+
+  String decrypt(String encryptedText) {
+    try {
+      PrivateKey privateKey = loadPrivateKey();
+      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("RSA");
+      cipher.init(javax.crypto.Cipher.DECRYPT_MODE, privateKey);
+      byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText)));
+      return new String(cipherData);
+    } catch (Exception e) {
+      throw Throwables.propagate(e);
+    }
+  }
+
+  private PrivateKey loadPrivateKey() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException, InvalidKeyException {
+    String path = settings.getClearString(CoreProperties.ENCRYPTION_PATH_TO_PRIVATE_KEY);
+    return loadPrivateKeyFromFile(path);
+  }
+
+  @VisibleForTesting
+  PrivateKey loadPrivateKeyFromFile(String path) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, InvalidKeyException {
+    if (StringUtils.isBlank(path)) {
+      throw new IllegalStateException("Impossible to decrypt text without the private key. Please set the property " + CoreProperties.ENCRYPTION_PATH_TO_PRIVATE_KEY);
+    }
+    File file = new File(path);
+    if (!file.exists() || !file.isFile()) {
+      throw new IllegalStateException("The property " + CoreProperties.ENCRYPTION_PATH_TO_PRIVATE_KEY + " does not link to a valid file: " + path);
+    }
+
+    String s = FileUtils.readFileToString(file);
+    if (StringUtils.isBlank(s)) {
+      throw new IllegalStateException("No private key in the file: " + path);
+    }
+    String[] fields = StringUtils.split(StringUtils.trim(s), ",");
+    if (fields.length != 2) {
+      throw new IllegalStateException("Badly formatted private key in the file: " + path);
+    }
+    BigInteger modulus = new BigInteger(fields[0]);
+    BigInteger exponent = new BigInteger(fields[1]);
+    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+    return keyFactory.generatePrivate(new RSAPrivateKeySpec(modulus, exponent));
+  }
+
+  @VisibleForTesting
+  PublicKey toPublicKey(String text) throws InvalidKeySpecException, NoSuchAlgorithmException {
+    if (StringUtils.isBlank(text)) {
+      throw new IllegalArgumentException("The public key is blank");
+    }
+    String[] fields = StringUtils.split(StringUtils.trim(text), ",");
+    if (fields.length != 2) {
+      throw new IllegalStateException("Unknown format of public key: " + text);
+    }
+    BigInteger modulus = new BigInteger(fields[0]);
+    BigInteger exponent = new BigInteger(fields[1]);
+    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+    return keyFactory.generatePublic(new RSAPublicKeySpec(modulus, exponent));
+  }
+
+  String[] generateRandomKeys() {
+    try {
+      KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
+      gen.initialize(1024, new SecureRandom());
+      KeyPair pair = gen.generateKeyPair();
+
+      KeyFactory fact = KeyFactory.getInstance("RSA");
+      RSAPublicKeySpec pub = fact.getKeySpec(pair.getPublic(), RSAPublicKeySpec.class);
+      RSAPrivateKeySpec priv = fact.getKeySpec(pair.getPrivate(), RSAPrivateKeySpec.class);
+
+      String publicKey = pub.getModulus() + "," + pub.getPublicExponent();
+      String privateKey = priv.getModulus() + "," + priv.getPrivateExponent();
+      return new String[]{publicKey, privateKey};
+
+    } catch (Exception e) {
+      throw new IllegalStateException("Fail to generate random RSA keys", e);
+    }
+  }
+}
index 755345be3f567429bea703946a49bf6cb0279f9f..8de950ac7f58bb5e7b914f0180a63ae002d9f169 100644 (file)
@@ -38,15 +38,18 @@ import java.util.*;
  */
 public class Settings implements BatchComponent, ServerComponent {
 
-  protected Map<String, String> properties = Maps.newHashMap();
-  protected PropertyDefinitions definitions;
+  protected final Map<String, String> properties;
+  protected final PropertyDefinitions definitions;
+  private final Encryption encryption;
 
   public Settings() {
     this(new PropertyDefinitions());
   }
 
   public Settings(PropertyDefinitions definitions) {
+    this.properties = Maps.newHashMap();
     this.definitions = definitions;
+    this.encryption = new Encryption(this);
   }
 
   public final String getDefaultValue(String key) {
@@ -62,6 +65,19 @@ public class Settings implements BatchComponent, ServerComponent {
   }
 
   public final String getString(String key) {
+    String value = properties.get(key);
+    if (value == null) {
+      value = getDefaultValue(key);
+    } else if (encryption.isEncrypted(value)) {
+      value = encryption.decrypt(value);
+    }
+    return value;
+  }
+
+  /**
+   * Does not decrypt value.
+   */
+  protected String getClearString(String key) {
     String value = properties.get(key);
     if (value == null) {
       value = getDefaultValue(key);
@@ -217,7 +233,8 @@ public class Settings implements BatchComponent, ServerComponent {
   }
 
   public final Settings setProperties(Map<String, String> props) {
-    properties = Maps.newHashMap(props);
+    properties.clear();
+    properties.putAll(props);
     return this;
   }
 
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/config/RsaCipherTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/config/RsaCipherTest.java
new file mode 100644 (file)
index 0000000..2bd4a53
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.api.config;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public class RsaCipherTest {
+  @Test
+  public void encrypt() throws IOException {
+    Settings settings = new Settings();
+    settings.setProperty("sonar.encryption.publicKey", loadPublicKey());
+    RsaCipher cipher = new RsaCipher(settings);
+    String encryptedText = cipher.encrypt("sonar");
+    System.out.println(encryptedText);
+    assertThat(StringUtils.isNotBlank(encryptedText), is(true));
+    assertThat(Base64.isArrayByteBase64(encryptedText.getBytes()), is(true));
+  }
+
+  @Test
+  public void decrypt() throws URISyntaxException, IOException {
+    File file = new File(getClass().getResource("/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt").toURI());
+    Settings settings = new Settings();
+    settings.setProperty("sonar.encryption.privateKeyPath", file.getCanonicalPath());
+    RsaCipher cipher = new RsaCipher(settings);
+
+    // the following value has been encrypted with the public key /org/sonar/api/config/RsaCipherTest/rsa_public_key.txt
+    String clearText = cipher.decrypt("bnFlXnB5A8kLV4VR1FSGI4BmKd9I1E7euOQq/yB8a8RIpW34YYQX0toM5GTymY5EwkMO+KvfcpKXIvvhthr+5beW8v2nDux8n3VSH+tb+3wJZ+UYZQBQAQj2G8FVvYxbvRk3WVGn9bpw3x6195/gEneGvcG/A41/YsDHDce9zLw=");
+    assertThat(clearText, is("this is a secret"));
+  }
+
+  @Test
+  public void encryptThenDecrypt() throws URISyntaxException, IOException {
+    File file = new File(getClass().getResource("/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt").toURI());
+    Settings settings = new Settings();
+    settings.setProperty("sonar.encryption.publicKey", loadPublicKey());
+    settings.setProperty("sonar.encryption.privateKeyPath", file.getCanonicalPath());
+    RsaCipher cipher = new RsaCipher(settings);
+
+    assertThat(cipher.decrypt(cipher.encrypt("foo")), is("foo"));
+  }
+
+  @Test
+  public void loadPrivateKeyFromFile() throws Exception {
+    File file = new File(getClass().getResource("/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt").toURI());
+    RsaCipher cipher = new RsaCipher(new Settings());
+    PrivateKey privateKey = cipher.loadPrivateKeyFromFile(file.getPath());
+    assertThat(privateKey.getAlgorithm(), is("RSA"));
+  }
+
+  @Test
+  public void toPublicKey() throws Exception {
+    RsaCipher cipher = new RsaCipher(new Settings());
+    PublicKey publicKey = cipher.toPublicKey(loadPublicKey());
+    assertThat(publicKey.getAlgorithm(), is("RSA"));
+  }
+
+  private String loadPublicKey() throws IOException {
+    InputStream input = getClass().getResourceAsStream("/org/sonar/api/config/RsaCipherTest/rsa_public_key.txt");
+    try {
+      return IOUtils.toString(input);
+    } finally {
+      IOUtils.closeQuietly(input);
+    }
+  }
+}
diff --git a/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt b/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt
new file mode 100644 (file)
index 0000000..10be545
--- /dev/null
@@ -0,0 +1 @@
+90219154459460484635307049294251309350624400174513872842964935995590426792849850754956692979878580134173903984923579664828287537160023584656524734039928278121145700539672753499137027823143447317638477535928797385199093031615075304372662494208460458746505946857452591645907526128623572362647338861106567346733,28487650981645105345729039749992166191644740180529930949117542540744133726768616377360480408404524731015987443184896433830608393640073974409602558039310242973125348929164889362700042142142217737063061860660679199646988663544428331146992861271726257946205621825278746735752228856292372558114153659087908459073
\ No newline at end of file
diff --git a/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_public_key.txt b/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_public_key.txt
new file mode 100644 (file)
index 0000000..ef74600
--- /dev/null
@@ -0,0 +1 @@
+90219154459460484635307049294251309350624400174513872842964935995590426792849850754956692979878580134173903984923579664828287537160023584656524734039928278121145700539672753499137027823143447317638477535928797385199093031615075304372662494208460458746505946857452591645907526128623572362647338861106567346733,65537
\ No newline at end of file