diff --git a/warp10/src/main/java/io/warp10/script/WarpScriptLib.java b/warp10/src/main/java/io/warp10/script/WarpScriptLib.java index d17ece63b..f6ed8e7d4 100644 --- a/warp10/src/main/java/io/warp10/script/WarpScriptLib.java +++ b/warp10/src/main/java/io/warp10/script/WarpScriptLib.java @@ -1094,6 +1094,10 @@ public class WarpScriptLib { public static final String TOBIN_ = "->BIN"; public static final String TOHEX_ = "->HEX"; public static final String TOB64 = "->B64"; + public static final String TOB58 = "->B58"; + public static final String TOB58C = "->B58C"; + public static final String B58TO = "B58->"; + public static final String B58CTO = "B58C->"; public static final String TOB64URL = "->B64URL"; public static final String TOENCODER = "->ENCODER"; public static final String TOENCODERS = "->ENCODERS"; @@ -1579,6 +1583,11 @@ public class WarpScriptLib { addNamedWarpScriptFunction(new OPB64TO(OPB64TO)); addNamedWarpScriptFunction(new OPB64TOHEX(OPB64TOHEX)); + addNamedWarpScriptFunction(new TOB58(TOB58, false)); + addNamedWarpScriptFunction(new TOB58(TOB58C, true)); + addNamedWarpScriptFunction(new B58TO(B58TO, false)); + addNamedWarpScriptFunction(new B58TO(B58CTO, true)); + // // Conditionals // diff --git a/warp10/src/main/java/io/warp10/script/functions/B58TO.java b/warp10/src/main/java/io/warp10/script/functions/B58TO.java new file mode 100644 index 000000000..af4ebdad8 --- /dev/null +++ b/warp10/src/main/java/io/warp10/script/functions/B58TO.java @@ -0,0 +1,147 @@ +// +// Copyright 2021 SenX S.A.S. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package io.warp10.script.functions; + +import java.math.BigInteger; +import java.util.Arrays; + +import org.bouncycastle.crypto.digests.SHA256Digest; + +import com.google.common.primitives.Bytes; + +import io.warp10.script.NamedWarpScriptFunction; +import io.warp10.script.WarpScriptException; +import io.warp10.script.WarpScriptStack; +import io.warp10.script.WarpScriptStackFunction; + +/** + * Decode a Base58 or Base58Check encoded String + */ +public class B58TO extends NamedWarpScriptFunction implements WarpScriptStackFunction { + + private static final BigInteger[] TEBAHPLA; + + static { + TEBAHPLA = new BigInteger[123]; + for (int i = 0; i < TOB58.ALPHABET.length(); i++) { + TEBAHPLA[TOB58.ALPHABET.charAt(i)] = BigInteger.valueOf(i); + } + } + + private final boolean check; + + public B58TO(String name, boolean check) { + super(name); + this.check = check; + } + + @Override + public Object apply(WarpScriptStack stack) throws WarpScriptException { + Object top = stack.pop(); + + byte[] prefix = null; + + if (this.check) { + if(!(top instanceof byte[])) { + throw new WarpScriptException(getName() + " expects a byte array prefix."); + } + prefix = (byte[]) top; + top = stack.pop(); + } + + if (!(top instanceof String)) { + throw new WarpScriptException(getName() + " operates on a STRING."); + } + + byte[] decoded; + + try { + decoded = decode((String) top); + } catch (WarpScriptException wse) { + throw new WarpScriptException(getName() + " encountered an error while decoding Base58.", wse); + } + + if (check) { + if (decoded.length < prefix.length + 4) { + throw new WarpScriptException(getName() + " Base58 STRING too short."); + } + + for (int i = 0; i < prefix.length; i++) { + if (i > decoded.length || decoded[i] != prefix[i]) { + throw new WarpScriptException(getName() + " invalid prefix."); + } + } + + SHA256Digest digest = new SHA256Digest(); + digest.update(decoded, 0, decoded.length - 4); + byte[] hash = new byte[digest.getDigestSize()]; + digest.doFinal(hash, 0); + digest.reset(); + digest.update(hash, 0, hash.length); + digest.doFinal(hash, 0); + + // + // Check trailer + // + + for (int i = 0; i < 4; i++) { + if (hash[i] != decoded[decoded.length - 4 + i]) { + throw new WarpScriptException(getName() + " invalid checksum."); + } + } + + byte[] data = new byte[decoded.length - prefix.length - 4]; + System.arraycopy(decoded, prefix.length, data, 0, data.length); + + decoded = data; + } + + stack.push(decoded); + return stack; + } + + public static byte[] decode(String encoded) throws WarpScriptException { + // + // See https://tools.ietf.org/id/draft-msporny-base58-02.html + // + + int zero_counter = 0; + + while(zero_counter < encoded.length() && '1' == encoded.charAt(zero_counter)) { + zero_counter++; + } + + BigInteger n = BigInteger.ZERO; + + for (int i = zero_counter; i < encoded.length(); i++) { + char c = encoded.charAt(i); + if (c > 127 || null == TEBAHPLA[c]) { + throw new WarpScriptException("Invalid input '" + encoded.charAt(i) + "' at position " + i + "."); + } + n = n.multiply(TOB58.FIFTY_EIGHT).add(TEBAHPLA[c]); + } + byte[] nbytes = n.toByteArray(); + // If the number is positive but would lead to an hexadecimal notation starting with a byte over 0x7F, + // the generated byte array starts with a leading 0, this must therefore be stripped. + + if ((byte) 0x00 == nbytes[0]) { + nbytes = Arrays.copyOfRange(nbytes, 1, nbytes.length); + } + + return Bytes.concat(new byte[zero_counter], nbytes); + } +} diff --git a/warp10/src/main/java/io/warp10/script/functions/TOB58.java b/warp10/src/main/java/io/warp10/script/functions/TOB58.java new file mode 100644 index 000000000..9b379ce31 --- /dev/null +++ b/warp10/src/main/java/io/warp10/script/functions/TOB58.java @@ -0,0 +1,121 @@ +// +// Copyright 2021 SenX S.A.S. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package io.warp10.script.functions; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.crypto.digests.SHA256Digest; + +import com.google.common.primitives.Bytes; + +import io.warp10.script.NamedWarpScriptFunction; +import io.warp10.script.WarpScriptException; +import io.warp10.script.WarpScriptStack; +import io.warp10.script.WarpScriptStackFunction; + +/** + * Encode a String into Base58 or Base58Check + * See https://tools.ietf.org/html/draft-msporny-base58-03 + */ +public class TOB58 extends NamedWarpScriptFunction implements WarpScriptStackFunction { + + public static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + public static final BigInteger FIFTY_EIGHT = new BigInteger("58"); + + private final boolean check; + + public TOB58(String name, boolean check) { + super(name); + this.check = check; + } + + @Override + public Object apply(WarpScriptStack stack) throws WarpScriptException { + Object top = stack.pop(); + + byte[] payload = null; + byte[] prefix = null; + + if (check) { + if (!(top instanceof byte[])) { + throw new WarpScriptException(getName() + " expects a byte array prefix."); + } + prefix = (byte[]) top; + top = stack.pop(); + } + + if (top instanceof byte[]) { + payload = (byte[]) top; + } else if (top instanceof String) { + payload = ((String) top).getBytes(StandardCharsets.UTF_8); + } else { + throw new WarpScriptException(getName() + " operates on STRING or a byte array."); + } + + if (check) { + SHA256Digest digest = new SHA256Digest(); + digest.update(prefix, 0, prefix.length); + digest.update(payload, 0, payload.length); + byte[] hash = new byte[digest.getDigestSize()]; + digest.doFinal(hash, 0); + digest.reset(); + digest.update(hash, 0, hash.length); + digest.doFinal(hash, 0); + byte[] data = new byte[prefix.length + payload.length + 4]; + System.arraycopy(prefix, 0, data, 0, prefix.length); + System.arraycopy(payload, 0, data, prefix.length, payload.length); + System.arraycopy(hash, 0, data, prefix.length + payload.length, 4); + payload = data; + } + + stack.push(encode(payload)); + return stack; + } + + public static String encode(byte[] data) { + // + // See https://tools.ietf.org/id/draft-msporny-base58-01.html + // + + int zero_counter = 0; + + StringBuilder b58_encoding = new StringBuilder(); + + while(zero_counter < data.length && 0x00 == data[zero_counter]) { + b58_encoding.append("1"); + zero_counter++; + } + + // Add a leading 0x00 byte if the first byte is above 128 as otherwise + // the number would be considered a negative one + if (0x00 != (data[0] & 0x80)) { + data = Bytes.concat(new byte[] { 0x00 }, data); + } + + BigInteger n = new BigInteger(data); + + while(!BigInteger.ZERO.equals(n)) { + int r = n.mod(FIFTY_EIGHT).intValue(); + n = n.divide(FIFTY_EIGHT); + b58_encoding.insert(zero_counter, ALPHABET.charAt(r)); + } + + return b58_encoding.toString(); + } +}