Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Update:
- feat: add `pollTransaction` method to `SorobanServer` to poll transaction status with retry strategy. ([#696](https://github.com/stellar/java-stellar-sdk/pull/696))
- feat: implement message signing and verification according to [SEP-53](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md), check `KeyPair.signMessage` and `KeyPair.verifyMessage` for more details. ([#698](https://github.com/stellar/java-stellar-sdk/pull/698))

## 1.5.0

Expand Down
69 changes: 69 additions & 0 deletions src/main/java/org/stellar/sdk/KeyPair.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.stellar.sdk;

import static java.lang.System.arraycopy;
import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
Expand Down Expand Up @@ -354,4 +355,72 @@ public int hashCode() {
Arrays.hashCode(publicKey.getEncoded()),
privateKey == null ? null : Arrays.hashCode(privateKey.getEncoded()));
}

/**
* Calculate the hash of a message according to <a
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
* target="_blank">SEP-53</a>.
*
* @param message The message to hash
* @return The SHA-256 hash of the prefixed message.
*/
private static byte[] calculateMessageHash(byte[] message) {
final byte[] messagePrefix = "Stellar Signed Message:\n".getBytes(UTF_8);
byte[] signedMessageBase = new byte[messagePrefix.length + message.length];
System.arraycopy(messagePrefix, 0, signedMessageBase, 0, messagePrefix.length);
System.arraycopy(message, 0, signedMessageBase, messagePrefix.length, message.length);
return Util.hash(signedMessageBase);
}

/**
* Sign a message according to <a
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
* target="_blank">SEP-53</a>.
*
* @param message The message to sign.
* @return The signature bytes.
*/
public byte[] signMessage(String message) {
return signMessage(message.getBytes(UTF_8));
}

/**
* Sign a message according to <a
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
* target="_blank">SEP-53</a>.
*
* @param message The message to sign.
* @return The signature bytes.
*/
public byte[] signMessage(byte[] message) {
byte[] messageHash = calculateMessageHash(message);
return sign(messageHash);
}

/**
* Verify a <a
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
* target="_blank">SEP-53</a> signed message.
*
* @param message The original message.
* @param signature The signature to verify.
* @return True if the signature is valid for the given message, false otherwise.
*/
public boolean verifyMessage(byte[] message, byte[] signature) {
byte[] messageHash = calculateMessageHash(message);
return verify(messageHash, signature);
}

/**
* Verify a <a
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md"
* target="_blank">SEP-53</a> signed message.
*
* @param message The original message.
* @param signature The signature to verify.
* @return True if the signature is valid for the given message, false otherwise.
*/
public boolean verifyMessage(String message, byte[] signature) {
return verifyMessage(message.getBytes(UTF_8), signature);
}
}
45 changes: 45 additions & 0 deletions src/test/java/org/stellar/sdk/KeyPairTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.junit.Assert.fail;

import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -212,4 +213,48 @@ public void testFromSecretSeedThrowsWithBytes() {
assertThrows(
IllegalArgumentException.class, () -> KeyPair.fromSecretSeed(Util.hexToBytes("00")));
}

@Test
public void testSignAndVerifyMessage() {
KeyPair kp = KeyPair.fromSecretSeed("SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW");

String inputEnglish = "Hello, World!";
byte[] expectedEnglishSig =
Base64.getDecoder()
.decode(
"fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==");
String inputJapanese = "こんにちは、世界!";
byte[] expectedJapaneseSig =
Base64.getDecoder()
.decode(
"CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA==");
byte[] inputBytes = Base64.getDecoder().decode("2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=");
byte[] expectedBytesSig =
Base64.getDecoder()
.decode(
"VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ==");

assertArrayEquals(expectedEnglishSig, kp.signMessage(inputEnglish));
assertArrayEquals(expectedJapaneseSig, kp.signMessage(inputJapanese));
assertArrayEquals(expectedBytesSig, kp.signMessage(inputBytes));

assertTrue(kp.verifyMessage(inputEnglish, expectedEnglishSig));
assertTrue(kp.verifyMessage(inputJapanese, expectedJapaneseSig));
assertTrue(kp.verifyMessage(inputBytes, expectedBytesSig));
}

@Test
public void testVerifyMessageWithInvalidSignature() {
KeyPair kp = KeyPair.fromAccountId("GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L");
String message = "Hello, World!";
String[] invalidSigs =
new String[] {
"VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ==",
"MTI="
};
for (String sigB64 : invalidSigs) {
byte[] sig = Base64.getDecoder().decode(sigB64);
assertFalse(kp.verifyMessage(message, sig));
}
}
}