Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial commit of Base58/Base58Check support #943

Merged
merged 4 commits into from
Apr 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions warp10/src/main/java/io/warp10/script/WarpScriptLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
//
Expand Down
147 changes: 147 additions & 0 deletions warp10/src/main/java/io/warp10/script/functions/B58TO.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
121 changes: 121 additions & 0 deletions warp10/src/main/java/io/warp10/script/functions/TOB58.java
Original file line number Diff line number Diff line change
@@ -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();
}
}