From e831a69a2df6dae2e25d0d7f018cab49a99e0397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9Cur=D0=B0d=20H=D0=B0mz=D0=B0?= Date: Wed, 28 Aug 2024 11:28:30 +0300 Subject: [PATCH] chore: working warp sync --- .../chain/lightsyncstate/Authority.java | 10 +- .../chain/lightsyncstate/LightSyncState.java | 1 - .../protocol/warp/dto/BlockHeader.java | 6 +- .../com/limechain/storage/DBConstants.java | 1 + .../limechain/storage/block/SyncState.java | 7 + .../limechain/sync/JustificationVerifier.java | 96 +++--- .../sync/warpsync/WarpSyncMachine.java | 16 +- .../java/com/limechain/utils/HashUtils.java | 23 +- src/main/webapp/index.html | 2 + src/main/webapp/js/blake2b.js | 167 +++++++++ src/main/webapp/js/ed25519.js | 319 ++++++++++++++++++ 11 files changed, 569 insertions(+), 79 deletions(-) create mode 100644 src/main/webapp/js/blake2b.js create mode 100644 src/main/webapp/js/ed25519.js diff --git a/src/main/java/com/limechain/chain/lightsyncstate/Authority.java b/src/main/java/com/limechain/chain/lightsyncstate/Authority.java index e612d2efd..65cc3ea10 100644 --- a/src/main/java/com/limechain/chain/lightsyncstate/Authority.java +++ b/src/main/java/com/limechain/chain/lightsyncstate/Authority.java @@ -1,14 +1,20 @@ package com.limechain.chain.lightsyncstate; +import com.limechain.teavm.annotation.Reflectable; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import java.io.Serializable; import java.math.BigInteger; @Getter +@Setter +@Reflectable @AllArgsConstructor +@NoArgsConstructor public class Authority implements Serializable { - private final byte[] publicKey; - private final BigInteger weight; + private byte[] publicKey; + private BigInteger weight; } diff --git a/src/main/java/com/limechain/chain/lightsyncstate/LightSyncState.java b/src/main/java/com/limechain/chain/lightsyncstate/LightSyncState.java index fa20d171a..a5025658e 100644 --- a/src/main/java/com/limechain/chain/lightsyncstate/LightSyncState.java +++ b/src/main/java/com/limechain/chain/lightsyncstate/LightSyncState.java @@ -47,7 +47,6 @@ public static LightSyncState decode(Map lightSyncStateMap) { lightSyncState.grandpaAuthoritySet = new AuthoritySetReader() .read(new ScaleCodecReader(StringUtils.hexToBytes(grandpaAuthoritySet))); - System.out.println(lightSyncState); return lightSyncState; } } diff --git a/src/main/java/com/limechain/network/protocol/warp/dto/BlockHeader.java b/src/main/java/com/limechain/network/protocol/warp/dto/BlockHeader.java index 7e62c857b..cbe856a35 100644 --- a/src/main/java/com/limechain/network/protocol/warp/dto/BlockHeader.java +++ b/src/main/java/com/limechain/network/protocol/warp/dto/BlockHeader.java @@ -2,10 +2,13 @@ import com.limechain.network.protocol.blockannounce.scale.BlockHeaderScaleWriter; import com.limechain.polkaj.Hash256; +import com.limechain.utils.HashUtils; +import com.limechain.utils.StringUtils; import com.limechain.utils.scale.ScaleUtils; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.teavm.jso.core.JSString; import java.math.BigInteger; import java.util.Arrays; @@ -37,6 +40,7 @@ public String toString() { public Hash256 getHash() { byte[] scaleEncoded = ScaleUtils.Encode.encode(BlockHeaderScaleWriter.getInstance(), this); - return null;//new Hash256(HashUtils.hashWithBlake2b(scaleEncoded)); + JSString jsString = HashUtils.hashWithBlake2b(StringUtils.toHex(scaleEncoded)); + return new Hash256(StringUtils.hexToBytes(jsString.stringValue())); } } diff --git a/src/main/java/com/limechain/storage/DBConstants.java b/src/main/java/com/limechain/storage/DBConstants.java index 3ef7b2d35..9fd4bb725 100644 --- a/src/main/java/com/limechain/storage/DBConstants.java +++ b/src/main/java/com/limechain/storage/DBConstants.java @@ -21,6 +21,7 @@ public class DBConstants { public static final String AUTHORITY_SET = "ss::authoritySet"; public static final String LATEST_ROUND = "ss::latestRound"; public static final String SET_ID = "ss::setId"; + public static final String STATE_ROOT = "ss::stateRoot"; // } diff --git a/src/main/java/com/limechain/storage/block/SyncState.java b/src/main/java/com/limechain/storage/block/SyncState.java index a8367d786..e18ed5106 100644 --- a/src/main/java/com/limechain/storage/block/SyncState.java +++ b/src/main/java/com/limechain/storage/block/SyncState.java @@ -24,6 +24,7 @@ public class SyncState { private final BigInteger startingBlock; private final Hash256 genesisBlockHash; private Hash256 lastFinalizedBlockHash; + private Hash256 stateRoot; @Setter private Authority[] authoritySet; private BigInteger latestRound; @@ -41,6 +42,8 @@ private void loadState() { DBConstants.LAST_FINALIZED_BLOCK_NUMBER, BigInteger.class).orElse(BigInteger.ZERO); this.lastFinalizedBlockHash = new Hash256(LocalStorage.find( DBConstants.LAST_FINALIZED_BLOCK_HASH, byte[].class).orElse(genesisBlockHash.getBytes())); + byte[] stateRootBytes = LocalStorage.find(DBConstants.STATE_ROOT, byte[].class).orElse(null); + this.stateRoot = stateRootBytes != null ? new Hash256(stateRootBytes) : null; this.authoritySet = LocalStorage.find(DBConstants.AUTHORITY_SET, Authority[].class).orElse(new Authority[0]); this.latestRound = LocalStorage.find(DBConstants.LATEST_ROUND, BigInteger.class).orElse(BigInteger.ONE); this.setId = LocalStorage.find(DBConstants.SET_ID, BigInteger.class).orElse(BigInteger.ZERO); @@ -52,17 +55,21 @@ public void persistState() { LocalStorage.save(DBConstants.AUTHORITY_SET, authoritySet); LocalStorage.save(DBConstants.LATEST_ROUND, latestRound); LocalStorage.save(DBConstants.SET_ID, setId); + LocalStorage.save(DBConstants.STATE_ROOT, stateRoot.getBytes()); } public void finalizeHeader(BlockHeader header) { this.lastFinalizedBlockNumber = header.getBlockNumber(); this.lastFinalizedBlockHash = header.getHash(); + this.stateRoot = header.getStateRoot(); } public void finalizedCommitMessage(CommitMessage commitMessage) { try { this.lastFinalizedBlockHash = commitMessage.getVote().getBlockHash(); this.lastFinalizedBlockNumber = commitMessage.getVote().getBlockNumber(); + this.setId = commitMessage.getSetId(); + this.latestRound = commitMessage.getRoundNumber(); } catch (HeaderNotFoundException ignored) { log.fine("Received commit message for a block that is not in the block store"); } diff --git a/src/main/java/com/limechain/sync/JustificationVerifier.java b/src/main/java/com/limechain/sync/JustificationVerifier.java index e790dac44..497a9203a 100644 --- a/src/main/java/com/limechain/sync/JustificationVerifier.java +++ b/src/main/java/com/limechain/sync/JustificationVerifier.java @@ -3,7 +3,6 @@ import com.limechain.chain.lightsyncstate.Authority; import com.limechain.network.protocol.warp.dto.Precommit; import com.limechain.polkaj.Hash256; -import com.limechain.polkaj.Hash512; import com.limechain.rpc.server.AppBean; import com.limechain.storage.block.SyncState; import com.limechain.utils.LittleEndianUtils; @@ -18,6 +17,14 @@ import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -60,9 +67,11 @@ public static boolean verify(Precommit[] precommits, BigInteger round) { byte[] data = getDataToVerify(precommit, authoritiesSetId, round); - boolean isValid = - verifySignature(precommit.getAuthorityPublicKey().toString(), precommit.getSignature().toString(), - data); + boolean isValid = verifySignature( + StringUtils.toHex(precommit.getAuthorityPublicKey().getBytes()), + StringUtils.toHex(precommit.getSignature().getBytes()), + StringUtils.toHex(data)); + if (!isValid) { log.log(Level.WARNING, "Failed to verify signature"); return false; @@ -77,6 +86,32 @@ public static boolean verify(Precommit[] precommits, BigInteger round) { return true; } + private static boolean verifySignature(String publicKeyHex, String signatureHex, + String messageHex) { + JSPromise verifyAsync = verifyAsync(publicKeyHex, signatureHex, messageHex); + Object lock = new Object(); + AtomicBoolean valid = new AtomicBoolean(false); + + verifyAsync.then((isValid) -> { + synchronized (lock) { + valid.set(isValid.booleanValue()); + lock.notify(); + } + return null; + }); + + boolean isValid; + synchronized (lock) { + try { + lock.wait(); + isValid = valid.get(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + return isValid; + } + private static byte[] getDataToVerify(Precommit precommit, BigInteger authoritiesSetId, BigInteger round) { // 1 reserved byte for data type // 32 reserved for target hash @@ -106,53 +141,8 @@ private static byte[] getDataToVerify(Precommit precommit, BigInteger authoritie return data; } - public static boolean verifySignature(String publicKeyHex, String signatureHex, byte[] data) { - String message = StringUtils.toHex(data); - AtomicBoolean verifier = new AtomicBoolean(false); - Object lock = new Object(); - - verifySignature(publicKeyHex, signatureHex, message).then(isValid -> { - synchronized (lock) { - verifier.set(isValid.booleanValue()); - lock.notify(); - } - return null; - }); - - synchronized (lock) { - try { - lock.wait(); - - boolean result = verifier.get(); - if (!result) { - log.log(Level.WARNING, "Invalid signature"); - } - return result; - } catch (InterruptedException e) { - log.log(Level.WARNING, "Interrupted while waiting for signature verification"); - return false; - } - } - } - - @JSBody(params = {"publicKeyHex", "signatureHex", "messageHex"}, - script = "return (async () => {" + - " const publicKeyBytes = new Uint8Array([...publicKeyHex.matchAll(/../g)].map(m => parseInt(m[0], 16)));" + - " const signatureBytes = new Uint8Array([...signatureHex.matchAll(/../g)].map(m => parseInt(m[0], 16)));" + - " const publicKey = await crypto.subtle.importKey(" + - " 'raw'," + " publicKeyBytes," + - " {" + " name: 'NODE-ED25519'," + - " namedCurve: 'ed25519'" + " }," + - " true," + " ['verify']" + " );" + - " const messageBytes = new Uint8Array([...messageHex.matchAll(/../g)].map(m => parseInt(m[0], 16)));;" + - " const isValid = await crypto.subtle.verify(" + - " {" + " name: 'NODE-ED25519'" + - " }," + " publicKey," + - " signatureBytes," + - " messageBytes" + " );" + - " return isValid;" + - - "})()") - public static native JSPromise verifySignature(String publicKeyHex, String signatureHex, - String messageHex); + @JSBody(params = {"publicKeyHex", "signatureHex", + "messageHex"}, script = "return verifyAsync(signatureHex, messageHex, publicKeyHex);") + public static native JSPromise verifyAsync(String publicKeyHex, String signatureHex, + String messageHex); } diff --git a/src/main/java/com/limechain/sync/warpsync/WarpSyncMachine.java b/src/main/java/com/limechain/sync/warpsync/WarpSyncMachine.java index bfaee3f46..c281bf243 100644 --- a/src/main/java/com/limechain/sync/warpsync/WarpSyncMachine.java +++ b/src/main/java/com/limechain/sync/warpsync/WarpSyncMachine.java @@ -63,17 +63,19 @@ public boolean isSyncing() { } public void start() { - if (this.chainService.getChainSpec().getLightSyncState() != null) { - LightSyncState initState = LightSyncState.decode(this.chainService.getChainSpec().getLightSyncState()); - if (this.syncState.getLastFinalizedBlockNumber() - .compareTo(initState.getFinalizedBlockHeader().getBlockNumber()) < 0) { - this.syncState.setLightSyncState(initState); - } + LightSyncState initState = LightSyncState.decode(this.chainService.getChainSpec().getLightSyncState()); + + if (this.syncState.getLastFinalizedBlockNumber() + .compareTo(initState.getFinalizedBlockHeader().getBlockNumber()) < 0) { + this.syncState.setLightSyncState(initState); } + System.out.println(this.syncState.getLastFinalizedBlockHash()); + System.out.println(this.syncState.getLastFinalizedBlockNumber()); + final Hash256 initStateHash = this.syncState.getLastFinalizedBlockHash(); // Always start with requesting fragments - log.log(Level.INFO, "Requesting fragments... " + initStateHash.toString()); + log.log(Level.INFO, "Requesting fragments... " + initStateHash); this.networkService.updateCurrentSelectedPeerWithNextBootnode(); this.warpSyncAction = new RequestFragmentsAction(initStateHash); diff --git a/src/main/java/com/limechain/utils/HashUtils.java b/src/main/java/com/limechain/utils/HashUtils.java index 2989d78c9..e2a895553 100644 --- a/src/main/java/com/limechain/utils/HashUtils.java +++ b/src/main/java/com/limechain/utils/HashUtils.java @@ -1,22 +1,15 @@ package com.limechain.utils; +import com.limechain.polkaj.Hash256; import lombok.experimental.UtilityClass; +import org.teavm.jso.JSBody; +import org.teavm.jso.core.JSString; @UtilityClass public class HashUtils { -// public static final int HASH256_HASH_LENGTH = Hash256.SIZE_BYTES * Byte.SIZE; -// -// /** -// * Conducts a 256-bit Blake2b hash. -// * @param input the data to be hashed. -// * @return byte array containing the 256-bit hash result. -// */ -// public static byte[] hashWithBlake2b(byte[] input) { -// Blake2bDigest digest = new Blake2bDigest(HASH256_HASH_LENGTH); -// digest.reset(); -// digest.update(input, 0, input.length); -// byte[] hash = new byte[digest.getDigestSize()]; -// digest.doFinal(hash, 0); -// return hash; -// } + + @JSBody(params = {"inputHex"}, script = "{" + + "let bytes = new Uint8Array([...inputHex.matchAll(/../g)].map(m => parseInt(m[0], 16)));" + + "return Blake2b.hash(bytes,undefined,32);" + "}") + public static native JSString hashWithBlake2b(String inputHex); } diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index d2ce5d208..517da320f 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -18,6 +18,8 @@ + + diff --git a/src/main/webapp/js/blake2b.js b/src/main/webapp/js/blake2b.js new file mode 100644 index 000000000..6b5de4354 --- /dev/null +++ b/src/main/webapp/js/blake2b.js @@ -0,0 +1,167 @@ +var Blake2b = { + v: new Uint32Array(32), + m: new Uint32Array(32), + BLAKE2B_IV32: new Uint32Array([4089235720, 1779033703, 2227873595, 3144134277, 4271175723, 1013904242, 1595750129, 2773480762, 2917565137, 1359893119, 725511199, 2600822924, 4215389547, 528734635, 327033209, 1541459225]), + parameterBlock: new Uint8Array(64).fill(0), + SIGMA82: new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3, 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4, 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8, 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13, 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9, 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11, 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10, 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5, 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3].map(function (x) { + return x * 2; + })), + ADD64AA(v2, a, b) { + const o0 = v2[a] + v2[b]; + let o1 = v2[a + 1] + v2[b + 1]; + if (o0 >= 4294967296) o1++; + v2[a] = o0; + v2[a + 1] = o1; + }, + ADD64AC(v2, a, b0, b1) { + let o0 = v2[a] + b0; + if (b0 < 0) o0 += 4294967296; + let o1 = v2[a + 1] + b1; + if (o0 >= 4294967296) o1++; + v2[a] = o0; + v2[a + 1] = o1; + }, + B2B_GET32(arr, i) { + return arr[i] ^ arr[i + 1] << 8 ^ arr[i + 2] << 16 ^ arr[i + 3] << 24; + }, + B2B_G(a, b, c, d, ix, iy) { + const x0 = (this.m)[ix]; + const x1 = (this.m)[ix + 1]; + const y0 = (this.m)[iy]; + const y1 = (this.m)[iy + 1]; + this.ADD64AA(this.v, a, b); + this.ADD64AC(this.v, a, x0, x1); + let xor0 = (this.v)[d] ^ (this.v)[a]; + let xor1 = (this.v)[d + 1] ^ (this.v)[a + 1]; + (this.v)[d] = xor1; + (this.v)[d + 1] = xor0; + this.ADD64AA(this.v, c, d); + xor0 = (this.v)[b] ^ (this.v)[c]; + xor1 = (this.v)[b + 1] ^ (this.v)[c + 1]; + (this.v)[b] = xor0 >>> 24 ^ xor1 << 8; + (this.v)[b + 1] = xor1 >>> 24 ^ xor0 << 8; + this.ADD64AA(this.v, a, b); + this.ADD64AC(this.v, a, y0, y1); + xor0 = (this.v)[d] ^ (this.v)[a]; + xor1 = (this.v)[d + 1] ^ (this.v)[a + 1]; + (this.v)[d] = xor0 >>> 16 ^ xor1 << 16; + (this.v)[d + 1] = xor1 >>> 16 ^ xor0 << 16; + this.ADD64AA(this.v, c, d); + xor0 = (this.v)[b] ^ (this.v)[c]; + xor1 = (this.v)[b + 1] ^ (this.v)[c + 1]; + (this.v)[b] = xor1 >>> 31 ^ xor0 << 1; + (this.v)[b + 1] = xor0 >>> 31 ^ xor1 << 1; + }, + blake2bCompress(ctx, last) { + let i = 0; + for (i = 0; i < 16; i++) { + (this.v)[i] = ctx.h[i]; + (this.v)[i + 16] = (this.BLAKE2B_IV32)[i]; + } + (this.v)[24] = (this.v)[24] ^ ctx.t; + (this.v)[25] = (this.v)[25] ^ ctx.t / 4294967296; + if (last) { + (this.v)[28] = ~(this.v)[28]; + (this.v)[29] = ~(this.v)[29]; + } + for (i = 0; i < 32; i++) { + (this.m)[i] = this.B2B_GET32(ctx.b, 4 * i); + } + for (i = 0; i < 12; i++) { + this.B2B_G(0, 8, 16, 24, (this.SIGMA82)[i * 16 + 0], (this.SIGMA82)[i * 16 + 1]); + this.B2B_G(2, 10, 18, 26, (this.SIGMA82)[i * 16 + 2], (this.SIGMA82)[i * 16 + 3]); + this.B2B_G(4, 12, 20, 28, (this.SIGMA82)[i * 16 + 4], (this.SIGMA82)[i * 16 + 5]); + this.B2B_G(6, 14, 22, 30, (this.SIGMA82)[i * 16 + 6], (this.SIGMA82)[i * 16 + 7]); + this.B2B_G(0, 10, 20, 30, (this.SIGMA82)[i * 16 + 8], (this.SIGMA82)[i * 16 + 9]); + this.B2B_G(2, 12, 22, 24, (this.SIGMA82)[i * 16 + 10], (this.SIGMA82)[i * 16 + 11]); + this.B2B_G(4, 14, 16, 26, (this.SIGMA82)[i * 16 + 12], (this.SIGMA82)[i * 16 + 13]); + this.B2B_G(6, 8, 18, 28, (this.SIGMA82)[i * 16 + 14], (this.SIGMA82)[i * 16 + 15]); + } + for (i = 0; i < 16; i++) { + ctx.h[i] = ctx.h[i] ^ (this.v)[i] ^ (this.v)[i + 16]; + } + }, + blake2bInit(outlen, key, salt, personal) { + if (outlen === 0 || outlen > 64) { + throw new Error("Illegal output length, expected 0 < length <= 64"); + } + if (key && key.length > 64) { + throw new Error("Illegal key, expected Uint8Array with 0 < length <= 64"); + } + if (salt && salt.length !== 16) { + throw new Error("Illegal salt, expected Uint8Array with length is 16"); + } + if (personal && personal.length !== 16) { + throw new Error("Illegal personal, expected Uint8Array with length is 16"); + } + const ctx = { + b: new Uint8Array(128), h: new Uint32Array(16), t: 0, c: 0, outlen + }; + this.parameterBlock.fill(0); + (this.parameterBlock)[0] = outlen; + if (key) (this.parameterBlock)[1] = key.length; + (this.parameterBlock)[2] = 1; + (this.parameterBlock)[3] = 1; + if (salt) this.parameterBlock.set(salt, 32); + if (personal) this.parameterBlock.set(personal, 48); + for (let i = 0; i < 16; i++) { + ctx.h[i] = (this.BLAKE2B_IV32)[i] ^ this.B2B_GET32(this.parameterBlock, i * 4); + } + if (key) { + this.blake2bUpdate(ctx, key); + ctx.c = 128; + } + return ctx; + }, + blake2bUpdate(ctx, input) { + for (let i = 0; i < input.length; i++) { + if (ctx.c === 128) { + ctx.t += ctx.c; + this.blake2bCompress(ctx, false); + ctx.c = 0; + } + ctx.b[ctx.c++] = input[i]; + } + }, + blake2bFinal(ctx) { + ctx.t += ctx.c; + while (ctx.c < 128) { + ctx.b[ctx.c++] = 0; + } + this.blake2bCompress(ctx, true); + const out = new Uint8Array(ctx.outlen); + for (let i = 0; i < ctx.outlen; i++) { + out[i] = ctx.h[i >> 2] >> 8 * (i & 3); + } + return out; + }, + blake2bStart(input, key, outlen, salt, personal) { + outlen = outlen || 64; + const ctx = this.blake2bInit(outlen, key, this.normalizeInput(salt), this.normalizeInput(personal)); + this.blake2bUpdate(ctx, this.normalizeInput(input)); + return this.blake2bFinal(ctx); + }, + normalizeInput(input) { + let ret; + if (input instanceof Uint8Array) { + ret = input; + } else if (typeof input === "string") { + const encoder = new TextEncoder; + ret = encoder.encode(input); + } else { + throw new Error("Input must be an string, Buffer or Uint8Array"); + } + return ret; + }, + toHex(bytes) { + return Array.prototype.map.call(bytes, function (n) { + return (n < 16 ? "0" : "") + n.toString(16); + }).join(""); + }, + hash(message = "", secret = undefined, length = 64, salt = new Uint8Array(16), personal = new Uint8Array(16)) { + if (secret?.length === 0) secret = undefined; + if (typeof secret === "string") secret = new TextEncoder().encode(secret); + const output = this.blake2bStart(message, secret, length, salt, personal); + return this.toHex(output); + } +} \ No newline at end of file diff --git a/src/main/webapp/js/ed25519.js b/src/main/webapp/js/ed25519.js new file mode 100644 index 000000000..a640b1019 --- /dev/null +++ b/src/main/webapp/js/ed25519.js @@ -0,0 +1,319 @@ +const P = 2n ** 255n - 19n; // ed25519 is twisted edwards curve +const N = 2n ** 252n + 27742317777372353535851937790883648493n; // curve's (group) order +const Gx = 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an; // base point x +const Gy = 0x6666666666666666666666666666666666666666666666666666666666666658n; // base point y +const CURVE = { + a: -1n, // where a=-1, d = -(121665/121666) == -(121665 * inv(121666)) mod P + d: 37095705934669439343138083508754565189542113879843219016388785533085940283555n, + p: P, n: N, h: 8, Gx, Gy // field prime, curve (group) order, cofactor +}; +const err = (m = '') => { throw new Error(m); }; // error helper, messes-up stack trace +const str = (s) => typeof s === 'string'; // is string +const isu8 = (a) => (a instanceof Uint8Array || + (a != null && typeof a === 'object' && a.constructor.name === 'Uint8Array')); +const au8 = (a, l) => // is Uint8Array (of specific length) + !isu8(a) || (typeof l === 'number' && l > 0 && a.length !== l) ? + err('Uint8Array of valid length expected') : a; +const u8n = (data) => new Uint8Array(data); // creates Uint8Array +const toU8 = (a, len) => au8(str(a) ? h2b(a) : u8n(au8(a)), len); // norm(hex/u8a) to u8a +const mod = (a, b = P) => { let r = a % b; return r >= 0n ? r : b + r; }; // mod division +const isPoint = (p) => (p instanceof Point ? p : err('Point expected')); // is xyzt point + +class Point { + constructor(ex, ey, ez, et) { + this.ex = ex; + this.ey = ey; + this.ez = ez; + this.et = et; + } + static fromHex(hex, zip215 = false) { + const { d } = CURVE; + hex = toU8(hex, 32); + const normed = hex.slice(); // copy the array to not mess it up + const lastByte = hex[31]; + normed[31] = lastByte & ~0x80; // adjust first LE byte = last BE byte + const y = b2n_LE(normed); // decode as little-endian, convert to num + if (zip215 && !(0n <= y && y < 2n ** 256n)) + err('bad y coord 1'); // zip215=true [1..2^256-1] + if (!zip215 && !(0n <= y && y < P)) + err('bad y coord 2'); // zip215=false [1..P-1] + const y2 = mod(y * y); // y² + const u = mod(y2 - 1n); // u=y²-1 + const v = mod(d * y2 + 1n); // v=dy²+1 + let { isValid, value: x } = uvRatio(u, v); // (uv³)(uv⁷)^(p-5)/8; square root + if (!isValid) + err('bad y coordinate 3'); // not square root: bad point + const isXOdd = (x & 1n) === 1n; // adjust sign of x coordinate + const isLastByteOdd = (lastByte & 0x80) !== 0; // x_0, last bit + if (!zip215 && x === 0n && isLastByteOdd) + err('bad y coord 3'); // x=0 and x_0 = 1 + if (isLastByteOdd !== isXOdd) + x = mod(-x); + return new Point(x, y, 1n, mod(x * y)); // Z=1, T=xy + } + equals(other) { + const { ex: X1, ey: Y1, ez: Z1 } = this; + const { ex: X2, ey: Y2, ez: Z2 } = isPoint(other); // isPoint() checks class equality + const X1Z2 = mod(X1 * Z2), X2Z1 = mod(X2 * Z1); + const Y1Z2 = mod(Y1 * Z2), Y2Z1 = mod(Y2 * Z1); + return X1Z2 === X2Z1 && Y1Z2 === Y2Z1; + } + is0() { return this.equals(I); } + negate() { + return new Point(mod(-this.ex), this.ey, this.ez, mod(-this.et)); + } + double() { + const { ex: X1, ey: Y1, ez: Z1 } = this; // Cost: 4M + 4S + 1*a + 6add + 1*2 + const { a } = CURVE; // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd + const A = mod(X1 * X1); + const B = mod(Y1 * Y1); + const C = mod(2n * mod(Z1 * Z1)); + const D = mod(a * A); + const x1y1 = X1 + Y1; + const E = mod(mod(x1y1 * x1y1) - A - B); + const G = D + B; + const F = G - C; + const H = D - B; + const X3 = mod(E * F); + const Y3 = mod(G * H); + const T3 = mod(E * H); + const Z3 = mod(F * G); + return new Point(X3, Y3, Z3, T3); + } + add(other) { + const { ex: X1, ey: Y1, ez: Z1, et: T1 } = this; // Cost: 8M + 1*k + 8add + 1*2. + const { ex: X2, ey: Y2, ez: Z2, et: T2 } = isPoint(other); // doesn't check if other on-curve + const { a, d } = CURVE; // http://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-3 + const A = mod(X1 * X2); + const B = mod(Y1 * Y2); + const C = mod(T1 * d * T2); + const D = mod(Z1 * Z2); + const E = mod((X1 + Y1) * (X2 + Y2) - A - B); + const F = mod(D - C); + const G = mod(D + C); + const H = mod(B - a * A); + const X3 = mod(E * F); + const Y3 = mod(G * H); + const T3 = mod(E * H); + const Z3 = mod(F * G); + return new Point(X3, Y3, Z3, T3); + } + mul(n, safe = true) { + if (n === 0n) + return safe === true ? err('cannot multiply by 0') : I; + if (!(typeof n === 'bigint' && 0n < n && n < N)) + err('invalid scalar, must be < L'); + if (!safe && this.is0() || n === 1n) + return this; // safe=true bans 0. safe=false allows 0. + if (this.equals(G)) + return wNAF(n).p; // use wNAF precomputes for base points + let p = I, f = G; // init result point & fake point + for (let d = this; n > 0n; d = d.double(), n >>= 1n) { // double-and-add ladder + if (n & 1n) + p = p.add(d); // if bit is present, add to point + else if (safe) + f = f.add(d); // if not, add to fake for timing safety + } + return p; + } + clearCofactor() { return this.mul(BigInt(CURVE.h), false); } // multiply by cofactor + isSmallOrder() { return this.clearCofactor().is0(); } // check if P is small order + toAffine() { + const { ex: x, ey: y, ez: z } = this; // (x, y, z, t) ∋ (x=x/z, y=y/z, t=xy) + if (this.equals(I)) + return { x: 0n, y: 1n }; // fast-path for zero point + const iz = invert(z); // z^-1: invert z + if (mod(z * iz) !== 1n) + err('invalid inverse'); // (z * z^-1) must be 1, otherwise bad math + return { x: mod(x * iz), y: mod(y * iz) }; // x = x*z^-1; y = y*z^-1 + } + toRawBytes() { + const { x, y } = this.toAffine(); // convert to affine 2d point + const b = n2b_32LE(y); // encode number to 32 bytes + b[31] |= x & 1n ? 0x80 : 0; // store sign in first LE byte + return b; + } +} +Point.BASE = new Point(Gx, Gy, 1n, mod(Gx * Gy)); // Generator / Base point +Point.ZERO = new Point(0n, 1n, 1n, 0n); // Identity / Zero point +const { BASE: G, ZERO: I } = Point; // Generator, identity points +const padh = (num, pad) => num.toString(16).padStart(pad, '0'); +const b2h = (b) => Array.from(b).map(e => padh(e, 2)).join(''); // bytes to hex +const h2b = (hex) => { + const l = hex.length; // error if not string, + if (!str(hex) || l % 2) + err('hex invalid 1'); // or has odd length like 3, 5. + const arr = u8n(l / 2); // create result array + for (let i = 0; i < arr.length; i++) { + const j = i * 2; + const h = hex.slice(j, j + 2); // hexByte. slice is faster than substr + const b = Number.parseInt(h, 16); // byte, created from string part + if (Number.isNaN(b) || b < 0) + err('hex invalid 2'); // byte must be valid 0 <= byte < 256 + arr[i] = b; + } + return arr; +}; + +const n2b_32LE = (num) => h2b(padh(num, 32 * 2)).reverse(); // number to bytes LE +const b2n_LE = (b) => BigInt('0x' + b2h(u8n(au8(b)).reverse())); // bytes LE to num +const concatB = (...arrs) => { + const r = u8n(arrs.reduce((sum, a) => sum + au8(a).length, 0)); // create u8a of summed length + let pad = 0; // walk through each array, + arrs.forEach(a => { r.set(a, pad); pad += a.length; }); // ensure they have proper type + return r; +}; +const invert = (num, md = P) => { + if (num === 0n || md <= 0n) + err('no inverse n=' + num + ' mod=' + md); // no neg exponent for now + let a = mod(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n; + while (a !== 0n) { // uses euclidean gcd algorithm + const q = b / a, r = b % a; // not constant-time + const m = x - u * q, n = y - v * q; + b = a, a = r, x = u, y = v, u = m, v = n; + } + return b === 1n ? mod(x, md) : err('no inverse'); // b is gcd at this point +}; +const pow2 = (x, power) => { + let r = x; + while (power-- > 0n) { + r *= r; + r %= P; + } + return r; +}; +const pow_2_252_3 = (x) => { + const x2 = (x * x) % P; // x^2, bits 1 + const b2 = (x2 * x) % P; // x^3, bits 11 + const b4 = (pow2(b2, 2n) * b2) % P; // x^(2^4-1), bits 1111 + const b5 = (pow2(b4, 1n) * x) % P; // x^(2^5-1), bits 11111 + const b10 = (pow2(b5, 5n) * b5) % P; // x^(2^10) + const b20 = (pow2(b10, 10n) * b10) % P; // x^(2^20) + const b40 = (pow2(b20, 20n) * b20) % P; // x^(2^40) + const b80 = (pow2(b40, 40n) * b40) % P; // x^(2^80) + const b160 = (pow2(b80, 80n) * b80) % P; // x^(2^160) + const b240 = (pow2(b160, 80n) * b80) % P; // x^(2^240) + const b250 = (pow2(b240, 10n) * b10) % P; // x^(2^250) + const pow_p_5_8 = (pow2(b250, 2n) * x) % P; // < To pow to (p+3)/8, multiply it by x. + return { pow_p_5_8, b2 }; +}; +const RM1 = 19681161376707505956807079304988542015446066515923890162744021073123829784752n; // √-1 +const uvRatio = (u, v) => { + const v3 = mod(v * v * v); // v³ + const v7 = mod(v3 * v3 * v); // v⁷ + const pow = pow_2_252_3(u * v7).pow_p_5_8; // (uv⁷)^(p-5)/8 + let x = mod(u * v3 * pow); // (uv³)(uv⁷)^(p-5)/8 + const vx2 = mod(v * x * x); // vx² + const root1 = x; // First root candidate + const root2 = mod(x * RM1); // Second root candidate; RM1 is √-1 + const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root + const useRoot2 = vx2 === mod(-u); // If vx² = -u, set x <-- x * 2^((p-1)/4) + const noRoot = vx2 === mod(-u * RM1); // There is no valid root, vx² = -u√-1 + if (useRoot1) + x = root1; + if (useRoot2 || noRoot) + x = root2; // We return root2 anyway, for const-time + if ((mod(x) & 1n) === 1n) + x = mod(-x); // edIsNegative + return { isValid: useRoot1 || useRoot2, value: x }; +}; +const modL_LE = (hash) => mod(b2n_LE(hash), N); // modulo L; but little-endian +let _shaS; +const sha512a = (...m) => etc.sha512Async(...m); // Async SHA512 +const sha512s = (...m) => // Sync SHA512, not set by default + typeof _shaS === 'function' ? _shaS(...m) : err('etc.sha512Sync not set'); + +function hashFinish(asynchronous, res) { + if (asynchronous) + return sha512a(res.hashable).then(res.finish); + return res.finish(sha512s(res.hashable)); +} + +const dvo = { zip215: true }; +const _verify = (sig, msg, pub, opts = dvo) => { + msg = toU8(msg); // Message hex str/Bytes + sig = toU8(sig, 64); // Signature hex str/Bytes, must be 64 bytes + const { zip215 } = opts; // switch between zip215 and rfc8032 verif + let A, R, s, SB, hashable = new Uint8Array(); + try { + A = Point.fromHex(pub, zip215); // public key A decoded + R = Point.fromHex(sig.slice(0, 32), zip215); // 0 <= R < 2^256: ZIP215 R can be >= P + s = b2n_LE(sig.slice(32, 64)); // Decode second half as an integer S + SB = G.mul(s, false); // in the range 0 <= s < L + hashable = concatB(R.toRawBytes(), A.toRawBytes(), msg); // dom2(F, C) || R || A || PH(M) + } + catch (error) { } + const finish = (hashed) => { + if (SB == null) + return false; // false if try-catch catched an error + if (!zip215 && A.isSmallOrder()) + return false; // false for SBS: Strongly Binding Signature + const k = modL_LE(hashed); // decode in little-endian, modulo L + const RkA = R.add(A.mul(k, false)); // [8]R + [8][k]A' + return RkA.add(SB.negate()).clearCofactor().is0(); // [8][S]B = [8]R + [8][k]A' + }; + return { hashable, finish }; +}; +const verifyAsync = async (s, m, p, opts = dvo) => hashFinish(true, _verify(s, m, p, opts)); +const cr = () => // We support: 1) browsers 2) node.js 19+ + typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined; +const etc = { + mod, invert, + sha512Async: async (...messages) => { + const crypto = cr(); + if (!crypto || !crypto.subtle) + err('crypto.subtle or etc.sha512Async must be defined'); + const m = concatB(...messages); + return u8n(await crypto.subtle.digest('SHA-512', m.buffer)); + }, +}; +Object.defineProperties(etc, { sha512Sync: { + configurable: false, get() { return _shaS; }, set(f) { if (!_shaS) + _shaS = f; }, + } }); +const W = 8; // Precomputes-related code. W = window size +const precompute = () => { + const points = []; // 10x sign(), 2x verify(). To achieve this, + const windows = 256 / W + 1; // app needs to spend 40ms+ to calculate + let p = G, b = p; // a lot of points related to base point G. + for (let w = 0; w < windows; w++) { // Points are stored in array and used + b = p; // any time Gx multiplication is done. + points.push(b); // They consume 16-32 MiB of RAM. + for (let i = 1; i < 2 ** (W - 1); i++) { + b = b.add(p); + points.push(b); + } + p = b.double(); // Precomputes don't speed-up getSharedKey, + } // which multiplies user point by scalar, + return points; // when precomputes are using base point +}; +let Gpows = undefined; // precomputes for base point G +const wNAF = (n) => { + // Compared to other point mult methods, + const comp = Gpows || (Gpows = precompute()); // stores 2x less points using subtraction + const neg = (cnd, p) => { let n = p.negate(); return cnd ? n : p; }; // negate + let p = I, f = G; // f must be G, or could become I in the end + const windows = 1 + 256 / W; // W=8 17 windows + const wsize = 2 ** (W - 1); // W=8 128 window size + const mask = BigInt(2 ** W - 1); // W=8 will create mask 0b11111111 + const maxNum = 2 ** W; // W=8 256 + const shiftBy = BigInt(W); // W=8 8 + for (let w = 0; w < windows; w++) { + const off = w * wsize; + let wbits = Number(n & mask); // extract W bits. + n >>= shiftBy; // shift number by W bits. + if (wbits > wsize) { + wbits -= maxNum; + n += 1n; + } // split if bits > max: +224 => 256-32 + const off1 = off, off2 = off + Math.abs(wbits) - 1; // offsets, evaluate both + const cnd1 = w % 2 !== 0, cnd2 = wbits < 0; // conditions, evaluate both + if (wbits === 0) { + f = f.add(neg(cnd1, comp[off1])); // bits are 0: add garbage to fake point + } + else { // ^ can't add off2, off2 = I + p = p.add(neg(cnd2, comp[off2])); // bits are 1: add to result point + } + } + return { p, f }; // return both real and fake points for JIT +}; \ No newline at end of file