├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── debuggor │ └── schnorrkel │ ├── merlin │ ├── Keccak.java │ ├── Strobe128.java │ ├── Transcript.java │ ├── TranscriptRng.java │ └── TranscriptRngBuilder.java │ ├── sign │ ├── ExpansionMode.java │ ├── KeyPair.java │ ├── PrivateKey.java │ ├── PublicKey.java │ ├── Signature.java │ ├── SigningContext.java │ └── SigningTranscript.java │ └── utils │ ├── HexUtils.java │ ├── NumberUtils.java │ └── ScalarUtils.java └── test └── java └── com └── debuggor └── schnorrkel └── sign ├── KeyTest.java └── SignTest.java /README.md: -------------------------------------------------------------------------------- 1 | 2 | # schnorrkel-java 3 | 4 | Java implementation of the sr25519 signature algorithm (schnorr over ristretto25519). The existing rust implementation is [here](https://github.com/w3f/schnorrkel). 5 | 6 | This library is currently able to create sr25519 keys, import sr25519 keys, and sign and verify messages. 7 | 8 | 9 | ### usage 10 | 11 | Example: signing and verification 12 | 13 | ```java 14 | import com.debuggor.schnorrkel.utils.HexUtils; 15 | 16 | public class SignTest { 17 | 18 | public static void main(String[] args) throws Exception { 19 | KeyPair keyPair = KeyPair.fromSecretSeed(HexUtils.hexToBytes("579d7aa286b37b800b95fe41adabbf0c2a577caf2854baeca98f8fb242ff43ae"), ExpansionMode.Ed25519); 20 | 21 | byte[] message = "test message".getBytes(); 22 | SigningContext ctx = SigningContext.createSigningContext("good".getBytes()); 23 | SigningTranscript t = ctx.bytes(message); 24 | Signature signature = keyPair.sign(t); 25 | byte[] sign = signature.to_bytes(); 26 | System.out.println(HexUtils.bytesToHex(sign)); 27 | 28 | 29 | SigningContext ctx2 = SigningContext.createSigningContext("good".getBytes()); 30 | SigningTranscript t2 = ctx2.bytes(message); 31 | KeyPair fromPublicKey = KeyPair.fromPublicKey(keyPair.getPublicKey().toPublicKey()); 32 | boolean verify = fromPublicKey.verify(t2, sign); 33 | System.out.println(verify); 34 | } 35 | } 36 | ``` 37 | 38 | 39 | ### other 40 | 41 | [Go implementation](https://github.com/ChainSafe/go-schnorrkel) 42 | 43 | Thanks to [@str4d](https://github.com/str4d) and [@isislovecruft](https://github.com/isislovecruft) for the open source [Curve25519 library](https://github.com/cryptography-cafe/curve25519-elisabeth/) 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.debuggor 8 | schnorrkel-java 9 | 1.0.0-SNAPSHOT 10 | 11 | 12 | 13 | cafe.cryptography 14 | curve25519-elisabeth 15 | 0.1.0 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/merlin/Keccak.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.merlin; 2 | 3 | /** 4 | * https://github.com/jrmelsha/keccak/blob/master/src/main/java/com/joemelsha/crypto/hash/Keccak.java#L365 5 | */ 6 | public class Keccak { 7 | 8 | public static void f1600(long[] a) { 9 | int c, i; 10 | long x, a_10_; 11 | long x0, x1, x2, x3, x4; 12 | long t0, t1, t2, t3, t4; 13 | long c0, c1, c2, c3, c4; 14 | long[] rc = RC; 15 | 16 | i = 0; 17 | do { 18 | //theta (precalculation part) 19 | c0 = a[0] ^ a[5 + 0] ^ a[10 + 0] ^ a[15 + 0] ^ a[20 + 0]; 20 | c1 = a[1] ^ a[5 + 1] ^ a[10 + 1] ^ a[15 + 1] ^ a[20 + 1]; 21 | c2 = a[2] ^ a[5 + 2] ^ a[10 + 2] ^ a[15 + 2] ^ a[20 + 2]; 22 | c3 = a[3] ^ a[5 + 3] ^ a[10 + 3] ^ a[15 + 3] ^ a[20 + 3]; 23 | c4 = a[4] ^ a[5 + 4] ^ a[10 + 4] ^ a[15 + 4] ^ a[20 + 4]; 24 | 25 | t0 = (c0 << 1) ^ (c0 >>> (64 - 1)) ^ c3; 26 | t1 = (c1 << 1) ^ (c1 >>> (64 - 1)) ^ c4; 27 | t2 = (c2 << 1) ^ (c2 >>> (64 - 1)) ^ c0; 28 | t3 = (c3 << 1) ^ (c3 >>> (64 - 1)) ^ c1; 29 | t4 = (c4 << 1) ^ (c4 >>> (64 - 1)) ^ c2; 30 | 31 | //theta (xorring part) + rho + pi 32 | a[0] ^= t1; 33 | x = a[1] ^ t2; 34 | a_10_ = (x << 1) | (x >>> (64 - 1)); 35 | x = a[6] ^ t2; 36 | a[1] = (x << 44) | (x >>> (64 - 44)); 37 | x = a[9] ^ t0; 38 | a[6] = (x << 20) | (x >>> (64 - 20)); 39 | x = a[22] ^ t3; 40 | a[9] = (x << 61) | (x >>> (64 - 61)); 41 | 42 | x = a[14] ^ t0; 43 | a[22] = (x << 39) | (x >>> (64 - 39)); 44 | x = a[20] ^ t1; 45 | a[14] = (x << 18) | (x >>> (64 - 18)); 46 | x = a[2] ^ t3; 47 | a[20] = (x << 62) | (x >>> (64 - 62)); 48 | x = a[12] ^ t3; 49 | a[2] = (x << 43) | (x >>> (64 - 43)); 50 | x = a[13] ^ t4; 51 | a[12] = (x << 25) | (x >>> (64 - 25)); 52 | 53 | x = a[19] ^ t0; 54 | a[13] = (x << 8) | (x >>> (64 - 8)); 55 | x = a[23] ^ t4; 56 | a[19] = (x << 56) | (x >>> (64 - 56)); 57 | x = a[15] ^ t1; 58 | a[23] = (x << 41) | (x >>> (64 - 41)); 59 | x = a[4] ^ t0; 60 | a[15] = (x << 27) | (x >>> (64 - 27)); 61 | x = a[24] ^ t0; 62 | a[4] = (x << 14) | (x >>> (64 - 14)); 63 | 64 | x = a[21] ^ t2; 65 | a[24] = (x << 2) | (x >>> (64 - 2)); 66 | x = a[8] ^ t4; 67 | a[21] = (x << 55) | (x >>> (64 - 55)); 68 | x = a[16] ^ t2; 69 | a[8] = (x << 45) | (x >>> (64 - 45)); 70 | x = a[5] ^ t1; 71 | a[16] = (x << 36) | (x >>> (64 - 36)); 72 | x = a[3] ^ t4; 73 | a[5] = (x << 28) | (x >>> (64 - 28)); 74 | 75 | x = a[18] ^ t4; 76 | a[3] = (x << 21) | (x >>> (64 - 21)); 77 | x = a[17] ^ t3; 78 | a[18] = (x << 15) | (x >>> (64 - 15)); 79 | x = a[11] ^ t2; 80 | a[17] = (x << 10) | (x >>> (64 - 10)); 81 | x = a[7] ^ t3; 82 | a[11] = (x << 6) | (x >>> (64 - 6)); 83 | x = a[10] ^ t1; 84 | a[7] = (x << 3) | (x >>> (64 - 3)); 85 | a[10] = a_10_; 86 | //chi 87 | c = 0; 88 | do { 89 | x0 = a[c + 0]; 90 | x1 = a[c + 1]; 91 | x2 = a[c + 2]; 92 | x3 = a[c + 3]; 93 | x4 = a[c + 4]; 94 | a[c + 0] = x0 ^ ((~x1) & x2); 95 | a[c + 1] = x1 ^ ((~x2) & x3); 96 | a[c + 2] = x2 ^ ((~x3) & x4); 97 | a[c + 3] = x3 ^ ((~x4) & x0); 98 | a[c + 4] = x4 ^ ((~x0) & x1); 99 | c += 5; 100 | } while (c < 25); 101 | //iota 102 | a[0] ^= rc[i]; 103 | i++; 104 | } while (i < 24); 105 | } 106 | 107 | private static final long[] RC = { 108 | 0x0000000000000001L, 0x0000000000008082L, 0x800000000000808AL, 0x8000000080008000L, 0x000000000000808BL, 109 | 0x0000000080000001L, 0x8000000080008081L, 0x8000000000008009L, 0x000000000000008AL, 0x0000000000000088L, 110 | 0x0000000080008009L, 0x000000008000000AL, 0x000000008000808BL, 0x800000000000008BL, 0x8000000000008089L, 111 | 0x8000000000008003L, 0x8000000000008002L, 0x8000000000000080L, 0x000000000000800AL, 0x800000008000000AL, 112 | 0x8000000080008081L, 0x8000000000008080L, 0x0000000080000001L, 0x8000000080008008L}; 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/merlin/Strobe128.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.merlin; 2 | 3 | import com.debuggor.schnorrkel.utils.NumberUtils; 4 | 5 | /** 6 | * @Author:yong.huang 7 | * @Date:2020-07-31 17:23 8 | */ 9 | public class Strobe128 implements Cloneable { 10 | 11 | private byte[] state; 12 | private int pos; 13 | private int pos_begin; 14 | private int cur_flags; 15 | 16 | private static int STROBE_R = 166; 17 | 18 | private static int FLAG_I = 1; 19 | private static int FLAG_A = 1 << 1; 20 | private static int FLAG_C = 1 << 2; 21 | private static int FLAG_T = 1 << 3; 22 | private static int FLAG_M = 1 << 4; 23 | private static int FLAG_K = 1 << 5; 24 | 25 | public Strobe128(byte[] state) { 26 | this.state = state; 27 | this.pos = 0; 28 | this.pos_begin = 0; 29 | this.cur_flags = 0; 30 | } 31 | 32 | public static Strobe128 createStrobe(byte[] protocol) throws Exception { 33 | byte[] state = initial_state(128); 34 | Strobe128 strobe = new Strobe128(state); 35 | strobe.meta_ad(protocol, false); 36 | return strobe; 37 | } 38 | 39 | public void meta_ad(byte[] data, boolean more) throws Exception { 40 | this.begin_op(FLAG_M | FLAG_A, more); 41 | this.absorb(data); 42 | } 43 | 44 | public void ad(byte[] data, boolean more) throws Exception { 45 | this.begin_op(FLAG_A, more); 46 | this.absorb(data); 47 | } 48 | 49 | public void prf(byte[] data, boolean more) throws Exception { 50 | this.begin_op(FLAG_I | FLAG_A | FLAG_C, more); 51 | this.squeeze(data); 52 | } 53 | 54 | public void key(byte[] data, boolean more) throws Exception { 55 | this.begin_op(FLAG_A | FLAG_C, more); 56 | this.overwrite(data); 57 | } 58 | 59 | 60 | private void begin_op(int flags, boolean more) throws Exception { 61 | // Check if we're continuing an operation 62 | if (more) { 63 | if (this.cur_flags != flags) { 64 | throw new Exception("You tried to continue op " + this.cur_flags + " but changed flags to " + flags); 65 | } 66 | return; 67 | } 68 | 69 | // Skip adjusting direction information (we just use AD, PRF) 70 | if ((flags & FLAG_T) != 0) { 71 | throw new Exception("You used the T flag, which this implementation doesn't support"); 72 | } 73 | 74 | int old_begin = this.pos_begin; 75 | this.pos_begin = this.pos + 1; 76 | this.cur_flags = flags; 77 | 78 | byte[] tmp = {(byte) old_begin, (byte) flags}; 79 | this.absorb(tmp); 80 | 81 | // Force running F if C or K is set 82 | boolean force_f = 0 != (flags & (FLAG_C | FLAG_K)); 83 | 84 | if (force_f && this.pos != 0) { 85 | this.run_f(); 86 | } 87 | } 88 | 89 | private void absorb(byte[] data) { 90 | for (int i = 0; i < data.length; i++) { 91 | this.state[this.pos] ^= data[i]; 92 | this.pos += 1; 93 | if (this.pos == STROBE_R) { 94 | this.run_f(); 95 | } 96 | } 97 | 98 | } 99 | 100 | private void run_f() { 101 | this.state[this.pos] ^= this.pos_begin; 102 | this.state[this.pos + 1] ^= 0x04; 103 | this.state[STROBE_R + 1] ^= 0x80; 104 | this.state = transmute_state(this.state, 200); 105 | this.pos = 0; 106 | this.pos_begin = 0; 107 | } 108 | 109 | private void overwrite(byte[] data) { 110 | for (int i = 0; i < data.length; i++) { 111 | this.state[this.pos] = data[i]; 112 | this.pos += 1; 113 | if (this.pos == STROBE_R) { 114 | this.run_f(); 115 | } 116 | } 117 | } 118 | 119 | private void squeeze(byte[] data) { 120 | for (int i = 0; i < data.length; i++) { 121 | data[i] = this.state[this.pos]; 122 | this.state[this.pos] = 0; 123 | this.pos += 1; 124 | if (this.pos == STROBE_R) { 125 | this.run_f(); 126 | } 127 | } 128 | } 129 | 130 | private static byte[] initial_state(int security) { 131 | if (security != 128 && security != 256) { 132 | throw new IllegalArgumentException("strobe: security must be set to either 128 or 256"); 133 | } 134 | byte[] st = new byte[200]; 135 | byte[] b1 = {1, (byte) (STROBE_R + 2), 1, 0, 1, 96}; 136 | byte[] b2 = "STROBEv1.0.2".getBytes(); 137 | System.arraycopy(b1, 0, st, 0, b1.length); 138 | System.arraycopy(b2, 0, st, b1.length, b2.length); 139 | 140 | int duplexRate = 1600 / 8 - security / 4; 141 | st = transmute_state(st, duplexRate); 142 | return st; 143 | } 144 | 145 | 146 | /** 147 | * runF: applies the STROBE's + cSHAKE's padding and the Keccak permutation 148 | * 149 | * @return 150 | */ 151 | private static byte[] transmute_state(byte[] buf, int duplexRate) { 152 | byte[] storage = new byte[duplexRate]; 153 | System.arraycopy(buf, 0, storage, 0, duplexRate); 154 | long[] state = NumberUtils.xorState(storage); 155 | Keccak.f1600(state); 156 | 157 | byte[] st = new byte[200]; 158 | int offset = 0; 159 | for (int i = 0; i < state.length; i++) { 160 | NumberUtils.int64ToBytes(state[i], st, offset); 161 | offset += 8; 162 | } 163 | return st; 164 | } 165 | 166 | @Override 167 | public Strobe128 clone() { 168 | Strobe128 strobe = new Strobe128(new byte[200]); 169 | strobe.cur_flags = this.cur_flags; 170 | strobe.pos = this.pos; 171 | strobe.pos_begin = this.pos_begin; 172 | System.arraycopy(this.state, 0, strobe.state, 0, this.state.length); 173 | return strobe; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/merlin/Transcript.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.merlin; 2 | 3 | import com.debuggor.schnorrkel.utils.NumberUtils; 4 | 5 | /** 6 | * @Author:yong.huang 7 | * @Date:2020-08-01 15:11 8 | */ 9 | public class Transcript { 10 | 11 | protected Strobe128 strobe; 12 | 13 | private static byte[] MERLIN_PROTOCOL_LABEL = "Merlin v1.0".getBytes(); 14 | 15 | public Transcript(Strobe128 strobe128) { 16 | this.strobe = strobe128; 17 | } 18 | 19 | public Strobe128 getStrobe() { 20 | return strobe; 21 | } 22 | 23 | public static Transcript createTranscript(byte[] label) throws Exception { 24 | Strobe128 strobe = Strobe128.createStrobe(MERLIN_PROTOCOL_LABEL); 25 | Transcript transcript = new Transcript(strobe); 26 | transcript.append_message("dom-sep".getBytes(), label); 27 | return transcript; 28 | } 29 | 30 | public void append_message(byte[] label, byte[] message) throws Exception { 31 | byte[] data_len = new byte[4]; 32 | NumberUtils.uint32ToBytes(message.length, data_len, 0); 33 | this.strobe.meta_ad(label, false); 34 | this.strobe.meta_ad(data_len, true); 35 | this.strobe.ad(message, false); 36 | } 37 | 38 | public void commit_bytes(byte[] label, byte[] message) throws Exception { 39 | this.append_message(label, message); 40 | } 41 | 42 | public void append_u64(byte[] label, long x) throws Exception { 43 | byte[] data = new byte[8]; 44 | NumberUtils.uint64ToBytes(x, data, 0); 45 | this.append_message(label, data); 46 | } 47 | 48 | public void commit_u64(byte[] label, long x) throws Exception { 49 | this.append_u64(label, x); 50 | } 51 | 52 | public void challenge_bytes(byte[] label, byte[] dest) throws Exception { 53 | byte[] data_len = new byte[4]; 54 | NumberUtils.uint32ToBytes(dest.length, data_len, 0); 55 | this.strobe.meta_ad(label, false); 56 | this.strobe.meta_ad(data_len, true); 57 | this.strobe.prf(dest, false); 58 | } 59 | 60 | public TranscriptRngBuilder build_rng() { 61 | Strobe128 strobe = this.strobe.clone(); 62 | return new TranscriptRngBuilder(strobe); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/merlin/TranscriptRng.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.merlin; 2 | 3 | import com.debuggor.schnorrkel.utils.NumberUtils; 4 | 5 | import java.security.SecureRandom; 6 | 7 | /** 8 | * @Author:yong.huang 9 | * @Date:2020-08-02 17:41 10 | */ 11 | public class TranscriptRng { 12 | 13 | private Strobe128 strobe; 14 | 15 | public TranscriptRng(Strobe128 strobe) { 16 | this.strobe = strobe; 17 | } 18 | 19 | public long next_u32() { 20 | return new SecureRandom().nextLong(); 21 | } 22 | 23 | public void fill_bytes(byte[] dest) throws Exception { 24 | byte[] dest_len = new byte[4]; 25 | NumberUtils.uint32ToBytes(dest.length, dest_len, 0); 26 | this.strobe.meta_ad(dest_len, false); 27 | this.strobe.prf(dest, false); 28 | } 29 | 30 | public void try_fill_bytes(byte[] dest) throws Exception { 31 | this.fill_bytes(dest); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/merlin/TranscriptRngBuilder.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.merlin; 2 | 3 | import com.debuggor.schnorrkel.utils.NumberUtils; 4 | 5 | import java.security.SecureRandom; 6 | 7 | /** 8 | * @Author:yong.huang 9 | * @Date:2020-08-02 17:32 10 | */ 11 | public class TranscriptRngBuilder { 12 | 13 | private Strobe128 strobe; 14 | 15 | public TranscriptRngBuilder(Strobe128 strobe) { 16 | this.strobe = strobe; 17 | } 18 | 19 | public TranscriptRngBuilder rekey_with_witness_bytes(byte[] label, byte[] witness) throws Exception { 20 | byte[] witness_len = new byte[4]; 21 | NumberUtils.uint32ToBytes(witness.length, witness_len, 0); 22 | this.strobe.meta_ad(label, false); 23 | this.strobe.meta_ad(witness_len, true); 24 | this.strobe.key(witness, false); 25 | return this; 26 | } 27 | 28 | public TranscriptRngBuilder commit_witness_bytes(byte[] label, byte[] witness) throws Exception { 29 | return this.rekey_with_witness_bytes(label, witness); 30 | } 31 | 32 | public TranscriptRng tFinalize() throws Exception { 33 | SecureRandom random = new SecureRandom(); 34 | byte[] random_bytes = new byte[32]; 35 | random.nextBytes(random_bytes); 36 | this.strobe.meta_ad("rng".getBytes(), false); 37 | this.strobe.key(random_bytes, false); 38 | return new TranscriptRng(this.strobe.clone()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/sign/ExpansionMode.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.sign; 2 | 3 | /** 4 | * @Author:yong.huang 5 | * @Date:2020-08-06 21:51 6 | */ 7 | public enum ExpansionMode { 8 | Uniform, 9 | Ed25519; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/sign/KeyPair.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.sign; 2 | 3 | import cafe.cryptography.curve25519.*; 4 | import com.debuggor.schnorrkel.utils.HexUtils; 5 | 6 | import java.security.SecureRandom; 7 | 8 | /** 9 | * @Author:yong.huang 10 | * @Date:2020-07-30 13:25 11 | */ 12 | public class KeyPair { 13 | 14 | private static final RistrettoGeneratorTable ristrettoTable = Constants.RISTRETTO_GENERATOR_TABLE; 15 | 16 | private static byte[] CTX = "substrate".getBytes(); 17 | 18 | private final PublicKey publicKey; 19 | private final PrivateKey privateKey; 20 | 21 | public KeyPair(PublicKey publicKey, PrivateKey privateKey) { 22 | this.publicKey = publicKey; 23 | this.privateKey = privateKey; 24 | } 25 | 26 | public PublicKey getPublicKey() { 27 | return publicKey; 28 | } 29 | 30 | public PrivateKey getPrivateKey() { 31 | return privateKey; 32 | } 33 | 34 | public static KeyPair fromSecretSeed(byte[] seed, ExpansionMode mode) { 35 | PrivateKey privateKey = new PrivateKey(seed, mode); 36 | byte[] key = privateKey.getKey(); 37 | RistrettoElement ristretto = ristrettoTable.multiply(Scalar.fromBits(key)); 38 | PublicKey publicKey = new PublicKey(ristretto); 39 | return new KeyPair(publicKey, privateKey); 40 | } 41 | 42 | public static KeyPair generateKeyPair() { 43 | SecureRandom random = new SecureRandom(); 44 | byte[] seed = new byte[32]; 45 | random.nextBytes(seed); 46 | return fromSecretSeed(seed, ExpansionMode.Ed25519); 47 | } 48 | 49 | public Signature sign(SigningTranscript t) throws Exception { 50 | t.proto_name("Schnorr-sig".getBytes()); 51 | t.commit_point("sign:pk".getBytes(), publicKey.getCompressedRistretto()); 52 | 53 | // context, message, A/public_key 54 | Scalar r = t.witness_scalar("signing".getBytes(), privateKey.getNonce()); 55 | CompressedRistretto R = ristrettoTable.multiply(r).compress(); 56 | t.commit_point("sign:R".getBytes(), R); 57 | 58 | // context, message, A/public_key, R=rG 59 | Scalar k = t.challenge_scalar("sign:c".getBytes()); 60 | 61 | Scalar key = Scalar.fromBits(privateKey.getKey()); 62 | Scalar s = k.multiplyAndAdd(key, r); 63 | r = Scalar.ZERO; 64 | return new Signature(R, s); 65 | } 66 | 67 | public static KeyPair fromPublicKey(byte[] pubkey) throws InvalidEncodingException { 68 | CompressedRistretto compressedRistretto = new CompressedRistretto(pubkey); 69 | RistrettoElement ristretto = compressedRistretto.decompress(); 70 | PublicKey publicKey = new PublicKey(ristretto); 71 | return new KeyPair(publicKey, null); 72 | } 73 | 74 | public boolean verify(SigningTranscript t, byte[] signature) throws Exception { 75 | Signature sign = Signature.from_bytes(signature); 76 | t.proto_name("Schnorr-sig".getBytes()); 77 | t.commit_point("sign:pk".getBytes(), publicKey.getCompressedRistretto()); 78 | t.commit_point("sign:R".getBytes(), sign.getR()); 79 | 80 | // context, message, A/public_key, R=rG 81 | Scalar k = t.challenge_scalar("sign:c".getBytes()); 82 | 83 | RistrettoElement publicPoint = publicKey.getRistretto(); 84 | RistrettoElement subtract = ristrettoTable.multiply(sign.getS()).subtract(publicPoint.multiply(k)); 85 | CompressedRistretto R = subtract.compress(); 86 | return R.equals(sign.getR()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/sign/PrivateKey.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.sign; 2 | 3 | import cafe.cryptography.curve25519.Scalar; 4 | import com.debuggor.schnorrkel.merlin.Transcript; 5 | import com.debuggor.schnorrkel.utils.ScalarUtils; 6 | 7 | import java.security.MessageDigest; 8 | import java.security.NoSuchAlgorithmException; 9 | import java.util.Arrays; 10 | 11 | /** 12 | * @Author:yong.huang 13 | * @Date:2020-07-30 13:23 14 | */ 15 | public class PrivateKey { 16 | 17 | private byte[] seed; 18 | private byte[] key; 19 | private byte[] nonce; 20 | private byte[] h; 21 | 22 | public PrivateKey(byte[] seed, ExpansionMode mode) { 23 | this.seed = seed; 24 | if (mode != null && mode.equals(ExpansionMode.Ed25519)) { 25 | expand_ed25519(seed); 26 | } else { 27 | expand_uniform(seed); 28 | } 29 | } 30 | 31 | 32 | private void expand_uniform(byte[] seed) { 33 | try { 34 | Transcript t = Transcript.createTranscript("ExpandSecretKeys".getBytes()); 35 | t.append_message("mini".getBytes(), seed); 36 | byte[] scalar_bytes = new byte[64]; 37 | t.challenge_bytes("sk".getBytes(), scalar_bytes); 38 | Scalar scalar = Scalar.fromBytesModOrderWide(scalar_bytes); 39 | key = scalar.toByteArray(); 40 | nonce = new byte[32]; 41 | t.challenge_bytes("no".getBytes(), nonce); 42 | } catch (Exception e) { 43 | e.printStackTrace(); 44 | } 45 | 46 | } 47 | 48 | private void expand_ed25519(byte[] seed) { 49 | try { 50 | MessageDigest hash = MessageDigest.getInstance("SHA-512"); 51 | h = hash.digest(seed); 52 | h[0] &= 248; 53 | h[31] &= 63; 54 | h[31] |= 64; 55 | key = Arrays.copyOfRange(h, 0, 32); 56 | key = ScalarUtils.divide_scalar_bytes_by_cofactor(key); 57 | nonce = Arrays.copyOfRange(h, 32, 64); 58 | } catch (NoSuchAlgorithmException e) { 59 | throw new IllegalArgumentException("Unsupported hash algorithm"); 60 | } 61 | } 62 | 63 | public byte[] getSeed() { 64 | return seed; 65 | } 66 | 67 | public byte[] getKey() { 68 | return key; 69 | } 70 | 71 | public byte[] getNonce() { 72 | return nonce; 73 | } 74 | 75 | /** 76 | * @return the hash of the seed 77 | */ 78 | public byte[] getH() { 79 | return h; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/sign/PublicKey.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.sign; 2 | 3 | import cafe.cryptography.curve25519.CompressedRistretto; 4 | import cafe.cryptography.curve25519.RistrettoElement; 5 | 6 | /** 7 | * @Author:yong.huang 8 | * @Date:2020-07-30 13:23 9 | */ 10 | public class PublicKey { 11 | 12 | private RistrettoElement ristretto; 13 | 14 | private CompressedRistretto compressedRistretto; 15 | 16 | public PublicKey(RistrettoElement ristretto) { 17 | this.ristretto = ristretto; 18 | this.compressedRistretto = ristretto.compress(); 19 | } 20 | 21 | public byte[] toPublicKey() { 22 | return compressedRistretto.toByteArray(); 23 | } 24 | 25 | public RistrettoElement getRistretto() { 26 | return ristretto; 27 | } 28 | 29 | public CompressedRistretto getCompressedRistretto() { 30 | return compressedRistretto; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/sign/Signature.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.sign; 2 | 3 | import cafe.cryptography.curve25519.CompressedRistretto; 4 | import cafe.cryptography.curve25519.Scalar; 5 | 6 | /** 7 | * @Author:yong.huang 8 | * @Date:2020-08-02 19:44 9 | */ 10 | public class Signature { 11 | 12 | private CompressedRistretto R; 13 | 14 | private Scalar s; 15 | 16 | public Signature(CompressedRistretto R, Scalar s) { 17 | this.R = R; 18 | this.s = s; 19 | } 20 | 21 | public CompressedRistretto getR() { 22 | return R; 23 | } 24 | 25 | public Scalar getS() { 26 | return s; 27 | } 28 | 29 | public byte[] to_bytes() { 30 | byte[] bytes = new byte[64]; 31 | byte[] rBytes = R.toByteArray(); 32 | byte[] sBytes = s.toByteArray(); 33 | System.arraycopy(rBytes, 0, bytes, 0, 32); 34 | System.arraycopy(sBytes, 0, bytes, 32, 32); 35 | bytes[63] |= 128; 36 | return bytes; 37 | } 38 | 39 | public static Signature from_bytes(byte[] bytes) throws Exception { 40 | if (bytes.length != 64) { 41 | throw new Exception("An error in the length of bytes handed to a constructor"); 42 | } 43 | byte[] lower = new byte[32]; 44 | byte[] upper = new byte[32]; 45 | System.arraycopy(bytes, 0, lower, 0, 32); 46 | System.arraycopy(bytes, 32, upper, 0, 32); 47 | if ((upper[31] & 128) == 0) { 48 | throw new Exception(" Signature not marked as schnorrkel, maybe try ed25519 instead"); 49 | } 50 | upper[31] &= 127; 51 | return new Signature(new CompressedRistretto(lower), Scalar.fromCanonicalBytes(upper)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/sign/SigningContext.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.sign; 2 | 3 | import com.debuggor.schnorrkel.merlin.Transcript; 4 | 5 | /** 6 | * @Author:yong.huang 7 | * @Date:2020-08-02 16:48 8 | */ 9 | public class SigningContext { 10 | 11 | private Transcript transcript; 12 | 13 | public SigningContext(Transcript transcript) { 14 | this.transcript = transcript; 15 | } 16 | 17 | public static SigningContext createSigningContext(byte[] context) throws Exception { 18 | Transcript t = Transcript.createTranscript("SigningContext".getBytes()); 19 | t.append_message("".getBytes(), context); 20 | return new SigningContext(t); 21 | } 22 | 23 | public SigningTranscript bytes(byte[] bytes) throws Exception { 24 | SigningTranscript t = new SigningTranscript(this.transcript.getStrobe()); 25 | t.append_message("sign-bytes".getBytes(), bytes); 26 | return t; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/sign/SigningTranscript.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.sign; 2 | 3 | import cafe.cryptography.curve25519.CompressedRistretto; 4 | import cafe.cryptography.curve25519.Scalar; 5 | import com.debuggor.schnorrkel.merlin.Strobe128; 6 | import com.debuggor.schnorrkel.merlin.Transcript; 7 | import com.debuggor.schnorrkel.merlin.TranscriptRng; 8 | import com.debuggor.schnorrkel.merlin.TranscriptRngBuilder; 9 | 10 | /** 11 | * @Author:yong.huang 12 | * @Date:2020-08-02 16:56 13 | */ 14 | public class SigningTranscript extends Transcript { 15 | 16 | public SigningTranscript(Strobe128 strobe128) { 17 | super(strobe128); 18 | } 19 | 20 | public void proto_name(byte[] label) throws Exception { 21 | commit_bytes("proto-name".getBytes(), label); 22 | } 23 | 24 | public void commit_point(byte[] label, CompressedRistretto compressed) throws Exception { 25 | this.commit_bytes(label, compressed.toByteArray()); 26 | } 27 | 28 | public Scalar witness_scalar(byte[] label, byte[] nonce_seeds) throws Exception { 29 | byte[] scalar_bytes = new byte[64]; 30 | this.witness_bytes(label, scalar_bytes, nonce_seeds); 31 | Scalar scalar = Scalar.fromBytesModOrderWide(scalar_bytes); 32 | return scalar; 33 | } 34 | 35 | public void witness_bytes(byte[] label, byte[] dest, byte[] nonce_seeds) throws Exception { 36 | this.witness_bytes_rng(label, dest, nonce_seeds); 37 | } 38 | 39 | public void witness_bytes_rng(byte[] label, byte[] dest, byte[] nonce_seeds) throws Exception { 40 | TranscriptRngBuilder br = build_rng(); 41 | br = br.commit_witness_bytes(label, nonce_seeds); 42 | TranscriptRng r = br.tFinalize(); 43 | r.fill_bytes(dest); 44 | } 45 | 46 | public Scalar challenge_scalar(byte[] label) throws Exception { 47 | byte[] buf = new byte[64]; 48 | this.challenge_bytes(label, buf); 49 | Scalar scalar = Scalar.fromBytesModOrderWide(buf); 50 | return scalar; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/utils/HexUtils.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.utils; 2 | 3 | 4 | public class HexUtils { 5 | 6 | /** 7 | * Constant-time byte comparison. 8 | * 9 | * @param b a byte 10 | * @param c a byte 11 | * @return 1 if b and c are equal, 0 otherwise. 12 | */ 13 | public static int equal(int b, int c) { 14 | int result = 0; 15 | int xor = b ^ c; 16 | for (int i = 0; i < 8; i++) { 17 | result |= xor >> i; 18 | } 19 | return (result ^ 0x01) & 0x01; 20 | } 21 | 22 | /** 23 | * Constant-time byte[] comparison. 24 | * 25 | * @param b a byte[] 26 | * @param c a byte[] 27 | * @return 1 if b and c are equal, 0 otherwise. 28 | */ 29 | public static int equal(byte[] b, byte[] c) { 30 | int result = 0; 31 | for (int i = 0; i < 32; i++) { 32 | result |= b[i] ^ c[i]; 33 | } 34 | 35 | return equal(result, 0); 36 | } 37 | 38 | /** 39 | * Constant-time determine if byte is negative. 40 | * 41 | * @param b the byte to check. 42 | * @return 1 if the byte is negative, 0 otherwise. 43 | */ 44 | public static int negative(int b) { 45 | return (b >> 8) & 1; 46 | } 47 | 48 | /** 49 | * Get the i'th bit of a byte array. 50 | * 51 | * @param h the byte array. 52 | * @param i the bit index. 53 | * @return 0 or 1, the value of the i'th bit in h 54 | */ 55 | public static int bit(byte[] h, int i) { 56 | return (h[i >> 3] >> (i & 7)) & 1; 57 | } 58 | 59 | /** 60 | * Converts a hex string to bytes. 61 | * 62 | * @param s the hex string to be converted. 63 | * @return the byte[] 64 | */ 65 | public static byte[] hexToBytes(String s) { 66 | int len = s.length(); 67 | byte[] data = new byte[len / 2]; 68 | for (int i = 0; i < len; i += 2) { 69 | data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) 70 | + Character.digit(s.charAt(i + 1), 16)); 71 | } 72 | return data; 73 | } 74 | 75 | /** 76 | * Converts bytes to a hex string. 77 | * 78 | * @param raw the byte[] to be converted. 79 | * @return the hex representation as a string. 80 | */ 81 | public static String bytesToHex(byte[] raw) { 82 | if (raw == null) { 83 | return null; 84 | } 85 | final StringBuilder hex = new StringBuilder(2 * raw.length); 86 | for (final byte b : raw) { 87 | hex.append(Character.forDigit((b & 0xF0) >> 4, 16)) 88 | .append(Character.forDigit((b & 0x0F), 16)); 89 | } 90 | return hex.toString(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/utils/NumberUtils.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.utils; 2 | 3 | /** 4 | * @Author:yong.huang 5 | * @Date:2020-08-01 00:23 6 | */ 7 | public class NumberUtils { 8 | 9 | public static long[] xorState(byte[] buf) { 10 | long[] state = new long[25]; 11 | int offset = 0; 12 | for (int i = 0; i < buf.length / 8; i++) { 13 | long a = readInt64(buf, offset); 14 | state[i] ^= a; 15 | offset += 8; 16 | } 17 | return state; 18 | } 19 | 20 | public static long readInt64(byte[] bytes, int offset) { 21 | return (long) bytes[offset] & 255L | 22 | ((long) bytes[offset + 1] & 255L) << 8 | 23 | ((long) bytes[offset + 2] & 255L) << 16 | 24 | ((long) bytes[offset + 3] & 255L) << 24 | 25 | ((long) bytes[offset + 4] & 255L) << 32 | 26 | ((long) bytes[offset + 5] & 255L) << 40 | 27 | ((long) bytes[offset + 6] & 255L) << 48 | 28 | ((long) bytes[offset + 7] & 255L) << 56; 29 | } 30 | 31 | 32 | public static void int64ToBytes(long val, byte[] result, int offset) { 33 | result[offset] = (byte) (255L & val); 34 | result[offset + 1] = (byte) (255L & val >> 8); 35 | result[offset + 2] = (byte) (255L & val >> 16); 36 | result[offset + 3] = (byte) (255L & val >> 24); 37 | result[offset + 4] = (byte) (255L & val >> 32); 38 | result[offset + 5] = (byte) (255L & val >> 40); 39 | result[offset + 6] = (byte) (255L & val >> 48); 40 | result[offset + 7] = (byte) (255L & val >> 56); 41 | } 42 | 43 | 44 | public static void uint32ToBytes(int val, byte[] out, int offset) { 45 | out[offset] = (byte) ((int) (255L & val)); 46 | out[offset + 1] = (byte) ((int) (255L & val >> 8)); 47 | out[offset + 2] = (byte) ((int) (255L & val >> 16)); 48 | out[offset + 3] = (byte) ((int) (255L & val >> 24)); 49 | } 50 | 51 | public static void uint64ToBytes(long val, byte[] out, int offset) { 52 | out[offset] = (byte) ((int) (255L & val)); 53 | out[offset + 1] = (byte) ((int) (255L & val >> 8)); 54 | out[offset + 2] = (byte) ((int) (255L & val >> 16)); 55 | out[offset + 3] = (byte) ((int) (255L & val >> 24)); 56 | out[offset + 4] = (byte) ((int) (255L & val >> 32)); 57 | out[offset + 5] = (byte) ((int) (255L & val >> 40)); 58 | out[offset + 6] = (byte) ((int) (255L & val >> 48)); 59 | out[offset + 7] = (byte) ((int) (255L & val >> 56)); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/debuggor/schnorrkel/utils/ScalarUtils.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.utils; 2 | 3 | /** 4 | * Scalar tooling 5 | * Elliptic curve utilities not provided by curve25519-dalek, 6 | * including some not so safe utilities for managing scalars and points. 7 | * https://github.com/w3f/schnorrkel/blob/master/src/scalars.rs 8 | * 9 | * @Author:yong.huang 10 | * @Date:2020-07-30 10:11 11 | */ 12 | public class ScalarUtils { 13 | 14 | public static byte[] divide_scalar_bytes_by_cofactor(byte[] scalar) { 15 | int low = 0; 16 | for (int i = scalar.length - 1; i >= 0; i--) { 17 | int b = scalar[i] & 0xFF; 18 | int r = b & 0b00000111; 19 | b >>= 3; 20 | b += low; 21 | low = r << 5; 22 | scalar[i] = (byte) b; 23 | } 24 | return scalar; 25 | } 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/debuggor/schnorrkel/sign/KeyTest.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.sign; 2 | 3 | import com.debuggor.schnorrkel.utils.HexUtils; 4 | 5 | 6 | /** 7 | * @Author:yong.huang 8 | * @Date:2020-07-29 23:03 9 | */ 10 | public class KeyTest { 11 | 12 | public static void main(String[] args) { 13 | byte[] seed = HexUtils.hexToBytes("579d7aa286b37b800b95fe41adabbf0c2a577caf2854baeca98f8fb242ff43ae"); 14 | KeyPair keyPair = KeyPair.fromSecretSeed(seed, ExpansionMode.Ed25519); 15 | PrivateKey privateKey = keyPair.getPrivateKey(); 16 | PublicKey publicKey = keyPair.getPublicKey(); 17 | 18 | byte[] key = privateKey.getKey(); 19 | byte[] nonce = privateKey.getNonce(); 20 | byte[] pubkey = publicKey.toPublicKey(); 21 | System.out.println("seed:" + HexUtils.bytesToHex(privateKey.getSeed())); 22 | System.out.println("key:" + HexUtils.bytesToHex(key)); 23 | System.out.println("nonce:" + HexUtils.bytesToHex(nonce)); 24 | System.out.println("pubkey:" + HexUtils.bytesToHex(pubkey)); 25 | 26 | KeyPair pair = KeyPair.generateKeyPair(); 27 | 28 | } 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/debuggor/schnorrkel/sign/SignTest.java: -------------------------------------------------------------------------------- 1 | package com.debuggor.schnorrkel.sign; 2 | 3 | import com.debuggor.schnorrkel.utils.HexUtils; 4 | 5 | /** 6 | * @Author:yong.huang 7 | * @Date:2020-08-02 17:53 8 | */ 9 | public class SignTest { 10 | 11 | public static void main(String[] args) throws Exception { 12 | KeyPair keyPair = KeyPair.fromSecretSeed(HexUtils.hexToBytes("579d7aa286b37b800b95fe41adabbf0c2a577caf2854baeca98f8fb242ff43ae"), ExpansionMode.Ed25519); 13 | 14 | byte[] message = "test message".getBytes(); 15 | SigningContext ctx = SigningContext.createSigningContext("good".getBytes()); 16 | SigningTranscript t = ctx.bytes(message); 17 | Signature signature = keyPair.sign(t); 18 | byte[] sign = signature.to_bytes(); 19 | System.out.println(HexUtils.bytesToHex(sign)); 20 | 21 | 22 | SigningContext ctx2 = SigningContext.createSigningContext("good".getBytes()); 23 | SigningTranscript t2 = ctx2.bytes(message); 24 | KeyPair fromPublicKey = KeyPair.fromPublicKey(keyPair.getPublicKey().toPublicKey()); 25 | boolean verify = fromPublicKey.verify(t2, sign); 26 | System.out.println(verify); 27 | } 28 | 29 | } 30 | --------------------------------------------------------------------------------