mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-23 04:36:50 +00:00
Refactor to use BouncyCastle Ed25519/X25519, and more...
Remove old whispersystems, etc. *25519 and use new v1.61 BouncyCastle. Fix proxy forging private key derivation from X25519 shared secret. Also include Javascript test version for comparison. Fix block rewards for proxy forging. Add extra useful info to API call GET /admin/forgingaccounts. Fix API response to POST/DELETE /admin/forgingaccounts when passed invalid private keys. Added block rewards and account flags to testchain config. Tests to cover changes above.
This commit is contained in:
@@ -1,12 +1,23 @@
|
||||
package org.qora.test;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.crypto.Crypto;
|
||||
import org.qora.test.common.Common;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
|
||||
import org.bitcoinj.core.Base58;
|
||||
import org.bouncycastle.crypto.agreement.X25519Agreement;
|
||||
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
|
||||
public class CryptoTests extends Common {
|
||||
@@ -37,4 +48,74 @@ public class CryptoTests extends Common {
|
||||
assertEquals(expected, Crypto.toAddress(publicKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifySignature() {
|
||||
final String privateKey58 = "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6";
|
||||
final String message58 = "111FDmMy7u7ChH3SNLNYoUqE9eQRDVKGzhYTAU7XJRVZ7L966aKdDFBeD5WBQP372Lgpdbt4L8HuPobB1CWbJzdUqa72MYVA8A8pmocQQpzRsC5Kreif94yiScTDnnvCWcNERj9J2sqTH12gVdeeLt9Ery7HZFi6tDyysTLBkWfmDjuLnSfDKc7xeqZFkMSG1oatPedzrsDtrBZ";
|
||||
final String expectedSignature58 = "41g1hidZGbNn8xCCH41j1V1tD9iUwz7LCF4UcH19eindYyBnjKxfHdPm9qyRvLYFmXp8PV8YXzMXWUUngmqHo5Ho";
|
||||
|
||||
final byte[] privateKey = Base58.decode(privateKey58);
|
||||
PrivateKeyAccount account = new PrivateKeyAccount(null, privateKey);
|
||||
|
||||
byte[] message = Base58.decode(message58);
|
||||
byte[] signature = account.sign(message);
|
||||
assertEquals(expectedSignature58, Base58.encode(signature));
|
||||
|
||||
assertTrue(account.verify(signature, message));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBCseed() throws NoSuchAlgorithmException, NoSuchProviderException {
|
||||
final String privateKey58 = "A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6";
|
||||
final String publicKey58 = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP";
|
||||
|
||||
final byte[] privateKey = Base58.decode(privateKey58);
|
||||
PrivateKeyAccount account = new PrivateKeyAccount(null, privateKey);
|
||||
|
||||
String expected58 = publicKey58;
|
||||
String actual58 = Base58.encode(account.getPublicKey());
|
||||
assertEquals("qora-core generated public key incorrect", expected58, actual58);
|
||||
|
||||
Ed25519PrivateKeyParameters privateKeyParams = new Ed25519PrivateKeyParameters(privateKey, 0);
|
||||
Ed25519PublicKeyParameters publicKeyParams = privateKeyParams.generatePublicKey();
|
||||
|
||||
actual58 = Base58.encode(publicKeyParams.getEncoded());
|
||||
assertEquals("BouncyCastle generated public key incorrect", expected58, actual58);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBCSharedSecret() throws NoSuchAlgorithmException, NoSuchProviderException {
|
||||
final byte[] ourPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
|
||||
final byte[] theirPublicKey = Base58.decode("2sbcMmVKke5inS4yrbeoG6Cyw2mZCptQNjyWgnY4YHaF");
|
||||
final String expectedProxyPrivateKey = "EZhKy6wEh1ncQsvx6x3yV2sqjjsoU1bTTqrMcFLjLmp4";
|
||||
|
||||
X25519PrivateKeyParameters ourPrivateKeyParams = new X25519PrivateKeyParameters(ourPrivateKey, 0);
|
||||
X25519PublicKeyParameters theirPublicKeyParams = new X25519PublicKeyParameters(theirPublicKey, 0);
|
||||
|
||||
byte[] sharedSecret = new byte[32];
|
||||
|
||||
X25519Agreement keyAgree = new X25519Agreement();
|
||||
keyAgree.init(ourPrivateKeyParams);
|
||||
keyAgree.calculateAgreement(theirPublicKeyParams, sharedSecret, 0);
|
||||
|
||||
String proxyPrivateKey = Base58.encode(Crypto.digest(sharedSecret));
|
||||
|
||||
assertEquals("proxy private key incorrect", expectedProxyPrivateKey, proxyPrivateKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSharedSecret() throws NoSuchAlgorithmException, NoSuchProviderException {
|
||||
final byte[] ourPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6");
|
||||
final byte[] theirPublicKey = Base58.decode("2sbcMmVKke5inS4yrbeoG6Cyw2mZCptQNjyWgnY4YHaF");
|
||||
final String expectedProxyPrivateKey = "EZhKy6wEh1ncQsvx6x3yV2sqjjsoU1bTTqrMcFLjLmp4";
|
||||
|
||||
PrivateKeyAccount generator = new PrivateKeyAccount(null, ourPrivateKey);
|
||||
|
||||
byte[] sharedSecret = generator.getSharedSecret(theirPublicKey);
|
||||
|
||||
String proxyPrivateKey = Base58.encode(Crypto.digest(sharedSecret));
|
||||
|
||||
assertEquals("proxy private key incorrect", expectedProxyPrivateKey, proxyPrivateKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
77
src/test/java/org/qora/test/forging/RewardTests.java
Normal file
77
src/test/java/org/qora/test/forging/RewardTests.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package org.qora.test.forging;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.asset.Asset;
|
||||
import org.qora.block.BlockChain;
|
||||
import org.qora.block.BlockChain.RewardsByHeight;
|
||||
import org.qora.block.BlockGenerator;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.repository.Repository;
|
||||
import org.qora.repository.RepositoryManager;
|
||||
import org.qora.test.common.AccountUtils;
|
||||
import org.qora.test.common.Common;
|
||||
|
||||
public class RewardTests extends Common {
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() throws DataException {
|
||||
Common.orphanCheck();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleReward() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORA);
|
||||
|
||||
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
|
||||
|
||||
BigDecimal firstReward = BlockChain.getInstance().getBlockRewardsByHeight().get(0).reward;
|
||||
|
||||
BlockGenerator.generateTestingBlock(repository, forgingAccount);
|
||||
|
||||
BigDecimal expectedBalance = initialBalances.get("alice").get(Asset.QORA).add(firstReward);
|
||||
AccountUtils.assertBalance(repository, "alice", Asset.QORA, expectedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRewards() throws DataException {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORA);
|
||||
|
||||
PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
|
||||
|
||||
List<RewardsByHeight> rewards = BlockChain.getInstance().getBlockRewardsByHeight();
|
||||
|
||||
int rewardIndex = rewards.size() - 1;
|
||||
|
||||
RewardsByHeight rewardInfo = rewards.get(rewardIndex);
|
||||
BigDecimal expectedBalance = initialBalances.get("alice").get(Asset.QORA);
|
||||
|
||||
for (int height = rewardInfo.height; height > 1; --height) {
|
||||
if (height < rewardInfo.height) {
|
||||
--rewardIndex;
|
||||
rewardInfo = rewards.get(rewardIndex);
|
||||
}
|
||||
|
||||
BlockGenerator.generateTestingBlock(repository, forgingAccount);
|
||||
expectedBalance = expectedBalance.add(rewardInfo.reward);
|
||||
}
|
||||
|
||||
AccountUtils.assertBalance(repository, "alice", Asset.QORA, expectedBalance);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
100
src/test/resources/Base58.js
Normal file
100
src/test/resources/Base58.js
Normal file
@@ -0,0 +1,100 @@
|
||||
// Generated by CoffeeScript 1.8.0
|
||||
(function() {
|
||||
var ALPHABET, ALPHABET_MAP, Base58, i;
|
||||
|
||||
Base58 = (typeof module !== "undefined" && module !== null ? module.exports : void 0) || (window.Base58 = {});
|
||||
|
||||
ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
ALPHABET_MAP = {};
|
||||
|
||||
i = 0;
|
||||
|
||||
while (i < ALPHABET.length) {
|
||||
ALPHABET_MAP[ALPHABET.charAt(i)] = i;
|
||||
i++;
|
||||
}
|
||||
|
||||
Base58.encode = function(buffer) {
|
||||
var carry, digits, j;
|
||||
if (buffer.length === 0) {
|
||||
return "";
|
||||
}
|
||||
i = void 0;
|
||||
j = void 0;
|
||||
digits = [0];
|
||||
i = 0;
|
||||
while (i < buffer.length) {
|
||||
j = 0;
|
||||
while (j < digits.length) {
|
||||
digits[j] <<= 8;
|
||||
j++;
|
||||
}
|
||||
digits[0] += buffer[i];
|
||||
carry = 0;
|
||||
j = 0;
|
||||
while (j < digits.length) {
|
||||
digits[j] += carry;
|
||||
carry = (digits[j] / 58) | 0;
|
||||
digits[j] %= 58;
|
||||
++j;
|
||||
}
|
||||
while (carry) {
|
||||
digits.push(carry % 58);
|
||||
carry = (carry / 58) | 0;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
i = 0;
|
||||
while (buffer[i] === 0 && i < buffer.length - 1) {
|
||||
digits.push(0);
|
||||
i++;
|
||||
}
|
||||
return digits.reverse().map(function(digit) {
|
||||
return ALPHABET[digit];
|
||||
}).join("");
|
||||
};
|
||||
|
||||
Base58.decode = function(string) {
|
||||
var bytes, c, carry, j;
|
||||
if (string.length === 0) {
|
||||
return new (typeof Uint8Array !== "undefined" && Uint8Array !== null ? Uint8Array : Buffer)(0);
|
||||
}
|
||||
i = void 0;
|
||||
j = void 0;
|
||||
bytes = [0];
|
||||
i = 0;
|
||||
while (i < string.length) {
|
||||
c = string[i];
|
||||
if (!(c in ALPHABET_MAP)) {
|
||||
throw "Base58.decode received unacceptable input. Character '" + c + "' is not in the Base58 alphabet.";
|
||||
}
|
||||
j = 0;
|
||||
while (j < bytes.length) {
|
||||
bytes[j] *= 58;
|
||||
j++;
|
||||
}
|
||||
bytes[0] += ALPHABET_MAP[c];
|
||||
carry = 0;
|
||||
j = 0;
|
||||
while (j < bytes.length) {
|
||||
bytes[j] += carry;
|
||||
carry = bytes[j] >> 8;
|
||||
bytes[j] &= 0xff;
|
||||
++j;
|
||||
}
|
||||
while (carry) {
|
||||
bytes.push(carry & 0xff);
|
||||
carry >>= 8;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
i = 0;
|
||||
while (string[i] === "1" && i < string.length - 1) {
|
||||
bytes.push(0);
|
||||
i++;
|
||||
}
|
||||
return new (typeof Uint8Array !== "undefined" && Uint8Array !== null ? Uint8Array : Buffer)(bytes.reverse());
|
||||
};
|
||||
|
||||
}).call(this);
|
642
src/test/resources/nacl_factory.js
Normal file
642
src/test/resources/nacl_factory.js
Normal file
File diff suppressed because one or more lines are too long
37
src/test/resources/shared-secret.html
Normal file
37
src/test/resources/shared-secret.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="Base58.js"></script>
|
||||
<script src="nacl_factory.js"></script>
|
||||
<script>
|
||||
nacl_factory.instantiate(function (nacl) {
|
||||
var mintingAccountPrk = 'A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6';
|
||||
// var recipientAccountPuk = 'C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry';
|
||||
var recipientAccountPuk = '2sbcMmVKke5inS4yrbeoG6Cyw2mZCptQNjyWgnY4YHaF';
|
||||
|
||||
mintingAccountPrk = Base58.decode(mintingAccountPrk);
|
||||
recipientAccountPuk = Base58.decode(recipientAccountPuk);
|
||||
|
||||
var mintingKeyPair = nacl.crypto_box_keypair_from_raw_sk(mintingAccountPrk);
|
||||
|
||||
// Expecting: A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6
|
||||
console.log("minting private key (for confirmation): " + Base58.encode(mintingKeyPair.boxSk));
|
||||
|
||||
// This WILL NOT be: 2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP because this is an X25519 keypair, not Ed25519 keypair?
|
||||
// console.log("minting PUBLIC key: " + Base58.encode(mintingKeyPair.boxPk));
|
||||
|
||||
var sharedSecret = nacl.crypto_scalarmult(mintingKeyPair.boxSk, recipientAccountPuk);
|
||||
console.log("shared secret (for debugging): " + Base58.encode(sharedSecret));
|
||||
console.log(sharedSecret); // log as Uint8Array
|
||||
|
||||
var proxyPrivateKey = nacl.crypto_hash_sha256(sharedSecret)
|
||||
console.log("proxy private key: " + Base58.encode(proxyPrivateKey));
|
||||
|
||||
var proxyKeyPair = nacl.crypto_sign_seed_keypair(proxyPrivateKey);
|
||||
console.log("proxy public key: " + Base58.encode(proxyKeyPair.signPk));
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
@@ -21,9 +21,15 @@
|
||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "TEST", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }
|
||||
{ "type": "ISSUE_ASSET", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 },
|
||||
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }
|
||||
]
|
||||
},
|
||||
"rewardsByHeight": [
|
||||
{ "height": 1, "reward": 100 },
|
||||
{ "height": 11, "reward": 10 },
|
||||
{ "height": 21, "reward": 1 }
|
||||
],
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
|
Reference in New Issue
Block a user