Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ public static String fromPrivateKey(ECKey privateKey) {
byte[] publicKeyBytes = privateKey.getPubKey();
return fromPublicKey(Hex.encode(publicKeyBytes));
}

public static boolean validate(String address) {
return address != null && address.matches("^0x[a-fA-F0-9]{40}$");
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.arkecosystem.crypto.identities;

import java.util.Arrays;
import org.arkecosystem.crypto.configuration.Network;
import org.arkecosystem.crypto.encoding.Base58;
import org.arkecosystem.crypto.encoding.Hex;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
Expand All @@ -14,4 +17,22 @@ public static ECKey fromPassphrase(String passphrase) {
public static ECKey fromHex(String privateKey) {
return ECKey.fromPrivate(Hex.decode(privateKey), true);
}

public static ECKey fromWif(String wif) {
byte[] decoded = Base58.decodeChecked(wif);

if (decoded.length < 33) {
throw new IllegalArgumentException("Invalid WIF: payload too short.");
}

int expectedVersion = Network.get().wif() & 0xff;
if ((decoded[0] & 0xff) != expectedVersion) {
throw new IllegalArgumentException(
"Invalid WIF: version byte does not match the active network.");
}

byte[] privateKeyBytes = Arrays.copyOfRange(decoded, 1, 33);

return ECKey.fromPrivate(privateKeyBytes, true);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package org.arkecosystem.crypto.identities;

import org.arkecosystem.crypto.encoding.Hex;
import org.bitcoinj.core.ECKey;

public class PublicKey {
public static String fromPassphrase(String passphrase) {
return PrivateKey.fromPassphrase(passphrase).getPublicKeyAsHex();
}

public static ECKey fromHex(String publicKey) {
ECKey key = ECKey.fromPublicOnly(Hex.decode(publicKey));
return ECKey.fromPublicOnly(key.getPubKeyPoint().getEncoded(true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ public TBuilder sign(String passphrase) {
return this.instance();
}

public TBuilder legacySecondSign(String passphrase, String secondPassphrase) {
this.transaction.sign(passphrase);
this.transaction.legacySecondSign(secondPassphrase);
this.transaction.computeId();
return this.instance();
}

public boolean verify() {
return this.transaction.verify();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public abstract class AbstractTransaction {
public List<String> multipaymentRecipients;
public List<BigInteger> multipaymentAmounts;
public String username;
public String legacySecondSignature;

public AbstractTransaction() {}

Expand Down Expand Up @@ -105,11 +106,23 @@ public byte[] hash(boolean skipSignature) {
}

public AbstractTransaction sign(String passphrase) {
byte[] hash = this.hash(true);

ECKey privateKey = PrivateKey.fromPassphrase(passphrase);
this.senderPublicKey = privateKey.getPublicKeyAsHex();

this.signature = signHash(this.hash(true), privateKey);

return this;
}

public AbstractTransaction legacySecondSign(String secondPassphrase) {
ECKey privateKey = PrivateKey.fromPassphrase(secondPassphrase);

this.legacySecondSignature = signHash(this.hash(true), privateKey);

return this;
}

private static String signHash(byte[] hash, ECKey privateKey) {
ECKey.ECDSASignature signature = privateKey.sign(Sha256Hash.wrap(hash));

int recId = -1;
Expand All @@ -135,9 +148,7 @@ public AbstractTransaction sign(String passphrase) {
System.arraycopy(signatureBytes, 0, signatureWithRecId, 0, 64);
signatureWithRecId[64] = (byte) recId;

this.signature = Hex.encode(signatureWithRecId);

return this;
return Hex.encode(signatureWithRecId);
}

private static byte[] bigIntegerToBytes(BigInteger b, int numBytes) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.arkecosystem.crypto.identities;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.bitcoinj.core.ECKey;
import org.junit.jupiter.api.Test;
Expand All @@ -27,4 +29,35 @@ public void fromPrivateKey() {
String actual = Address.fromPrivateKey(privateKey);
assertEquals("0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01", actual);
}

@Test
public void validate_accepts_valid_checksum_address() {
assertTrue(Address.validate("0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01"));
}

@Test
public void validate_accepts_lowercase_address() {
assertTrue(Address.validate("0xb0ff9213f7226bbb72b84de16af86e56f1f38b01"));
}

@Test
public void validate_rejects_missing_prefix() {
assertFalse(Address.validate("b0FF9213f7226bBB72b84dE16af86e56f1f38B01"));
}

@Test
public void validate_rejects_wrong_length() {
assertFalse(Address.validate("0xb0FF9213f7226bBB72b84dE16af86e56f1f38B0"));
assertFalse(Address.validate("0xb0FF9213f7226bBB72b84dE16af86e56f1f38B011"));
}

@Test
public void validate_rejects_non_hex_characters() {
assertFalse(Address.validate("0xb0FF9213f7226bBB72b84dE16af86e56f1f38BZZ"));
}

@Test
public void validate_rejects_null() {
assertFalse(Address.validate(null));
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
package org.arkecosystem.crypto.identities;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.IOException;
import org.arkecosystem.crypto.configuration.Network;
import org.arkecosystem.crypto.networks.Devnet;
import org.arkecosystem.crypto.networks.Mainnet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class PrivateKeyTest {

@BeforeEach
void setUp() {
Network.set(new Devnet());
}

@AfterEach
void tearDown() {
Network.set(new Devnet());
}

@Test
public void fromPassphrase() {
String actual =
Expand All @@ -21,4 +38,35 @@ public void fromHex() {
.getPrivateKeyAsHex();
assertEquals("d8839c2432bfd0a67ef10a804ba991eabba19f154a3d707917681d45822a5712", actual);
}

@Test
public void fromWif_round_trips_with_fromPassphrase() throws IOException {
String wif = WIF.fromPassphrase("this is a top secret passphrase");

String fromWif = PrivateKey.fromWif(wif).getPrivateKeyAsHex();
String fromPassphrase =
PrivateKey.fromPassphrase("this is a top secret passphrase").getPrivateKeyAsHex();

assertEquals(fromPassphrase, fromWif);
}

@Test
public void fromWif_rejects_when_version_byte_belongs_to_another_network() throws IOException {
String mainnetWif;
try {
Network.set(new Mainnet());
mainnetWif = WIF.fromPassphrase("this is a top secret passphrase");
} finally {
Network.set(new Devnet());
}

// Mainnet and Devnet share the same wif byte (170) so this branch only
// triggers when the prefix differs, e.g. against testnet (186).
Network.set(new org.arkecosystem.crypto.networks.Testnet());
try {
assertThrows(IllegalArgumentException.class, () -> PrivateKey.fromWif(mainnetWif));
} finally {
Network.set(new Devnet());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,23 @@ public void fromPassphrase() {
}

@Test
public void fromHex() {
String actual =
PrivateKey.fromHex(
"d8839c2432bfd0a67ef10a804ba991eabba19f154a3d707917681d45822a5712")
.getPrivateKeyAsHex();
Assertions.assertEquals(
"d8839c2432bfd0a67ef10a804ba991eabba19f154a3d707917681d45822a5712", actual);
public void fromHex_round_trips_to_same_compressed_hex() {
String hex = "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192";

String actual = PublicKey.fromHex(hex).getPublicKeyAsHex();

Assertions.assertEquals(hex, actual);
}

@Test
public void fromHex_accepts_uncompressed_public_key() {
String compressed = "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192";
String uncompressed =
org.arkecosystem.crypto.encoding.Hex.encode(
PublicKey.fromHex(compressed).getPubKeyPoint().getEncoded(false));

String actual = PublicKey.fromHex(uncompressed).getPublicKeyAsHex();

Assertions.assertEquals(compressed, actual);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,37 @@ public void it_should_sign_it_with_a_passphrase() throws Exception {
assertEquals(data.get("id"), builder.transaction.getId());
assertTrue(builder.verify());
}

@Test
public void it_should_attach_a_legacy_second_signature() {
TransferBuilder builder =
new TransferBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(21000)
.nonce(1L)
.recipientAddress("0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A")
.value("100000000")
.legacySecondSign(this.passphrase, "second secret passphrase");

assertNotNull(builder.transaction.signature);
assertNotNull(builder.transaction.legacySecondSignature);
assertEquals(130, builder.transaction.legacySecondSignature.length());
assertNotEquals(builder.transaction.signature, builder.transaction.legacySecondSignature);
assertTrue(builder.verify());
}

@Test
public void legacy_second_signature_is_not_set_by_a_regular_sign() {
TransferBuilder builder =
new TransferBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(21000)
.nonce(1L)
.recipientAddress("0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A")
.value("100000000")
.sign(this.passphrase);

assertNotNull(builder.transaction.signature);
assertNull(builder.transaction.legacySecondSignature);
}
}
Loading