From b1963e5223f59f95c4dbe3dd24077120f4b02cc1 Mon Sep 17 00:00:00 2001 From: Mateus Xavier Date: Tue, 6 Dec 2022 16:43:54 -0300 Subject: [PATCH] Refactor ecdsa structure to suit starkbank's pattern --- CHANGELOG.md | 2 + README.md | 149 +- .../com/starkbank/ellipticcurve/Curve.java | 73 +- .../com/starkbank/ellipticcurve/Ecdsa.java | 34 +- .../com/starkbank/ellipticcurve/Math.java | 6 +- .../starkbank/ellipticcurve/PrivateKey.java | 173 +- .../starkbank/ellipticcurve/PublicKey.java | 191 +- .../starkbank/ellipticcurve/Signature.java | 120 +- .../starkbank/ellipticcurve/utils/Base64.java | 2065 ----------------- .../starkbank/ellipticcurve/utils/Binary.java | 110 + .../ellipticcurve/utils/BinaryAscii.java | 80 - .../ellipticcurve/utils/ByteString.java | 133 -- .../starkbank/ellipticcurve/utils/Der.java | 472 ++-- .../starkbank/ellipticcurve/utils/File.java | 4 +- .../starkbank/ellipticcurve/utils/Oid.java | 53 + .../starkbank/ellipticcurve/utils/Pem.java | 32 + .../ellipticcurve/utils/RandomInteger.java | 1 + .../ellipticcurve/CompPublicKeyTest.java | 49 + .../starkbank/ellipticcurve/CurveTest.java | 100 + .../starkbank/ellipticcurve/OpenSSLTest.java | 9 +- .../ellipticcurve/PrivateKeyTest.java | 9 +- .../ellipticcurve/PublicKeyTest.java | 9 +- .../ellipticcurve/SignatureTest.java | 9 +- .../SignatureWithRecoveryIdTest.java | 38 + .../com/starkbank/ellipticcurve/Utils.java | 6 +- 25 files changed, 966 insertions(+), 2961 deletions(-) delete mode 100644 src/main/java/com/starkbank/ellipticcurve/utils/Base64.java create mode 100644 src/main/java/com/starkbank/ellipticcurve/utils/Binary.java delete mode 100644 src/main/java/com/starkbank/ellipticcurve/utils/BinaryAscii.java delete mode 100644 src/main/java/com/starkbank/ellipticcurve/utils/ByteString.java create mode 100644 src/main/java/com/starkbank/ellipticcurve/utils/Oid.java create mode 100644 src/main/java/com/starkbank/ellipticcurve/utils/Pem.java create mode 100644 src/test/java/com/starkbank/ellipticcurve/CompPublicKeyTest.java create mode 100644 src/test/java/com/starkbank/ellipticcurve/CurveTest.java create mode 100644 src/test/java/com/starkbank/ellipticcurve/SignatureWithRecoveryIdTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a806988..bdc22be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Given a version number MAJOR.MINOR.PATCH, increment: ## [Unreleased] +### Changed +- internal structure to suit starkbank's pattern ### Fixed - groupId in pom.xml diff --git a/README.md b/README.md index 14a388d..7195a8b 100644 --- a/README.md +++ b/README.md @@ -24,20 +24,20 @@ mvn clean install ### Curves -We currently support `secp256k1`, but it's super easy to add more curves to the project. Just add them on `Curve.java` +We currently support `secp256k1`, but you can add more curves to the project. You just need to use the `Curve.add()` function. ### Speed -We ran a test on JDK 13.0.1 on a MAC Pro i5 2019. The libraries ran 100 times and showed the average times displayed bellow: +We ran a test on JDK 13.0.1 on a MAC Air M1 2020. The libraries ran 100 times and showed the average times displayed bellow: | Library | sign | verify | | ------------------ |:-------------:| -------:| | [java.security] | 0.9ms | 2.4ms | -| starkbank-ecdsa | 4.3ms | 9.9ms | +| starkbank-ecdsa | 2.5ms | 3.7ms | ### Sample Code -How to use it: +How to sign a json message for [Stark Bank]: ```java import com.starkbank.ellipticcurve.PrivateKey; @@ -46,26 +46,100 @@ import com.starkbank.ellipticcurve.Signature; import com.starkbank.ellipticcurve.Ecdsa; -public class GenerateKeys{ +// Generate privateKey from PEM string +PrivateKey privateKey = PrivateKey.fromPem("-----BEGIN EC PARAMETERS-----\nBgUrgQQACg==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIODvZuS34wFbt0X53+P5EnSj6tMjfVK01dD1dgDH02RzoAcGBSuBBAAK\noUQDQgAE/nvHu/SQQaos9TUljQsUuKI15Zr5SabPrbwtbfT/408rkVVzq8vAisbB\nRmpeRREXj5aog/Mq8RrdYy75W9q/Ig==\n-----END EC PRIVATE KEY-----"); - public static void main(String[] args){ - // Generate Keys - PrivateKey privateKey = new PrivateKey(); - PublicKey publicKey = privateKey.publicKey(); +// Create message from json +String message = "{'transfers':[{'amount':100000000,'taxId':'594.739.480-42','name':'Daenerys Targaryen Stormborn','bankCode':'341','branchCode':'2201','accountNumber':'76543-8','tags':['daenerys','targaryen','transfer-1-external-id']}]}'"; - String message = "Testing message"; - // Generate Signature - Signature signature = Ecdsa.sign(message, privateKey); +Signature signature = Ecdsa.sign(message, privateKey); - // Verify if signature is valid - boolean verified = Ecdsa.verify(message, signature, publicKey) ; +// Generate Signature in base64. This result can be sent to Stark Bank in the request header as the Digital-Signature parameter. +System.out.println(signature.toBase64()); - // Return the signature verification status - System.out.println("Verified: " + verified); +// To double check if the message matches the signature, do this: +PublicKey publicKey = privateKey.publicKey(); - } -} +System.out.println(Ecdsa.verify(message, signature, publicKey)); ``` + +Simple use: + +```java +import com.starkbank.ellipticcurve.PrivateKey; +import com.starkbank.ellipticcurve.Ecdsa; + + +// Generate new Keys +PrivateKey privateKey = new PrivateKey(); +PublicKey publicKey = privateKey.publicKey(); + +String message = "My test message"; + +// Generate Signature +Signature signature = Ecdsa.sign(message, privateKey); + +// To verify if the signature is valid +System.out.println(Ecdsa.verify(message, signature, publicKey)); + +``` + +How to add more curves: + +```java +import com.starkbank.ellipticcurve.PrivateKey; +import com.starkbank.ellipticcurve.PublicKey; +import com.starkbank.ellipticcurve.Curve; +import java.math.BigInteger; + + +Curve newCurve = new Curve( + new BigInteger("f1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c00", 16), + new BigInteger("ee353fca5428a9300d4aba754a44c00fdfec0c9ae4b1a1803075ed967b7bb73f", 16), + new BigInteger("f1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c03", 16), + new BigInteger("f1fd178c0b3ad58f10126de8ce42435b53dc67e140d2bf941ffdd459c6d655e1", 16), + new BigInteger("b6b3d4c356c139eb31183d4749d423958c27d2dcaf98b70164c97a2dd98f5cff", 16), + new BigInteger("6142e0f7c8b204911f9271f0f3ecef8c2701c307e8e4c9e183115a1554062cfb", 16), + "frp256v1", + new long[]{1, 2, 250, 1, 223, 101, 256, 1} +); + +Curve.add(newCurve); + +String publicKeyPem = "-----BEGIN PUBLIC KEY-----\nMFswFQYHKoZIzj0CAQYKKoF6AYFfZYIAAQNCAATeEFFYiQL+HmDYTf+QDmvQmWGD\ndRJPqLj11do8okvkSxq2lwB6Ct4aITMlCyg3f1msafc/ROSN/Vgj69bDhZK6\n-----END PUBLIC KEY-----"; + +PublicKey publicKey = PublicKey.fromPem(publicKeyPem); + +System.out.println(publicKey.toPem()); +``` + +How to generate compressed public key: + +```java +import com.starkbank.ellipticcurve.PrivateKey; +import com.starkbank.ellipticcurve.PublicKey; + + +PrivateKey privateKey = new PrivateKey(); +PublicKey publicKey = privateKey.publicKey(); +String compressedPublicKey = publicKey.toCompressed(); + +System.out.println(compressedPublicKey); +``` + +How to recover a compressed public key: + +```java +import com.starkbank.ellipticcurve.PrivateKey; +import com.starkbank.ellipticcurve.PublicKey; + + +String compressedPublicKey = "0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2"; +PublicKey publicKey = PublicKey.fromCompressed(compressedPublicKey); + +System.out.println(publicKey.toPem()); +``` + ### OpenSSL This library is compatible with OpenSSL, so you can use it to generate keys: @@ -91,24 +165,16 @@ import com.starkbank.ellipticcurve.utils.ByteString; import com.starkbank.ellipticcurve.utils.File; -public class VerifyKeys { - - public static void main(String[] args){ - // Read files - String publicKeyPem = File.read("publicKey.pem"); - byte[] signatureBin = File.readBytes("signatureBinary.txt"); - String message = File.read("message.txt"); +// Read files +String publicKeyPem = Utils.readFileAsString("publicKey.pem"); +byte[] signatureBin = Utils.readFileAsBytes("signature.binary"); +String message = Utils.readFileAsString("message.txt"); - ByteString byteString = new ByteString(signatureBin); +PublicKey publicKey = PublicKey.fromPem(publicKeyPem); +Signature signature = Signature.fromDer(signatureBin); - PublicKey publicKey = PublicKey.fromPem(publicKeyPem); - Signature signature = Signature.fromDer(byteString); - - // Get verification status: - boolean verified = Ecdsa.verify(message, signature, publicKey); - System.out.println("Verification status: " + verified); - } -} +// Get verification status: +System.out.println(Ecdsa.verify(message, signature, publicKey)); ``` You can also verify it on terminal: @@ -131,16 +197,11 @@ import com.starkbank.ellipticcurve.Signature; import com.starkbank.ellipticcurve.utils.File; -public class GenerateSignature { - - public static void main(String[] args) { - // Load signature file - byte[] signatureBin = File.readBytes("signatureBinary.txt"); - Signature signature = Signature.fromDer(new ByteString(signatureBin)); - // Print signature - System.out.println(signature.toBase64()); - } -} +// Load signature file +byte[] signatureBin = File.readBytes("signatureBinary.txt"); +Signature signature = Signature.fromDer(signatureBin); +// Print signature +System.out.println(signature.toBase64()); ``` [Stark Bank]: https://starkbank.com diff --git a/src/main/java/com/starkbank/ellipticcurve/Curve.java b/src/main/java/com/starkbank/ellipticcurve/Curve.java index 5d285c0..dc09388 100644 --- a/src/main/java/com/starkbank/ellipticcurve/Curve.java +++ b/src/main/java/com/starkbank/ellipticcurve/Curve.java @@ -2,12 +2,12 @@ import java.math.BigInteger; import java.util.*; + /** * Elliptic Curve Equation. * y^2 = x^3 + A*x + B (mod P) * */ - public class Curve { public BigInteger A; @@ -16,6 +16,7 @@ public class Curve { public BigInteger N; public Point G; public String name; + public String nistName; public long[] oid; /** @@ -27,18 +28,30 @@ public class Curve { * @param Gx Gx * @param Gy Gy * @param name name + * @param nistName nistName * @param oid oid */ - public Curve(BigInteger A, BigInteger B, BigInteger P, BigInteger N, BigInteger Gx, BigInteger Gy, String name, long[] oid) { + public Curve( + BigInteger A, BigInteger B, BigInteger P, BigInteger N, BigInteger Gx, + BigInteger Gy, String name, String nistName, long[] oid + ) { this.A = A; this.B = B; this.P = P; this.N = N; this.G = new Point(Gx, Gy); this.name = name; + this.nistName = nistName; this.oid = oid; } + public Curve( + BigInteger A, BigInteger B, BigInteger P, BigInteger N, BigInteger Gx, + BigInteger Gy, String name, long[] oid + ) { + this(A, B, P, N, Gx, Gy, name, null, oid); + } + /** * Verify if the point `p` is on the curve * @@ -69,9 +82,32 @@ public int length() { return (1 + N.toString(16).length()) / 2; } - /** - * - */ + public BigInteger y(BigInteger x, Boolean isEven) { + BigInteger ySquared = (x.modPow(BigInteger.valueOf(3), this.P).add(this.A.multiply(x)).add(this.B)).mod(this.P); + BigInteger y = Math.modularSquareRoot(ySquared, this.P); + if (isEven != y.mod(BigInteger.valueOf(2)).equals(BigInteger.ZERO)) { + return y = this.P.subtract(y); + } + return y; + } + + public static final Map curvesByOid = new HashMap(); + + public static void add(Curve curve) { + curvesByOid.put(Arrays.hashCode(curve.oid), curve); + } + + public static Curve getByOid(long[] oid) { + String[] names = new String[curvesByOid.size()]; + for (int i = 0; i < names.length; i++) { + names[i] = ((Curve) curvesByOid.values().toArray()[i]).name; + } + if(!curvesByOid.containsKey(Arrays.hashCode(oid))) { + throw new Error("Unknown curve with oid " + Arrays.toString(oid) + "; The following are registered: " + Arrays.toString(names)); + } + return (Curve) curvesByOid.get(Arrays.hashCode(oid)); + } + public static final Curve secp256k1 = new Curve( BigInteger.ZERO, BigInteger.valueOf(7), @@ -83,22 +119,21 @@ public int length() { new long[]{1, 3, 132, 0, 10} ); - /** - * - */ - public static final List supportedCurves = new ArrayList(); + public static final Curve prime256v1 = new Curve( + new BigInteger("ffffffff00000001000000000000000000000000fffffffffffffffffffffffc", 16), + new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16), + new BigInteger("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", 16), + new BigInteger("ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16), + new BigInteger("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", 16), + new BigInteger("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", 16), + "prime256v1", + new long[]{1, 2, 840, 10045, 3, 1, 7} + ); - /** - * - */ - public static final Map curvesByOid = new HashMap(); + public static final Curve p256 = prime256v1; static { - supportedCurves.add(secp256k1); - - for (Object c : supportedCurves) { - Curve curve = (Curve) c; - curvesByOid.put(Arrays.hashCode(curve.oid), curve); - } + add(secp256k1); + add(prime256v1); } } diff --git a/src/main/java/com/starkbank/ellipticcurve/Ecdsa.java b/src/main/java/com/starkbank/ellipticcurve/Ecdsa.java index a3cf289..233ec65 100644 --- a/src/main/java/com/starkbank/ellipticcurve/Ecdsa.java +++ b/src/main/java/com/starkbank/ellipticcurve/Ecdsa.java @@ -1,5 +1,5 @@ package com.starkbank.ellipticcurve; -import com.starkbank.ellipticcurve.utils.BinaryAscii; +import com.starkbank.ellipticcurve.utils.Binary; import com.starkbank.ellipticcurve.utils.RandomInteger; import java.math.BigInteger; import java.security.MessageDigest; @@ -15,16 +15,26 @@ public class Ecdsa { * @param hashfunc hashfunc * @return Signature */ - public static Signature sign(String message, PrivateKey privateKey, MessageDigest hashfunc) { byte[] hashMessage = hashfunc.digest(message.getBytes()); - BigInteger numberMessage = BinaryAscii.numberFromString(hashMessage); + BigInteger numberMessage = Binary.numberFromString(hashMessage); Curve curve = privateKey.curve; - BigInteger randNum = RandomInteger.between(BigInteger.ONE, curve.N); - Point randomSignPoint = Math.multiply(curve.G, randNum, curve.N, curve.A, curve.P); - BigInteger r = randomSignPoint.x.mod(curve.N); - BigInteger s = ((numberMessage.add(r.multiply(privateKey.secret))).multiply(Math.inv(randNum, curve.N))).mod(curve.N); - return new Signature(r, s); + + BigInteger r = BigInteger.ZERO; + BigInteger s = BigInteger.ZERO; + Point randomSignPoint = null; + while(r.equals(BigInteger.ZERO) || s.equals(BigInteger.ZERO)) { + BigInteger randNum = RandomInteger.between(BigInteger.ONE, curve.N.subtract(BigInteger.ONE)); + randomSignPoint = Math.multiply(curve.G, randNum, curve.N, curve.A, curve.P); + r = randomSignPoint.x.mod(curve.N); + s = ((numberMessage.add(r.multiply(privateKey.secret))).multiply(Math.inv(randNum, curve.N))).mod(curve.N); + } + BigInteger recoveryId = randomSignPoint.y.and(BigInteger.ONE); + if(randomSignPoint.y.compareTo(curve.N) > 0){ + recoveryId = recoveryId.add(BigInteger.valueOf(2)); + } + + return new Signature(r, s, recoveryId); } /** @@ -51,7 +61,7 @@ public static Signature sign(String message, PrivateKey privateKey) { */ public static boolean verify(String message, Signature signature, PublicKey publicKey, MessageDigest hashfunc) { byte[] hashMessage = hashfunc.digest(message.getBytes()); - BigInteger numberMessage = BinaryAscii.numberFromString(hashMessage); + BigInteger numberMessage = Binary.numberFromString(hashMessage); Curve curve = publicKey.curve; BigInteger r = signature.r; BigInteger s = signature.s; @@ -69,9 +79,9 @@ public static boolean verify(String message, Signature signature, PublicKey publ return false; } - BigInteger w = Math.inv(s, curve.N); - Point u1 =Math.multiply(curve.G, numberMessage.multiply(w).mod(curve.N), curve.N, curve.A, curve.P); - Point u2 = Math.multiply(publicKey.point, r.multiply(w).mod(curve.N), curve.N, curve.A, curve.P); + BigInteger inv = Math.inv(s, curve.N); + Point u1 = Math.multiply(curve.G, numberMessage.multiply(inv).mod(curve.N), curve.N, curve.A, curve.P); + Point u2 = Math.multiply(publicKey.point, r.multiply(inv).mod(curve.N), curve.N, curve.A, curve.P); Point v = Math.add(u1, u2, curve.A, curve.P); if (v.isAtInfinity()) { return false; diff --git a/src/main/java/com/starkbank/ellipticcurve/Math.java b/src/main/java/com/starkbank/ellipticcurve/Math.java index 8ae022a..61bbc88 100644 --- a/src/main/java/com/starkbank/ellipticcurve/Math.java +++ b/src/main/java/com/starkbank/ellipticcurve/Math.java @@ -4,6 +4,10 @@ public final class Math { + public static BigInteger modularSquareRoot(BigInteger value, BigInteger prime) { + return value.modPow((prime.add(BigInteger.ONE).divide(BigInteger.valueOf(4))), prime); + } + /** * Fast way to multiply point and scalar in elliptic curves * @@ -27,7 +31,6 @@ public static Point multiply(Point p, BigInteger n, BigInteger N, BigInteger A, * @param P Prime number in the module of the equation Y^2 = X^3 + A*X + B (mod P) * @return Point that represents the sum of First and Second Point */ - public static Point add(Point p, Point q, BigInteger A, BigInteger P) { return fromJacobian(jacobianAdd(toJacobian(p), toJacobian(q), A, P), P); } @@ -67,7 +70,6 @@ public static BigInteger inv(BigInteger x, BigInteger n) { * @return Point in Jacobian coordinates */ public static Point toJacobian(Point p) { - return new Point(p.x, p.y, BigInteger.ONE); } diff --git a/src/main/java/com/starkbank/ellipticcurve/PrivateKey.java b/src/main/java/com/starkbank/ellipticcurve/PrivateKey.java index 515d200..9036181 100644 --- a/src/main/java/com/starkbank/ellipticcurve/PrivateKey.java +++ b/src/main/java/com/starkbank/ellipticcurve/PrivateKey.java @@ -1,23 +1,32 @@ package com.starkbank.ellipticcurve; -import com.starkbank.ellipticcurve.utils.ByteString; import com.starkbank.ellipticcurve.utils.Der; -import com.starkbank.ellipticcurve.utils.BinaryAscii; +import com.starkbank.ellipticcurve.utils.Pem; +import com.starkbank.ellipticcurve.utils.Binary; import com.starkbank.ellipticcurve.utils.RandomInteger; +import com.starkbank.ellipticcurve.utils.Der.DerFieldType; import java.math.BigInteger; -import java.util.Arrays; public class PrivateKey { public Curve curve; public BigInteger secret; + private static final String pemTemplate = "-----BEGIN EC PRIVATE KEY-----\n%s-----END EC PRIVATE KEY-----"; /** * */ public PrivateKey() { this(Curve.secp256k1, null); - secret = RandomInteger.between(BigInteger.ONE, curve.N); + secret = RandomInteger.between(BigInteger.ONE, curve.N.subtract(BigInteger.ONE)); + } + + public PrivateKey(Curve curve) { + this(curve, RandomInteger.between(BigInteger.ONE, curve.N.subtract(BigInteger.ONE))); + } + + public PrivateKey(BigInteger secret) { + this(Curve.secp256k1, secret); } /** @@ -40,136 +49,60 @@ public PublicKey publicKey() { return new PublicKey(publicPoint, curve); } - /** - * - * @return ByteString - */ - public ByteString toByteString() { - return BinaryAscii.stringFromNumber(this.secret, this.curve.length()); + public String toString() { + return Binary.hexFromInt(secret); } - /** - * - * @return ByteString - */ - public ByteString toDer() { - ByteString encodedPublicKey = this.publicKey().toByteString(true); - return Der.encodeSequence( - Der.encodeInteger(BigInteger.valueOf(1)), - Der.encodeOctetString(this.toByteString()), - Der.encodeConstructed(0, Der.encodeOid(this.curve.oid)), - Der.encodeConstructed(1, Der.encodeBitString(encodedPublicKey))); + public byte[] toDer() { + String publicKeyString = this.publicKey().toString(true); + String hexadecimal = Der.encodeConstructed( + Der.encodePrimitive(DerFieldType.Integer, "1"), + Der.encodePrimitive(DerFieldType.OctetString, Binary.hexFromInt(this.secret)), + Der.encodePrimitive(DerFieldType.OidContainer, Der.encodePrimitive(DerFieldType.Object, this.curve.oid)), + Der.encodePrimitive(DerFieldType.PublicKeyPointContainer, Der.encodePrimitive(DerFieldType.BitString, publicKeyString)) + ); + return Binary.byteFromHex(hexadecimal); } - /** - * - * @return String - */ public String toPem() { - return Der.toPem(this.toDer(), "EC PRIVATE KEY"); + byte[] der = this.toDer(); + return Pem.createPem(Binary.base64FromByte(der), pemTemplate); } - - /** - * - * @param string string - * @return PrivateKey - */ - public static PrivateKey fromPem(String string) { - String privkeyPem = string.substring(string.indexOf("-----BEGIN EC PRIVATE KEY-----")); - return PrivateKey.fromDer(Der.fromPem(privkeyPem)); + public static PrivateKey fromPem(String pem) throws Exception { + String privateKeyPem = Pem.getPemContent(pem, pemTemplate); + byte[] der = Binary.byteFromBase64(privateKeyPem); + return fromDer(der); } - /** - * - * @param string string - * @return Privatekey - */ - public static PrivateKey fromDer(String string) { - return fromDer(new ByteString(string.getBytes())); - } - - /** - * - * @param string ByteString - * @return PrivateKey - */ - public static PrivateKey fromDer(ByteString string) { - ByteString[] str = Der.removeSequence(string); - ByteString s = str[0]; - ByteString empty = str[1]; - if (!empty.isEmpty()) { - throw new RuntimeException(String.format("trailing junk after DER privkey: %s", BinaryAscii.hexFromBinary(empty))); - } - - Object[] o = Der.removeInteger(s); - long one = Long.valueOf(o[0].toString()); - s = (ByteString) o[1]; - if (one != 1) { - throw new RuntimeException(String.format("expected '1' at start of DER privkey, got %d", one)); + public static PrivateKey fromDer(byte[] der) throws Exception { + String hexadecimal = Binary.hexFromByte(der); + Object[] parsed = (Object[]) Der.parse(hexadecimal)[0]; + int privateKeyFlag = Integer.parseInt(parsed[0].toString()); + Object[] parsedObject = (Object[]) parsed[1]; + String secretHex = parsedObject[0].toString(); + Object[] parsedObject1 = (Object[]) parsedObject[1]; + Object[] curveDataObject = (Object[]) parsedObject1[0]; + long[] curveData = Binary.longFromString(curveDataObject[0].toString()); + Object[] publicKeyStringObject = (Object[]) parsedObject1[1]; + String publicKeyString = publicKeyStringObject[0].toString().toLowerCase(); + if(privateKeyFlag != 1){ + throw new Exception("Private keys should start with a '1' flag, but a " + privateKeyFlag + " was found instead"); } - - str = Der.removeOctetString(s); - ByteString privkeyStr = str[0]; - s = str[1]; - Object[] t = Der.removeConstructed(s); - long tag = Long.valueOf(t[0].toString()); - ByteString curveOidStr = (ByteString) t[1]; - s = (ByteString) t[2]; - if (tag != 0) { - throw new RuntimeException(String.format("expected tag 0 in DER privkey, got %d", tag)); - } - - o = Der.removeObject(curveOidStr); - long[] oidCurve = (long[]) o[0]; - empty = (ByteString) o[1]; - if (!"".equals(empty.toString())) { - throw new RuntimeException(String.format("trailing junk after DER privkey curve_oid: %s", BinaryAscii.hexFromBinary(empty))); + Curve curve = Curve.getByOid(curveData); + PrivateKey privateKey = PrivateKey.fromString(secretHex, curve); + if(!privateKey.publicKey().toString(true).equals(publicKeyString)){ + throw new Exception("The public key described inside the private key file doesn't match the actual public key of the pair"); } - Curve curve = (Curve) Curve.curvesByOid.get(Arrays.hashCode(oidCurve)); - if (curve == null) { - throw new RuntimeException(String.format("Unknown curve with oid %s. I only know about these: %s", Arrays.toString(oidCurve), Arrays.toString(Curve.supportedCurves.toArray()))); - } - - if (privkeyStr.length() < curve.length()) { - int l = curve.length() - privkeyStr.length(); - byte[] bytes = new byte[l + privkeyStr.length()]; - for (int i = 0; i < curve.length() - privkeyStr.length(); i++) { - bytes[i] = 0; - } - byte[] privateKey = privkeyStr.getBytes(); - System.arraycopy(privateKey, 0, bytes, l, bytes.length - l); - privkeyStr = new ByteString(bytes); - } - - return PrivateKey.fromString(privkeyStr, curve); - } - - /** - * - * @param string byteString - * @param curve curve - * @return PrivateKey - */ - public static PrivateKey fromString(ByteString string, Curve curve) { - return new PrivateKey(curve, BinaryAscii.numberFromString(string.getBytes())); + return privateKey; } - /** - * - * @param string string - * @return PrivateKey - */ - public static PrivateKey fromString(String string) { - return fromString(new ByteString(string.getBytes())); + public static PrivateKey fromString(String string, Curve curve){ + return new PrivateKey(curve, Binary.intFromHex(string)); } - /** - * - * @param string byteString - * @return PrivateKey - */ - public static PrivateKey fromString(ByteString string) { - return PrivateKey.fromString(string, Curve.secp256k1); + public static PrivateKey fromString(String string){ + Curve curve = Curve.secp256k1; + return fromString(string, curve); } } diff --git a/src/main/java/com/starkbank/ellipticcurve/PublicKey.java b/src/main/java/com/starkbank/ellipticcurve/PublicKey.java index 1011680..eac4ee9 100644 --- a/src/main/java/com/starkbank/ellipticcurve/PublicKey.java +++ b/src/main/java/com/starkbank/ellipticcurve/PublicKey.java @@ -1,16 +1,21 @@ package com.starkbank.ellipticcurve; -import com.starkbank.ellipticcurve.utils.ByteString; import com.starkbank.ellipticcurve.utils.Der; -import com.starkbank.ellipticcurve.utils.BinaryAscii; -import java.util.Arrays; +import com.starkbank.ellipticcurve.utils.Pem; +import com.starkbank.ellipticcurve.utils.Der.DerFieldType; +import com.starkbank.ellipticcurve.utils.Binary; import static com.starkbank.ellipticcurve.Curve.secp256k1; -import static com.starkbank.ellipticcurve.Curve.supportedCurves; +import java.math.BigInteger; +import java.util.Arrays; public class PublicKey { public Point point; public Curve curve; + private static final String pemTemplate = "-----BEGIN PUBLIC KEY-----\n%s-----END PUBLIC KEY-----"; + private static final long[] ecdsaPublicKeyOid = {1, 2, 840, 10045, 2, 1}; + private static final String evenTag = "02"; + private static final String oddTag = "03"; /** * @@ -24,35 +29,51 @@ public PublicKey(Point point, Curve curve) { /** * - * @return ByteString + * @return String */ - public ByteString toByteString() { - return toByteString(false); + public String toString() { + return toString(false); } /** * * @param encoded encoded - * @return ByteString + * @return string */ - public ByteString toByteString(boolean encoded) { - ByteString xStr = BinaryAscii.stringFromNumber(point.x, curve.length()); - ByteString yStr = BinaryAscii.stringFromNumber(point.y, curve.length()); - xStr.insert(yStr.getBytes()); + public String toString(boolean encoded) { + int baseLength = 2 * this.curve.length(); + String xHex = Binary.padLeftZeros(Binary.hexFromInt(this.point.x), baseLength); + String yHex = Binary.padLeftZeros(Binary.hexFromInt(this.point.y), baseLength); + String string = xHex + yHex; if(encoded) { - xStr.insert(0, new byte[]{0, 4} ); + return "0004" + string; } - return xStr; + return string; + } + + public String toCompressed() { + int baseLength = 2 * this.curve.length(); + String parityTag = oddTag; + if (point.y.mod(BigInteger.valueOf(2)).equals(BigInteger.ZERO)) { + parityTag = evenTag; + } + String xHex = Binary.padLeftZeros(Binary.hexFromInt(point.x), baseLength); + return parityTag + xHex; } /** * - * @return ByteString + * @return string */ - public ByteString toDer() { - long[] oidEcPublicKey = new long[]{1, 2, 840, 10045, 2, 1}; - ByteString encodeEcAndOid = Der.encodeSequence(Der.encodeOid(oidEcPublicKey), Der.encodeOid(curve.oid)); - return Der.encodeSequence(encodeEcAndOid, Der.encodeBitString(this.toByteString(true))); + public byte[] toDer() { + String hexadecimal = Der.encodeConstructed( + Der.encodeConstructed( + Der.encodePrimitive(DerFieldType.Object, ecdsaPublicKeyOid), + Der.encodePrimitive(DerFieldType.Object, this.curve.oid) + ), + Der.encodePrimitive(DerFieldType.BitString, this.toString(true)) + ); + return Binary.byteFromHex(hexadecimal); } /** @@ -60,113 +81,79 @@ public ByteString toDer() { * @return String */ public String toPem() { - return Der.toPem(this.toDer(), "PUBLIC KEY"); + byte[] der = this.toDer(); + return Pem.createPem(Binary.base64FromByte(der), pemTemplate); } - /** - * - * @param string string - * @return PublicKey - */ - public static PublicKey fromPem(String string) { - return PublicKey.fromDer(Der.fromPem(string)); + public static PublicKey fromPem(String string) throws Exception{ + String publicKeyPem = Pem.getPemContent(string, pemTemplate); + return fromDer(Binary.byteFromBase64(publicKeyPem)); } - /** - * - * @param string byteString - * @return PublicKey - */ - public static PublicKey fromDer(ByteString string) { - ByteString[] str = Der.removeSequence(string); - ByteString s1 = str[0]; - ByteString empty = str[1]; - if (!empty.isEmpty()) { - throw new RuntimeException (String.format("trailing junk after DER pubkey: %s", BinaryAscii.hexFromBinary(empty))); - } - str = Der.removeSequence(s1); - ByteString s2 = str[0]; - ByteString pointStrBitstring = str[1]; - Object[] o = Der.removeObject(s2); - ByteString rest = (ByteString) o[1]; - o = Der.removeObject(rest); - long[] oidCurve = (long[]) o[0]; - empty = (ByteString) o[1]; - if (!empty.isEmpty()) { - throw new RuntimeException (String.format("trailing junk after DER pubkey objects: %s", BinaryAscii.hexFromBinary(empty))); + public static PublicKey fromDer(byte[] der) throws Exception{ + String hexadecimal = Binary.hexFromByte(der); + Object[] parsed = (Object[]) Der.parse(hexadecimal)[0]; + Object[] curveData = (Object[]) parsed[0]; + String pointString = parsed[1].toString(); + long[] publicKeyOid = Binary.longFromString(curveData[0].toString()); + Object[] curveOidObject = (Object[]) curveData[1]; + long[] curveOid = Binary.longFromString(curveOidObject[0].toString()); + + if(!Arrays.equals(publicKeyOid, ecdsaPublicKeyOid)) { + throw new Exception("The Public Key Object Identifier (OID) should be " + Arrays.toString(ecdsaPublicKeyOid) + ", but " + Arrays.toString(publicKeyOid) + " was found instead"); } + Curve curve = Curve.getByOid(curveOid); + return fromString((String) pointString, curve); + } - Curve curve = (Curve) Curve.curvesByOid.get(Arrays.hashCode(oidCurve)); - if (curve == null) { - throw new RuntimeException(String.format("Unknown curve with oid %s. I only know about these: %s", Arrays.toString(oidCurve), Arrays.toString(supportedCurves.toArray()))); - } + public static PublicKey fromString(String string) { + return fromString(string, secp256k1); + } - str = Der.removeBitString(pointStrBitstring); - ByteString pointStr = str[0]; - empty = str[1]; - if (!empty.isEmpty()) { - throw new RuntimeException (String.format("trailing junk after pubkey pointstring: %s", BinaryAscii.hexFromBinary(empty))); - } - return PublicKey.fromString(pointStr.substring(2), curve); + public static PublicKey fromString(String string, Curve curve){ + return fromString(string, curve, false); } - /** - * - * @param string byteString - * @param curve curve - * @param validatePoint validatePoint - * @return PublicKey - */ - public static PublicKey fromString(ByteString string, Curve curve, boolean validatePoint) { - int baselen = curve.length(); + public static PublicKey fromString(String string, Curve curve, Boolean ValidatePoint) { + int baseLength = 2 * curve.length(); + if (string.length() > 2 * baseLength && string.substring(0, 4).equals("0004")) { + string = string.substring(4); + } - ByteString xs = string.substring(0, baselen); - ByteString ys = string.substring(baselen); + String xs = string.substring(0, baseLength); + String ys = string.substring(baseLength); - Point p = new Point(BinaryAscii.numberFromString(xs.getBytes()), BinaryAscii.numberFromString(ys.getBytes())); + Point p = new Point( + Binary.intFromHex(xs), + Binary.intFromHex(ys) + ); PublicKey publicKey = new PublicKey(p, curve); - if (!validatePoint) { + if (!ValidatePoint) { return publicKey; } - if (p.isAtInfinity()) { + if(p.isAtInfinity()){ throw new RuntimeException("Public Key point is at infinity"); } if (!curve.contains(p)) { - throw new RuntimeException(String.format("Point (%s,%s) is not valid for curve %s", p.x, p.y, curve.name)); + throw new RuntimeException("Point (" + p.x + "," + p.y + " is not valid for curve " + curve.name); } - if (!Math.multiply(p, curve.N, curve.N, curve.A, curve.P).isAtInfinity()) { - throw new RuntimeException(String.format("Point (%s,%s) * %s.N is not at infinity", p.x, p.y, curve.name)); + if(!Math.multiply(p, curve.N, curve.N, curve.A, curve.P).isAtInfinity()){ + throw new RuntimeException("Point (" + p.x + "," + p.y + ") * " + curve.name + ".N is not infinity"); } return publicKey; } - /** - * - * @param string byteString - * @param curve curve - * @return PublicKey - */ - public static PublicKey fromString(ByteString string, Curve curve) { - return fromString(string, curve, true); + public static PublicKey fromCompressed(String string) { + return fromCompressed(string, secp256k1); } - /** - * - * @param string byteString - * @param validatePoint validatePoint - * @return PublicKey - */ - public static PublicKey fromString(ByteString string, boolean validatePoint) { - return fromString(string, secp256k1, validatePoint); - } - - /** - * - * @param string byteString - * @return PublicKey - */ - public static PublicKey fromString(ByteString string) { - return fromString(string, true); + public static PublicKey fromCompressed(String string, Curve curve) { + String parityTag = string.substring(0, 2); + String xHex = string.substring(2); + BigInteger x = Binary.intFromHex(xHex); + BigInteger y = curve.y(x, parityTag.equals(evenTag)); + Point p = new Point(x, y); + return new PublicKey(p, curve); } } diff --git a/src/main/java/com/starkbank/ellipticcurve/Signature.java b/src/main/java/com/starkbank/ellipticcurve/Signature.java index 1ede9f2..c71cfcf 100644 --- a/src/main/java/com/starkbank/ellipticcurve/Signature.java +++ b/src/main/java/com/starkbank/ellipticcurve/Signature.java @@ -1,79 +1,99 @@ package com.starkbank.ellipticcurve; -import com.starkbank.ellipticcurve.utils.Base64; -import com.starkbank.ellipticcurve.utils.BinaryAscii; -import com.starkbank.ellipticcurve.utils.ByteString; +import com.starkbank.ellipticcurve.utils.Binary; import com.starkbank.ellipticcurve.utils.Der; -import java.io.IOException; +import com.starkbank.ellipticcurve.utils.Der.DerFieldType; import java.math.BigInteger; +import java.util.Arrays; public class Signature { public BigInteger r; public BigInteger s; + public BigInteger recoveryId; /** * * @param r r * @param s s */ - public Signature(BigInteger r, BigInteger s) { + public Signature(BigInteger r, BigInteger s, BigInteger recoveryId) { this.r = r; this.s = s; + this.recoveryId = recoveryId; } - /** - * - * @return ByteString - */ - public ByteString toDer() { - return Der.encodeSequence(Der.encodeInteger(r), Der.encodeInteger(s)); + public Signature(BigInteger r, BigInteger s) { + this(r, s, null); + } + + public byte[] toDer(Boolean withRecoveryId) { + String hexadecimal = this._toString(); + byte[] encodedSequence = Binary.byteFromHex(hexadecimal); + if(!withRecoveryId) return encodedSequence; + + byte[] finalEncodedSequence = new byte[encodedSequence.length + 1]; + finalEncodedSequence[0] = (byte) (27 + this.recoveryId.intValue()); + for (int i = 0; i < encodedSequence.length; i++) { + finalEncodedSequence[i + 1] = encodedSequence[i]; + } + return finalEncodedSequence; + } + + public byte[] toDer() { + return this.toDer(false); + } + + public String toBase64(Boolean withRecoveryId) { + return Binary.base64FromByte(this.toDer(withRecoveryId)); } - /** - * - * @return String - */ public String toBase64() { - return Base64.encodeBytes(toDer().getBytes()); + return this.toBase64(false); } - /** - * - * @param string byteString - * @return Signature - */ - public static Signature fromDer(ByteString string) { - ByteString[] str = Der.removeSequence(string); - ByteString rs = str[0]; - ByteString empty = str[1]; - if (!empty.isEmpty()) { - throw new RuntimeException(String.format("trailing junk after DER sig: %s", BinaryAscii.hexFromBinary(empty))); - } - Object[] o = Der.removeInteger(rs); - BigInteger r = new BigInteger(o[0].toString()); - ByteString rest = (ByteString) o[1]; - o = Der.removeInteger(rest); - BigInteger s = new BigInteger(o[0].toString()); - empty = (ByteString) o[1]; - if (!empty.isEmpty()) { - throw new RuntimeException(String.format("trailing junk after DER numbers: %s", BinaryAscii.hexFromBinary(empty))); + public static Signature fromDer(byte[] der, Boolean recoveryByte) throws Exception { + BigInteger recoveryId = null; + if (recoveryByte) { + recoveryId = BigInteger.valueOf(der[0]); + recoveryId = recoveryId.subtract(BigInteger.valueOf(27)); + der = Arrays.copyOfRange(der, 1, der.length); } - return new Signature(r, s); + + String hexadecimal = Binary.hexFromByte(der); + return Signature._fromString(hexadecimal, recoveryId); } - /** - * - * @param string byteString - * @return Signature - */ - public static Signature fromBase64(ByteString string) { - ByteString der = null; - try { - der = new ByteString(Base64.decode(string.getBytes())); - } catch (IOException e) { - throw new IllegalArgumentException("Corrupted base64 string! Could not decode base64 from it"); - } - return fromDer(der); + public static Signature fromDer(byte[] der) throws Exception { + return Signature.fromDer(der, false); + } + + public static Signature fromBase64(String string, Boolean recoveryByte) throws Exception { + byte[] der = Binary.byteFromBase64(string); + return Signature.fromDer(der, recoveryByte); } + + public static Signature fromBase64(String string) throws Exception { + return Signature.fromBase64(string, false); + } + + public String _toString() { + return Der.encodeConstructed( + Der.encodePrimitive(DerFieldType.Integer, this.r.toString()), + Der.encodePrimitive(DerFieldType.Integer, this.s.toString()) + ); + } + + public static Signature _fromString(String string, BigInteger recoveryId) throws Exception { + Object[] parsed = (Object[]) Der.parse(string)[0]; + BigInteger r = new BigInteger(parsed[0].toString()); + Object[] parsedS = (Object[]) parsed[1]; + BigInteger s = new BigInteger(parsedS[0].toString()); + return new Signature(r, s, recoveryId); + } + + public static Signature _fromString(String string) throws Exception { + return Signature._fromString(string, null); + } + } diff --git a/src/main/java/com/starkbank/ellipticcurve/utils/Base64.java b/src/main/java/com/starkbank/ellipticcurve/utils/Base64.java deleted file mode 100644 index 6debc0d..0000000 --- a/src/main/java/com/starkbank/ellipticcurve/utils/Base64.java +++ /dev/null @@ -1,2065 +0,0 @@ -package com.starkbank.ellipticcurve.utils; - -/** - *

Encodes and decodes to and from Base64 notation.

- *

Homepage: http://iharder.net/base64.

- * - *

Example:

- * - * String encoded = Base64.encode( myByteArray ); - * byte[] myByteArray = Base64.decode( encoded ); - * - *

The options parameter, which appears in a few places, is used to pass - * several pieces of information to the encoder. In the "higher level" methods such as - * encodeBytes( bytes, options ) the options parameter can be used to indicate such - * things as first gzipping the bytes before encoding them, not inserting linefeeds, - * and encoding using the URL-safe and Ordered dialects.

- * - *

Note, according to RFC3548, - * Section 2.1, implementations should not add line feeds unless explicitly told - * to do so. I've got Base64 set to this behavior now, although earlier versions - * broke lines by default.

- * - *

The constants defined in Base64 can be OR-ed together to combine options, so you - * might make a call like this:

- * - * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); - *

to compress the data before encoding it and then making the output have newline characters.

- *

Also...

- * String encoded = Base64.encodeBytes( crazyString.getBytes() ); - * - * - * - *

- * Change Log: - *

- *
    - *
  • v2.3.7 - Fixed subtle bug when base 64 input stream contained the - * value 01111111, which is an invalid base 64 character but should not - * throw an ArrayIndexOutOfBoundsException either. Led to discovery of - * mishandling (or potential for better handling) of other bad input - * characters. You should now get an IOException if you try decoding - * something that has bad characters in it.
  • - *
  • v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded - * string ended in the last column; the buffer was not properly shrunk and - * contained an extra (null) byte that made it into the string.
  • - *
  • v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size - * was wrong for files of size 31, 34, and 37 bytes.
  • - *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing - * the Base64.OutputStream closed the Base64 encoding (by padding with equals - * signs) too soon. Also added an option to suppress the automatic decoding - * of gzipped streams. Also added experimental support for specifying a - * class loader when using the - * {@link #decodeToObject(String, int, ClassLoader)} - * method.
  • - *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java - * footprint with its CharEncoders and so forth. Fixed some javadocs that were - * inconsistent. Removed imports and specified things like java.io.IOException - * explicitly inline.
  • - *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the - * final encoded data will be so that the code doesn't have to create two output - * arrays: an oversized initial one and then a final, exact-sized one. Big win - * when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not - * using the gzip options which uses a different mechanism with streams and stuff).
  • - *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some - * similar helper methods to be more efficient with memory by not returning a - * String but just a byte array.
  • - *
  • v2.3 - This is not a drop-in replacement! This is two years of comments - * and bug fixes queued up and finally executed. Thanks to everyone who sent - * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else. - * Much bad coding was cleaned up including throwing exceptions where necessary - * instead of returning null values or something similar. Here are some changes - * that may affect you: - *
      - *
    • Does not break lines, by default. This is to keep in compliance with - * RFC3548.
    • - *
    • Throws exceptions instead of returning null values. Because some operations - * (especially those that may permit the GZIP option) use IO streams, there - * is a possiblity of an java.io.IOException being thrown. After some discussion and - * thought, I've changed the behavior of the methods to throw java.io.IOExceptions - * rather than return null if ever there's an error. I think this is more - * appropriate, though it will require some changes to your code. Sorry, - * it should have been done this way to begin with.
    • - *
    • Removed all references to System.out, System.err, and the like. - * Shame on me. All I can say is sorry they were ever there.
    • - *
    • Throws NullPointerExceptions and IllegalArgumentExceptions as needed - * such as when passed arrays are null or offsets are invalid.
    • - *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. - * This was especially annoying before for people who were thorough in their - * own projects and then had gobs of javadoc warnings on this file.
    • - *
    - *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug - * when using very small files (~< 40 bytes).
  • - *
  • v2.2 - Added some helper methods for encoding/decoding directly from - * one file to the next. Also added a main() method to support command line - * encoding/decoding from one file to the next. Also added these Base64 dialects: - *
      - *
    1. The default is RFC3548 format.
    2. - *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates - * URL and file name friendly format as described in Section 4 of RFC3548. - * http://www.faqs.org/rfcs/rfc3548.html
    4. - *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates - * URL and file name friendly format that preserves lexical ordering as described - * in http://www.faqs.org/qa/rfcc-1940.html
    6. - *
    - * Special thanks to Jim Kellerman at http://www.powerset.com/ - * for contributing the new Base64 dialects. - *
  • - * - *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added - * some convenience methods for reading and writing to and from files.
  • - *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems - * with other encodings (like EBCDIC).
  • - *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the - * encoded data was a single byte.
  • - *
  • v2.0 - I got rid of methods that used booleans to set options. - * Now everything is more consolidated and cleaner. The code now detects - * when data that's being decoded is gzip-compressed and will decompress it - * automatically. Generally things are cleaner. You'll probably have to - * change some method calls that you were making to support the new - * options format (ints that you "OR" together).
  • - *
  • v1.5.1 - Fixed bug when decompressing and decoding to a - * byte[] using decode( String s, boolean gzipCompressed ). - * Added the ability to "suspend" encoding in the Output Stream so - * you can turn on and off the encoding if you need to embed base64 - * data in an otherwise "normal" stream (like an XML file).
  • - *
  • v1.5 - Output stream pases on flush() command but doesn't do anything itself. - * This helps when using GZIP streams. - * Added the ability to GZip-compress objects before encoding them.
  • - *
  • v1.4 - Added helper methods to read/write files.
  • - *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • - *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream - * where last buffer being read, if not completely full, was not returned.
  • - *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • - *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • - *
- * - *

- * I am placing this code in the Public Domain. Do with it as you will. - * This software comes with no guarantees or warranties but with - * plenty of well-wishing instead! - * Please visit http://iharder.net/base64 - * periodically to check for updates or to contribute improvements. - *

- * - * @author Robert Harder - * @author rob@iharder.net - * @version 2.3.7 - */ -public class Base64 -{ - -/* ******** P U B L I C F I E L D S ******** */ - - - /** No options specified. Value is zero. */ - public final static int NO_OPTIONS = 0; - - /** Specify encoding in first bit. Value is one. */ - public final static int ENCODE = 1; - - - /** Specify decoding in first bit. Value is zero. */ - public final static int DECODE = 0; - - - /** Specify that data should be gzip-compressed in second bit. Value is two. */ - public final static int GZIP = 2; - - /** Specify that gzipped data should not be automatically gunzipped. */ - public final static int DONT_GUNZIP = 4; - - - /** Do break lines when encoding. Value is 8. */ - public final static int DO_BREAK_LINES = 8; - - /** - * Encode using Base64-like encoding that is URL- and Filename-safe as described - * in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * It is important to note that data encoded this way is not officially valid Base64, - * or at the very least should not be called Base64 without also specifying that is - * was encoded using the URL- and Filename-safe dialect. - */ - public final static int URL_SAFE = 16; - - - /** - * Encode using the special "ordered" dialect of Base64 described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - public final static int ORDERED = 32; - - -/* ******** P R I V A T E F I E L D S ******** */ - - - /** Maximum line length (76) of Base64 output. */ - private final static int MAX_LINE_LENGTH = 76; - - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte)'='; - - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte)'\n'; - - - /** Preferred encoding. */ - private final static String PREFERRED_ENCODING = "US-ASCII"; - - - private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding - private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding - - -/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ - - /** The 64 valid Base64 values. */ - /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ - private final static byte[] _STANDARD_ALPHABET = { - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', - (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' - }; - - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - **/ - private final static byte[] _STANDARD_DECODABET = { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9,-9,-9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' - 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' - -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 - 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' - 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' - -9,-9,-9,-9,-9 // Decimal 123 - 127 - ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 - }; - - -/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ - - /** - * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." - */ - private final static byte[] _URL_SAFE_ALPHABET = { - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', - (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_' - }; - - /** - * Used in decoding URL- and Filename-safe dialects of Base64. - */ - private final static byte[] _URL_SAFE_DECODABET = { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 62, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' - 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' - -9,-9,-9,-9, // Decimal 91 - 94 - 63, // Underscore at decimal 95 - -9, // Decimal 96 - 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' - 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' - -9,-9,-9,-9,-9 // Decimal 123 - 127 - ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 - }; - - - -/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ - - /** - * I don't get the point of this technique, but someone requested it, - * and it is described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - private final static byte[] _ORDERED_ALPHABET = { - (byte)'-', - (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', - (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', - (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', - (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', - (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', - (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', - (byte)'_', - (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', - (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', - (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', - (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z' - }; - - /** - * Used in decoding the "ordered" dialect of Base64. - */ - private final static byte[] _ORDERED_DECODABET = { - -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 - -5,-5, // Whitespace: Tab and Linefeed - -9,-9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 - -9,-9,-9,-9,-9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 0, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine - -9,-9,-9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9,-9,-9, // Decimal 62 - 64 - 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M' - 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z' - -9,-9,-9,-9, // Decimal 91 - 94 - 37, // Underscore at decimal 95 - -9, // Decimal 96 - 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm' - 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z' - -9,-9,-9,-9,-9 // Decimal 123 - 127 - ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 - }; - - -/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ - - - /** - * Returns one of the _SOMETHING_ALPHABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URLSAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getAlphabet( int options ) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_ALPHABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_ALPHABET; - } else { - return _STANDARD_ALPHABET; - } - } // end getAlphabet - - - /** - * Returns one of the _SOMETHING_DECODABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URL_SAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getDecodabet( int options ) { - if( (options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_DECODABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_DECODABET; - } else { - return _STANDARD_DECODABET; - } - } // end getAlphabet - - - - /** Defeats instantiation. */ - public Base64(){} - - - - -/* ******** E N C O D I N G M E T H O D S ******** */ - - - /** - * Encodes up to the first three bytes of array threeBytes - * and returns a four-byte array in Base64 notation. - * The actual number of significant bytes in your array is - * given by numSigBytes. - * The array threeBytes needs only be as big as - * numSigBytes. - * Code can reuse a byte array by passing a four-byte array as b4. - * - * @param b4 A reusable byte array to reduce array instantiation - * @param threeBytes the array to convert - * @param numSigBytes the number of significant bytes in your array - * @return four byte array in Base64 notation. - * @since 1.5.1 - */ - private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options ) { - encode3to4( threeBytes, 0, numSigBytes, b4, 0, options ); - return b4; - } // end encode3to4 - - - /** - *

Encodes up to three bytes of the array source - * and writes the resulting four Base64 bytes to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 3 for - * the source array or destOffset + 4 for - * the destination array. - * The actual number of significant bytes in your array is - * given by numSigBytes.

- *

This is the lowest level of the encoding methods with - * all possible parameters.

- * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @return the destination array - * @since 1.3 - */ - private static byte[] encode3to4( - byte[] source, int srcOffset, int numSigBytes, - byte[] destination, int destOffset, int options ) { - - byte[] ALPHABET = getAlphabet( options ); - - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index ALPHABET - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) - | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) - | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); - - switch( numSigBytes ) - { - case 3: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; - destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; - return destination; - - case 2: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; - destination[ destOffset + 3 ] = EQUALS_SIGN; - return destination; - - case 1: - destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; - destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; - destination[ destOffset + 2 ] = EQUALS_SIGN; - destination[ destOffset + 3 ] = EQUALS_SIGN; - return destination; - - default: - return destination; - } // end switch - } // end encode3to4 - - - - /** - * Performs Base64 encoding on the raw ByteBuffer, - * writing it to the encoded ByteBuffer. - * This is an experimental feature. Currently it does not - * pass along any options (such as {@link #DO_BREAK_LINES} - * or {@link #GZIP}. - * - * @param raw input buffer - * @param encoded output buffer - * @since 2.3 - */ - public static void encode( java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded ){ - byte[] raw3 = new byte[3]; - byte[] enc4 = new byte[4]; - - while( raw.hasRemaining() ){ - int rem = java.lang.Math.min(3,raw.remaining()); - raw.get(raw3,0,rem); - Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); - encoded.put(enc4); - } // end input remaining - } - - - /** - * Performs Base64 encoding on the raw ByteBuffer, - * writing it to the encoded CharBuffer. - * This is an experimental feature. Currently it does not - * pass along any options (such as {@link #DO_BREAK_LINES} - * or {@link #GZIP}. - * - * @param raw input buffer - * @param encoded output buffer - * @since 2.3 - */ - public static void encode( java.nio.ByteBuffer raw, java.nio.CharBuffer encoded ){ - byte[] raw3 = new byte[3]; - byte[] enc4 = new byte[4]; - - while( raw.hasRemaining() ){ - int rem = java.lang.Math.min(3,raw.remaining()); - raw.get(raw3,0,rem); - Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); - for( int i = 0; i < 4; i++ ){ - encoded.put( (char)(enc4[i] & 0xFF) ); - } - } // end input remaining - } - - - - - /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. - * - *

As of v 2.3, if the object - * cannot be serialized or there is another error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * The object is not GZip-compressed before being encoded. - * - * @param serializableObject The object to encode - * @return The Base64-encoded object - * @throws java.io.IOException if there is an error - * @throws NullPointerException if serializedObject is null - * @since 1.4 - */ - public static String encodeObject( java.io.Serializable serializableObject ) - throws java.io.IOException { - return encodeObject( serializableObject, NO_OPTIONS ); - } // end encodeObject - - - - /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. - * - *

As of v 2.3, if the object - * cannot be serialized or there is another error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * The object is not GZip-compressed before being encoded. - *

- * Example options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     * 
- *

- * Example: encodeObject( myObj, Base64.GZIP ) or - *

- * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * @param serializableObject The object to encode - * @param options Specified options - * @return The Base64-encoded object - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException if there is an error - * @since 2.0 - */ - public static String encodeObject( java.io.Serializable serializableObject, int options ) - throws java.io.IOException { - - if( serializableObject == null ){ - throw new NullPointerException( "Cannot serialize a null object." ); - } // end if: null - - // Streams - java.io.ByteArrayOutputStream baos = null; - java.io.OutputStream b64os = null; - java.util.zip.GZIPOutputStream gzos = null; - java.io.ObjectOutputStream oos = null; - - - try { - // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream - baos = new java.io.ByteArrayOutputStream(); - b64os = new OutputStream( baos, ENCODE | options ); - if( (options & GZIP) != 0 ){ - // Gzip - gzos = new java.util.zip.GZIPOutputStream(b64os); - oos = new java.io.ObjectOutputStream( gzos ); - } else { - // Not gzipped - oos = new java.io.ObjectOutputStream( b64os ); - } - oos.writeObject( serializableObject ); - } // end try - catch( java.io.IOException e ) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try{ oos.close(); } catch( Exception e ){} - try{ gzos.close(); } catch( Exception e ){} - try{ b64os.close(); } catch( Exception e ){} - try{ baos.close(); } catch( Exception e ){} - } // end finally - - // Return value according to relevant encoding. - try { - return new String( baos.toByteArray(), PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue){ - // Fall back to some Java default - return new String( baos.toByteArray() ); - } // end catch - - } // end encode - - - - /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. - * - * @param source The data to convert - * @return The data in Base64-encoded form - * @throws NullPointerException if source array is null - * @since 1.4 - */ - public static String encodeBytes( byte[] source ) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - - - /** - * Encodes a byte array into Base64 notation. - *

- * Example options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     *     Note: Technically, this makes your encoding non-compliant.
-     * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * - *

As of v 2.3, if there is an error with the GZIP stream, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * - * @param source The data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @since 2.0 - */ - public static String encodeBytes( byte[] source, int options ) throws java.io.IOException { - return encodeBytes( source, 0, source.length, options ); - } // end encodeBytes - - - /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. - * - *

As of v 2.3, if there is an error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @return The Base64-encoded data as a String - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @since 1.4 - */ - public static String encodeBytes( byte[] source, int off, int len ) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes( source, off, len, NO_OPTIONS ); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - - - /** - * Encodes a byte array into Base64 notation. - *

- * Example options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     *     Note: Technically, this makes your encoding non-compliant.
-     * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * - *

As of v 2.3, if there is an error with the GZIP stream, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @since 2.0 - */ - public static String encodeBytes( byte[] source, int off, int len, int options ) throws java.io.IOException { - byte[] encoded = encodeBytesToBytes( source, off, len, options ); - - // Return value according to relevant encoding. - try { - return new String( encoded, PREFERRED_ENCODING ); - } // end try - catch (java.io.UnsupportedEncodingException uue) { - return new String( encoded ); - } // end catch - - } // end encodeBytes - - - - - /** - * Similar to {@link #encodeBytes(byte[])} but returns - * a byte array instead of instantiating a String. This is more efficient - * if you're working with I/O streams and have large data sets to encode. - * - * - * @param source The data to convert - * @return The Base64-encoded data as a byte[] (of ASCII characters) - * @throws NullPointerException if source array is null - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes( byte[] source ) { - byte[] encoded = null; - try { - encoded = encodeBytesToBytes( source, 0, source.length, Base64.NO_OPTIONS ); - } catch( java.io.IOException ex ) { - assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); - } - return encoded; - } - - - /** - * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns - * a byte array instead of instantiating a String. This is more efficient - * if you're working with I/O streams and have large data sets to encode. - * - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int options ) throws java.io.IOException { - - if( source == null ){ - throw new NullPointerException( "Cannot serialize a null array." ); - } // end if: null - - if( off < 0 ){ - throw new IllegalArgumentException( "Cannot have negative offset: " + off ); - } // end if: off < 0 - - if( len < 0 ){ - throw new IllegalArgumentException( "Cannot have length offset: " + len ); - } // end if: len < 0 - - if( off + len > source.length ){ - throw new IllegalArgumentException( - String.format( "Cannot have offset of %d and length of %d with array of length %d", off,len,source.length)); - } // end if: off < 0 - - - - // Compress? - if( (options & GZIP) != 0 ) { - java.io.ByteArrayOutputStream baos = null; - java.util.zip.GZIPOutputStream gzos = null; - OutputStream b64os = null; - - try { - // GZip -> Base64 -> ByteArray - baos = new java.io.ByteArrayOutputStream(); - b64os = new OutputStream( baos, ENCODE | options ); - gzos = new java.util.zip.GZIPOutputStream( b64os ); - - gzos.write( source, off, len ); - gzos.close(); - } // end try - catch( java.io.IOException e ) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try{ gzos.close(); } catch( Exception e ){} - try{ b64os.close(); } catch( Exception e ){} - try{ baos.close(); } catch( Exception e ){} - } // end finally - - return baos.toByteArray(); - } // end if: compress - - // Else, don't compress. Better not to use streams at all then. - else { - boolean breakLines = (options & DO_BREAK_LINES) != 0; - - //int len43 = len * 4 / 3; - //byte[] outBuff = new byte[ ( len43 ) // Main 4:3 - // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding - // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines - // Try to determine more precisely how big the array needs to be. - // If we get it right, we don't have to do an array copy, and - // we save a bunch of memory. - int encLen = ( len / 3 ) * 4 + ( len % 3 > 0 ? 4 : 0 ); // Bytes needed for actual encoding - if( breakLines ){ - encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters - } - byte[] outBuff = new byte[ encLen ]; - - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for( ; d < len2; d+=3, e+=4 ) { - encode3to4( source, d+off, 3, outBuff, e, options ); - - lineLength += 4; - if( breakLines && lineLength >= MAX_LINE_LENGTH ) - { - outBuff[e+4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // en dfor: each piece of array - - if( d < len ) { - encode3to4( source, d+off, len - d, outBuff, e, options ); - e += 4; - } // end if: some padding needed - - - // Only resize array if we didn't guess it right. - if( e <= outBuff.length - 1 ){ - // If breaking lines and the last byte falls right at - // the line length (76 bytes per line), there will be - // one extra byte, and the array will need to be resized. - // Not too bad of an estimate on array size, I'd say. - byte[] finalOut = new byte[e]; - System.arraycopy(outBuff,0, finalOut,0,e); - //System.err.println("Having to resize array from " + outBuff.length + " to " + e ); - return finalOut; - } else { - //System.err.println("No need to resize array."); - return outBuff; - } - - } // end else: don't compress - - } // end encodeBytesToBytes - - - - - -/* ******** D E C O D I N G M E T H O D S ******** */ - - - /** - * Decodes four bytes from array source - * and writes the resulting bytes (up to three of them) - * to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 4 for - * the source array or destOffset + 3 for - * the destination array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - *

This is the lowest level of the decoding methods with - * all possible parameters.

- * - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param options alphabet type is pulled from this (standard, url-safe, ordered) - * @return the number of decoded bytes converted - * @throws NullPointerException if source or destination arrays are null - * @throws IllegalArgumentException if srcOffset or destOffset are invalid - * or there is not enough room in the array. - * @since 1.3 - */ - private static int decode4to3( - byte[] source, int srcOffset, - byte[] destination, int destOffset, int options ) { - - // Lots of error checking and exception throwing - if( source == null ){ - throw new NullPointerException( "Source array was null." ); - } // end if - if( destination == null ){ - throw new NullPointerException( "Destination array was null." ); - } // end if - if( srcOffset < 0 || srcOffset + 3 >= source.length ){ - throw new IllegalArgumentException( String.format( - "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset ) ); - } // end if - if( destOffset < 0 || destOffset +2 >= destination.length ){ - throw new IllegalArgumentException( String.format( - "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset ) ); - } // end if - - - byte[] DECODABET = getDecodabet( options ); - - // Example: Dk== - if( source[ srcOffset + 2] == EQUALS_SIGN ) { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 ); - - destination[ destOffset ] = (byte)( outBuff >>> 16 ); - return 1; - } - - // Example: DkL= - else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) - | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 ); - - destination[ destOffset ] = (byte)( outBuff >>> 16 ); - destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 ); - return 2; - } - - // Example: DkLE - else { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) - // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); - int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) - | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) - | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6) - | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) ); - - - destination[ destOffset ] = (byte)( outBuff >> 16 ); - destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); - destination[ destOffset + 2 ] = (byte)( outBuff ); - - return 3; - } - } // end decodeToBytes - - - - - - /** - * Low-level access to decoding ASCII characters in - * the form of a byte array. Ignores GUNZIP option, if - * it's set. This is not generally a recommended method, - * although it is used internally as part of the decoding process. - * Special case: if len = 0, an empty array is returned. Still, - * if you need more speed and reduced memory footprint (and aren't - * gzipping), consider this method. - * - * @param source The Base64 encoded data - * @return decoded data - * @throws java.io.IOException java.io.IOException - * @since 2.3.1 - */ - public static byte[] decode( byte[] source ) - throws java.io.IOException { - byte[] decoded = null; -// try { - decoded = decode( source, 0, source.length, Base64.NO_OPTIONS ); -// } catch( java.io.IOException ex ) { -// assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); -// } - return decoded; - } - - - - /** - * Low-level access to decoding ASCII characters in - * the form of a byte array. Ignores GUNZIP option, if - * it's set. This is not generally a recommended method, - * although it is used internally as part of the decoding process. - * Special case: if len = 0, an empty array is returned. Still, - * if you need more speed and reduced memory footprint (and aren't - * gzipping), consider this method. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @param options Can specify options such as alphabet type to use - * @return decoded data - * @throws java.io.IOException If bogus characters exist in source data - * @since 1.3 - */ - public static byte[] decode( byte[] source, int off, int len, int options ) - throws java.io.IOException { - - // Lots of error checking and exception throwing - if( source == null ){ - throw new NullPointerException( "Cannot decode null source array." ); - } // end if - if( off < 0 || off + len > source.length ){ - throw new IllegalArgumentException( String.format( - "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len ) ); - } // end if - - if( len == 0 ){ - return new byte[0]; - }else if( len < 4 ){ - throw new IllegalArgumentException( - "Base64-encoded string must have at least four characters, but length specified was " + len ); - } // end if - - byte[] DECODABET = getDecodabet( options ); - - int len34 = len * 3 / 4; // Estimate on array size - byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output - int outBuffPosn = 0; // Keep track of where we're writing - - byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space - int b4Posn = 0; // Keep track of four byte input buffer - int i = 0; // Source array counter - byte sbiDecode = 0; // Special value from DECODABET - - for( i = off; i < off+len; i++ ) { // Loop through source - - sbiDecode = DECODABET[ source[i]&0xFF ]; - - // White space, Equals sign, or legit Base64 character - // Note the values such as -5 and -9 in the - // DECODABETs at the top of the file. - if( sbiDecode >= WHITE_SPACE_ENC ) { - if( sbiDecode >= EQUALS_SIGN_ENC ) { - b4[ b4Posn++ ] = source[i]; // Save non-whitespace - if( b4Posn > 3 ) { // Time to decode? - outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options ); - b4Posn = 0; - - // If that was the equals sign, break out of 'for' loop - if( source[i] == EQUALS_SIGN ) { - break; - } // end if: equals sign - } // end if: quartet built - } // end if: equals sign or better - } // end if: white space, equals sign or better - else { - // There's a bad input character in the Base64 stream. - throw new java.io.IOException( String.format( - "Bad Base64 input character decimal %d in array position %d", ((int)source[i])&0xFF, i ) ); - } // end else: - } // each input character - - byte[] out = new byte[ outBuffPosn ]; - System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); - return out; - } // end decode - - - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @return the decoded data - * @throws java.io.IOException If there is a problem - * @since 1.4 - */ - public static byte[] decode( String s ) throws java.io.IOException { - return decode( s, NO_OPTIONS ); - } - - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @param options encode options such as URL_SAFE - * @return the decoded data - * @throws java.io.IOException if there is an error - * @throws NullPointerException if s is null - * @since 1.4 - */ - public static byte[] decode( String s, int options ) throws java.io.IOException { - - if( s == null ){ - throw new NullPointerException( "Input string was null." ); - } // end if - - byte[] bytes; - try { - bytes = s.getBytes( PREFERRED_ENCODING ); - } // end try - catch( java.io.UnsupportedEncodingException uee ) { - bytes = s.getBytes(); - } // end catch - // - - // Decode - bytes = decode( bytes, 0, bytes.length, options ); - - // Check to see if it's gzip-compressed - // GZIP Magic Two-Byte Number: 0x8b1f (35615) - boolean dontGunzip = (options & DONT_GUNZIP) != 0; - if( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) { - - int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); - if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) { - java.io.ByteArrayInputStream bais = null; - java.util.zip.GZIPInputStream gzis = null; - java.io.ByteArrayOutputStream baos = null; - byte[] buffer = new byte[2048]; - int length = 0; - - try { - baos = new java.io.ByteArrayOutputStream(); - bais = new java.io.ByteArrayInputStream( bytes ); - gzis = new java.util.zip.GZIPInputStream( bais ); - - while( ( length = gzis.read( buffer ) ) >= 0 ) { - baos.write(buffer,0,length); - } // end while: reading input - - // No error? Get new bytes. - bytes = baos.toByteArray(); - - } // end try - catch( java.io.IOException e ) { - e.printStackTrace(); - // Just return originally-decoded bytes - } // end catch - finally { - try{ baos.close(); } catch( Exception e ){} - try{ gzis.close(); } catch( Exception e ){} - try{ bais.close(); } catch( Exception e ){} - } // end finally - - } // end if: gzipped - } // end if: bytes.length >= 2 - - return bytes; - } // end decode - - - - /** - * Attempts to decode Base64 data and deserialize a Java - * Object within. Returns null if there was an error. - * - * @param encodedObject The Base64 data to decode - * @return The decoded and deserialized object - * @throws NullPointerException if encodedObject is null - * @throws java.io.IOException if there is a general error - * @throws ClassNotFoundException if the decoded object is of a - * class that cannot be found by the JVM - * @since 1.5 - */ - public static Object decodeToObject( String encodedObject ) - throws java.io.IOException, ClassNotFoundException { - return decodeToObject(encodedObject,NO_OPTIONS,null); - } - - - /** - * Attempts to decode Base64 data and deserialize a Java - * Object within. Returns null if there was an error. - * If loader is not null, it will be the class loader - * used when deserializing. - * - * @param encodedObject The Base64 data to decode - * @param options Various parameters related to decoding - * @param loader Optional class loader to use in deserializing classes. - * @return The decoded and deserialized object - * @throws NullPointerException if encodedObject is null - * @throws java.io.IOException if there is a general error - * @throws ClassNotFoundException if the decoded object is of a - * class that cannot be found by the JVM - * @since 2.3.4 - */ - public static Object decodeToObject( - String encodedObject, int options, final ClassLoader loader ) - throws java.io.IOException, ClassNotFoundException { - - // Decode and gunzip if necessary - byte[] objBytes = decode( encodedObject, options ); - - java.io.ByteArrayInputStream bais = null; - java.io.ObjectInputStream ois = null; - Object obj = null; - - try { - bais = new java.io.ByteArrayInputStream( objBytes ); - - // If no custom class loader is provided, use Java's builtin OIS. - if( loader == null ){ - ois = new java.io.ObjectInputStream( bais ); - } // end if: no loader provided - - // Else make a customized object input stream that uses - // the provided class loader. - else { - ois = new java.io.ObjectInputStream(bais){ - @Override - public Class resolveClass(java.io.ObjectStreamClass streamClass) - throws java.io.IOException, ClassNotFoundException { - Class c = Class.forName(streamClass.getName(), false, loader); - if( c == null ){ - return super.resolveClass(streamClass); - } else { - return c; // Class loader knows of this class. - } // end else: not null - } // end resolveClass - }; // end ois - } // end else: no custom class loader - - obj = ois.readObject(); - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and throw in order to execute finally{} - } // end catch - catch( ClassNotFoundException e ) { - throw e; // Catch and throw in order to execute finally{} - } // end catch - finally { - try{ bais.close(); } catch( Exception e ){} - try{ ois.close(); } catch( Exception e ){} - } // end finally - - return obj; - } // end decodeObject - - - - /** - * Convenience method for encoding data to a file. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param dataToEncode byte array of data to encode in base64 form - * @param filename Filename for saving encoded data - * @throws java.io.IOException if there is an error - * @throws NullPointerException if dataToEncode is null - * @since 2.1 - */ - public static void encodeToFile( byte[] dataToEncode, String filename ) - throws java.io.IOException { - - if( dataToEncode == null ){ - throw new NullPointerException( "Data to encode was null." ); - } // end iff - - OutputStream bos = null; - try { - bos = new OutputStream( - new java.io.FileOutputStream( filename ), Base64.ENCODE ); - bos.write( dataToEncode ); - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and throw to execute finally{} block - } // end catch: java.io.IOException - finally { - try{ bos.close(); } catch( Exception e ){} - } // end finally - - } // end encodeToFile - - - /** - * Convenience method for decoding data to a file. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param dataToDecode Base64-encoded data as a string - * @param filename Filename for saving decoded data - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static void decodeToFile( String dataToDecode, String filename ) - throws java.io.IOException { - - OutputStream bos = null; - try{ - bos = new OutputStream( - new java.io.FileOutputStream( filename ), Base64.DECODE ); - bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) ); - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and throw to execute finally{} block - } // end catch: java.io.IOException - finally { - try{ bos.close(); } catch( Exception e ){} - } // end finally - - } // end decodeToFile - - - - - /** - * Convenience method for reading a base64-encoded - * file and decoding it. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param filename Filename for reading encoded data - * @return decoded byte array - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static byte[] decodeFromFile( String filename ) - throws java.io.IOException { - - byte[] decodedData = null; - InputStream bis = null; - try - { - // Set up some useful variables - java.io.File file = new java.io.File( filename ); - byte[] buffer = null; - int length = 0; - int numBytes = 0; - - // Check for size of file - if( file.length() > Integer.MAX_VALUE ) - { - throw new java.io.IOException( "File is too big for this convenience method (" + file.length() + " bytes)." ); - } // end if: file too big for int index - buffer = new byte[ (int)file.length() ]; - - // Open a stream - bis = new InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( file ) ), Base64.DECODE ); - - // Read until done - while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) { - length += numBytes; - } // end while - - // Save in a variable to return - decodedData = new byte[ length ]; - System.arraycopy( buffer, 0, decodedData, 0, length ); - - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and release to execute finally{} - } // end catch: java.io.IOException - finally { - try{ bis.close(); } catch( Exception e) {} - } // end finally - - return decodedData; - } // end decodeFromFile - - - - /** - * Convenience method for reading a binary file - * and base64-encoding it. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param filename Filename for reading binary data - * @return base64-encoded string - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static String encodeFromFile( String filename ) - throws java.io.IOException { - - String encodedData = null; - InputStream bis = null; - try - { - // Set up some useful variables - java.io.File file = new java.io.File( filename ); - byte[] buffer = new byte[ java.lang.Math.max((int)(file.length() * 1.4+1),40) ]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5) - int length = 0; - int numBytes = 0; - - // Open a stream - bis = new InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream( file ) ), Base64.ENCODE ); - - // Read until done - while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) { - length += numBytes; - } // end while - - // Save in a variable to return - encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING ); - - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and release to execute finally{} - } // end catch: java.io.IOException - finally { - try{ bis.close(); } catch( Exception e) {} - } // end finally - - return encodedData; - } // end encodeFromFile - - /** - * Reads infile and encodes it to outfile. - * - * @param infile Input file - * @param outfile Output file - * @throws java.io.IOException if there is an error - * @since 2.2 - */ - public static void encodeFileToFile( String infile, String outfile ) - throws java.io.IOException { - - String encoded = Base64.encodeFromFile( infile ); - java.io.OutputStream out = null; - try{ - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream( outfile ) ); - out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output. - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and release to execute finally{} - } // end catch - finally { - try { out.close(); } - catch( Exception ex ){} - } // end finally - } // end encodeFileToFile - - - /** - * Reads infile and decodes it to outfile. - * - * @param infile Input file - * @param outfile Output file - * @throws java.io.IOException if there is an error - * @since 2.2 - */ - public static void decodeFileToFile( String infile, String outfile ) - throws java.io.IOException { - - byte[] decoded = Base64.decodeFromFile( infile ); - java.io.OutputStream out = null; - try{ - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream( outfile ) ); - out.write( decoded ); - } // end try - catch( java.io.IOException e ) { - throw e; // Catch and release to execute finally{} - } // end catch - finally { - try { out.close(); } - catch( Exception ex ){} - } // end finally - } // end decodeFileToFile - - - /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ - - - - /** - * A {@link InputStream} will read data from another - * java.io.InputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class InputStream extends java.io.FilterInputStream { - - private boolean encode; // Encoding or decoding - private int position; // Current position in the buffer - private byte[] buffer; // Small buffer holding converted data - private int bufferLength; // Length of buffer (3 or 4) - private int numSigBytes; // Number of meaningful bytes in the buffer - private int lineLength; - private boolean breakLines; // Break lines at less than 80 characters - private int options; // Record options used to create the stream. - private byte[] decodabet; // Local copies to avoid extra method calls - - - /** - * Constructs a {@link InputStream} in DECODE mode. - * - * @param in the java.io.InputStream from which to read data. - * @since 1.3 - */ - public InputStream( java.io.InputStream in ) { - this( in, DECODE ); - } // end constructor - - - /** - * Constructs a {@link InputStream} in - * either ENCODE or DECODE mode. - *

- * Valid options:

-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DO_BREAK_LINES: break lines at 76 characters
-         *     (only meaningful when encoding)
-         * 
- *

- * Example: new Base64.InputStream( in, Base64.DECODE ) - * - * - * @param in the java.io.InputStream from which to read data. - * @param options Specified options - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public InputStream( java.io.InputStream in, int options ) { - - super( in ); - this.options = options; // Record for later - this.breakLines = (options & DO_BREAK_LINES) > 0; - this.encode = (options & ENCODE) > 0; - this.bufferLength = encode ? 4 : 3; - this.buffer = new byte[ bufferLength ]; - this.position = -1; - this.lineLength = 0; - this.decodabet = getDecodabet(options); - } // end constructor - - /** - * Reads enough of the input stream to convert - * to/from Base64 and returns the next byte. - * - * @return next byte - * @since 1.3 - */ - @Override - public int read() throws java.io.IOException { - - // Do we need to get data? - if( position < 0 ) { - if( encode ) { - byte[] b3 = new byte[3]; - int numBinaryBytes = 0; - for( int i = 0; i < 3; i++ ) { - int b = in.read(); - - // If end of stream, b is -1. - if( b >= 0 ) { - b3[i] = (byte)b; - numBinaryBytes++; - } else { - break; // out of for loop - } // end else: end of stream - - } // end for: each needed input byte - - if( numBinaryBytes > 0 ) { - encode3to4( b3, 0, numBinaryBytes, buffer, 0, options ); - position = 0; - numSigBytes = 4; - } // end if: got data - else { - return -1; // Must be end of stream - } // end else - } // end if: encoding - - // Else decoding - else { - byte[] b4 = new byte[4]; - int i = 0; - for( i = 0; i < 4; i++ ) { - // Read four "meaningful" bytes: - int b = 0; - do{ b = in.read(); } - while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC ); - - if( b < 0 ) { - break; // Reads a -1 if end of stream - } // end if: end of stream - - b4[i] = (byte)b; - } // end for: each needed input byte - - if( i == 4 ) { - numSigBytes = decode4to3( b4, 0, buffer, 0, options ); - position = 0; - } // end if: got four characters - else if( i == 0 ){ - return -1; - } // end else if: also padded correctly - else { - // Must have broken out from above. - throw new java.io.IOException( "Improperly padded Base64 input." ); - } // end - - } // end else: decode - } // end else: get data - - // Got data? - if( position >= 0 ) { - // End of relevant data? - if( /*!encode &&*/ position >= numSigBytes ){ - return -1; - } // end if: got data - - if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) { - lineLength = 0; - return '\n'; - } // end if - else { - lineLength++; // This isn't important when decoding - // but throwing an extra "if" seems - // just as wasteful. - - int b = buffer[ position++ ]; - - if( position >= bufferLength ) { - position = -1; - } // end if: end - - return b & 0xFF; // This is how you "cast" a byte that's - // intended to be unsigned. - } // end else - } // end if: position >= 0 - - // Else error - else { - throw new java.io.IOException( "Error in Base64 code reading stream." ); - } // end else - } // end read - - - /** - * Calls {@link #read()} repeatedly until the end of stream - * is reached or len bytes are read. - * Returns number of bytes read into array or -1 if - * end of stream is encountered. - * - * @param dest array to hold values - * @param off offset for array - * @param len max number of bytes to read into array - * @return bytes read into array or -1 if end of stream is encountered. - * @since 1.3 - */ - @Override - public int read( byte[] dest, int off, int len ) - throws java.io.IOException { - int i; - int b; - for( i = 0; i < len; i++ ) { - b = read(); - - if( b >= 0 ) { - dest[off + i] = (byte) b; - } - else if( i == 0 ) { - return -1; - } - else { - break; // Out of 'for' loop - } // Out of 'for' loop - } // end for: each byte read - return i; - } // end read - - } // end inner class InputStream - - - - - - - /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ - - - - /** - * A {@link OutputStream} will write data to another - * java.io.OutputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class OutputStream extends java.io.FilterOutputStream { - - private boolean encode; - private int position; - private byte[] buffer; - private int bufferLength; - private int lineLength; - private boolean breakLines; - private byte[] b4; // Scratch used in a few places - private boolean suspendEncoding; - private int options; // Record for later - private byte[] decodabet; // Local copies to avoid extra method calls - - /** - * Constructs a {@link OutputStream} in ENCODE mode. - * - * @param out the java.io.OutputStream to which data will be written. - * @since 1.3 - */ - public OutputStream( java.io.OutputStream out ) { - this( out, ENCODE ); - } // end constructor - - - /** - * Constructs a {@link OutputStream} in - * either ENCODE or DECODE mode. - *

- * Valid options:

-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DO_BREAK_LINES: don't break lines at 76 characters
-         *     (only meaningful when encoding)
-         * 
- *

- * Example: new Base64.OutputStream( out, Base64.ENCODE ) - * - * @param out the java.io.OutputStream to which data will be written. - * @param options Specified options. - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DO_BREAK_LINES - * @since 1.3 - */ - public OutputStream( java.io.OutputStream out, int options ) { - super( out ); - this.breakLines = (options & DO_BREAK_LINES) != 0; - this.encode = (options & ENCODE) != 0; - this.bufferLength = encode ? 3 : 4; - this.buffer = new byte[ bufferLength ]; - this.position = 0; - this.lineLength = 0; - this.suspendEncoding = false; - this.b4 = new byte[4]; - this.options = options; - this.decodabet = getDecodabet(options); - } // end constructor - - - /** - * Writes the byte to the output stream after - * converting to/from Base64 notation. - * When encoding, bytes are buffered three - * at a time before the output stream actually - * gets a write() call. - * When decoding, bytes are buffered four - * at a time. - * - * @param theByte the byte to write - * @since 1.3 - */ - @Override - public void write(int theByte) - throws java.io.IOException { - // Encoding suspended? - if( suspendEncoding ) { - this.out.write( theByte ); - return; - } // end if: supsended - - // Encode? - if( encode ) { - buffer[ position++ ] = (byte)theByte; - if( position >= bufferLength ) { // Enough to encode. - - this.out.write( encode3to4( b4, buffer, bufferLength, options ) ); - - lineLength += 4; - if( breakLines && lineLength >= MAX_LINE_LENGTH ) { - this.out.write( NEW_LINE ); - lineLength = 0; - } // end if: end of line - - position = 0; - } // end if: enough to output - } // end if: encoding - - // Else, Decoding - else { - // Meaningful Base64 character? - if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) { - buffer[ position++ ] = (byte)theByte; - if( position >= bufferLength ) { // Enough to output. - - int len = Base64.decode4to3( buffer, 0, b4, 0, options ); - out.write( b4, 0, len ); - position = 0; - } // end if: enough to output - } // end if: meaningful base64 character - else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) { - throw new java.io.IOException( "Invalid character in Base64 data." ); - } // end else: not white space either - } // end else: decoding - } // end write - - - - /** - * Calls {@link #write(int)} repeatedly until len - * bytes are written. - * - * @param theBytes array from which to read bytes - * @param off offset for array - * @param len max number of bytes to read into array - * @since 1.3 - */ - @Override - public void write( byte[] theBytes, int off, int len ) - throws java.io.IOException { - // Encoding suspended? - if( suspendEncoding ) { - this.out.write( theBytes, off, len ); - return; - } // end if: supsended - - for( int i = 0; i < len; i++ ) { - write( theBytes[ off + i ] ); - } // end for: each byte written - - } // end write - - - - /** - * Method added by PHIL. [Thanks, PHIL. -Rob] - * This pads the buffer without closing the stream. - * @throws java.io.IOException if there's an error. - */ - public void flushBase64() throws java.io.IOException { - if( position > 0 ) { - if( encode ) { - out.write( encode3to4( b4, buffer, position, options ) ); - position = 0; - } // end if: encoding - else { - throw new java.io.IOException( "Base64 input not properly padded." ); - } // end else: decoding - } // end if: buffer partially full - - } // end flush - - - /** - * Flushes and closes (I think, in the superclass) the stream. - * - * @since 1.3 - */ - @Override - public void close() throws java.io.IOException { - // 1. Ensure that pending characters are written - flushBase64(); - - // 2. Actually close the stream - // Base class both flushes and closes. - super.close(); - - buffer = null; - out = null; - } // end close - - - - /** - * Suspends encoding of the stream. - * May be helpful if you need to embed a piece of - * base64-encoded data in a stream. - * - * @throws java.io.IOException if there's an error flushing - * @since 1.5.1 - */ - public void suspendEncoding() throws java.io.IOException { - flushBase64(); - this.suspendEncoding = true; - } // end suspendEncoding - - - /** - * Resumes encoding of the stream. - * May be helpful if you need to embed a piece of - * base64-encoded data in a stream. - * - * @since 1.5.1 - */ - public void resumeEncoding() { - this.suspendEncoding = false; - } // end resumeEncoding - - - - } // end inner class OutputStream - - -} // end class Base64 diff --git a/src/main/java/com/starkbank/ellipticcurve/utils/Binary.java b/src/main/java/com/starkbank/ellipticcurve/utils/Binary.java new file mode 100644 index 0000000..58111ab --- /dev/null +++ b/src/main/java/com/starkbank/ellipticcurve/utils/Binary.java @@ -0,0 +1,110 @@ +package com.starkbank.ellipticcurve.utils; +import java.math.BigInteger; +import java.util.Base64; + +public class Binary { + + static public String hexFromInt(BigInteger number) { + String hexadecimal = number.toString(16); + if (hexadecimal.length() % 2 != 0) { + hexadecimal = "0" + hexadecimal; + } + return hexadecimal; + } + + static public String hexFromInt(int number) { + String hexadecimal = Integer.toHexString(number); + if (hexadecimal.length() % 2 != 0) { + hexadecimal = "0" + hexadecimal; + } + return hexadecimal; + } + + static public String hexFromInt(long number) { + return hexFromInt((int) number); + } + + static public BigInteger intFromHex(String hexadecimal) { + return new BigInteger(hexadecimal, 16); + } + + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + static public String hexFromByte(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; j++ ) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + static public byte[] byteFromHex(String hexadecimal) { + int hexLength = hexadecimal.length(); + if(hexLength % 2 != 0) { + throw new IllegalArgumentException("Hexadecimal string must have an even number of characters"); + } + + byte[] retBuf = new byte[hexLength / 2]; + + for (int i = 0; i < hexLength; i += 2) { + int top = Character.digit(hexadecimal.charAt(i), 16); + int bottom = Character.digit(hexadecimal.charAt(i + 1), 16); + if (top == -1 || bottom == -1) { + throw new IllegalArgumentException("Hexadecimal string contains non-hexadecimal characters"); + } + retBuf[i / 2] = (byte) ((top << 4) + bottom); + } + + return retBuf; + } + + static public String base64FromByte(byte[] byteString) { + Base64.Encoder encoder = Base64.getEncoder(); + return encoder.encodeToString(byteString); + } + + static public byte[] byteFromBase64(String base64) { + Base64.Decoder decoder = Base64.getDecoder(); + return decoder.decode(base64); + } + + static public String bitsFromHex(String hex) { + String binary = intFromHex(hex).toString(2); + binary = padLeftZeros(binary, 4 * hex.length()); + return binary; + } + + static public String bitsFromHex(char hex) { + String binary = intFromHex(String.valueOf(hex)).toString(2); + binary = padLeftZeros(binary, 4 * String.valueOf(hex).length()); + return binary; + } + + static public String padLeftZeros(String inputString, int length) { + if (inputString.length() >= length) { + return inputString; + } + StringBuilder sb = new StringBuilder(); + while (sb.length() < length - inputString.length()) { + sb.append('0'); + } + sb.append(inputString); + + return sb.toString(); + } + + public static long[] longFromString(String s) { + String[] test = s.substring(s.indexOf("[") + 1, s.indexOf("]")).split(", "); + long[] oid = new long[test.length]; + for (int i = 0; i < test.length; i++) { + oid[i] = Long.parseLong(test[i]); + } + return oid; + } + + public static BigInteger numberFromString(byte[] string) { + return new BigInteger(Binary.hexFromByte(string), 16); + } +} diff --git a/src/main/java/com/starkbank/ellipticcurve/utils/BinaryAscii.java b/src/main/java/com/starkbank/ellipticcurve/utils/BinaryAscii.java deleted file mode 100644 index a67c704..0000000 --- a/src/main/java/com/starkbank/ellipticcurve/utils/BinaryAscii.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.starkbank.ellipticcurve.utils; -import java.math.BigInteger; -import java.util.Arrays; - - -public final class BinaryAscii { - - /** - * - * @param string byteString - * @return String - */ - public static String hexFromBinary(ByteString string) { - return hexFromBinary(string.getBytes()); - } - - /** - * - * @param bytes byte[] - * @return String - */ - public static String hexFromBinary(byte[] bytes) { - StringBuilder hexString = new StringBuilder(); - - for (byte aByte : bytes) { - String hex = Integer.toHexString(0xFF & aByte); - if (hex.length() == 1) { - hexString.append('0'); - } - hexString.append(hex); - } - return hexString.toString(); - } - - /** - * - * @param string string - * @return byte[] - */ - public static byte[] binaryFromHex(String string) { - byte[] bytes = new BigInteger(string, 16).toByteArray(); - int i = 0; - while (i < bytes.length && bytes[i] == 0) { - i++; - } - return Arrays.copyOfRange(bytes, i, bytes.length); - } - - /** - * - * @param c c - * @return byte[] - */ - public static byte[] toBytes(int c) { - return new byte[]{(byte) c}; - } - - /** - * Get a number representation of a string - * - * @param string String to be converted in a number - * @return Number in hex from string - */ - public static BigInteger numberFromString(byte[] string) { - return new BigInteger(BinaryAscii.hexFromBinary(string), 16); - } - - /** - * Get a string representation of a number - * - * @param number number to be converted in a string - * @param length length max number of character for the string - * @return hexadecimal string - */ - public static ByteString stringFromNumber(BigInteger number, int length) { - String fmtStr = "%0" + String.valueOf(2 * length) + "x"; - String hexString = String.format(fmtStr, number); - return new ByteString(BinaryAscii.binaryFromHex(hexString)); - } -} diff --git a/src/main/java/com/starkbank/ellipticcurve/utils/ByteString.java b/src/main/java/com/starkbank/ellipticcurve/utils/ByteString.java deleted file mode 100644 index f32dafc..0000000 --- a/src/main/java/com/starkbank/ellipticcurve/utils/ByteString.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.starkbank.ellipticcurve.utils; -import java.io.UnsupportedEncodingException; -import java.util.Arrays; - - -public class ByteString { - private byte[] bytes; - - /** - * - */ - public ByteString() { - bytes = new byte[]{}; - } - - /** - * - * @param bytes byte[] - */ - public ByteString(byte[] bytes) { - this.bytes = bytes; - } - - /** - * - * @param index index - * @return short - */ - public short getShort(int index) { - return (short) (bytes[index] & 0xFF); - } - - /** - * - * @param start start - * @return ByteString - */ - public ByteString substring(int start) { - return substring(start, bytes.length); - } - - /** - * - * @param start start - * @param end end - * @return ByteString - */ - public ByteString substring(int start, int end) { - if (end > bytes.length) { - end = bytes.length; - } - if (end < 0) { - end = bytes.length - end; - } - if (start > end) { - return new ByteString(); - } - - return new ByteString(Arrays.copyOfRange(bytes, start, end)); - } - - /** - * - * @return byte[] - */ - public byte[] getBytes() { - return Arrays.copyOf(bytes, bytes.length); - } - - /** - * - * @return int - */ - public int length() { - return bytes.length; - } - - /** - * - * @return boolean - */ - public boolean isEmpty() { - return bytes.length == 0; - } - - /** - * - * @param b b - */ - public void insert(byte[] b) { - this.insert(bytes.length, b); - } - - /** - * - * @param index index - * @param b b - */ - public void insert(int index, byte[] b) { - byte[] result = new byte[b.length + bytes.length]; - System.arraycopy(bytes, 0, result, 0, index); - System.arraycopy(b, 0, result, index, b.length); - if (index < bytes.length) { - System.arraycopy(bytes, index, result, b.length + index, bytes.length - index); - } - this.bytes = result; - } - - /** - * - * @param index index - * @param value value - */ - public void replace(int index, byte value) { - bytes[index] = value; - } - - /** - * - * @return string - */ - @Override - public String toString() { - if (bytes.length == 0) { - return ""; - } - try { - return new String(bytes, "ASCII"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(); - } - } -} diff --git a/src/main/java/com/starkbank/ellipticcurve/utils/Der.java b/src/main/java/com/starkbank/ellipticcurve/utils/Der.java index fd28684..ad1e743 100644 --- a/src/main/java/com/starkbank/ellipticcurve/utils/Der.java +++ b/src/main/java/com/starkbank/ellipticcurve/utils/Der.java @@ -1,11 +1,9 @@ package com.starkbank.ellipticcurve.utils; - -import java.io.IOException; import java.math.BigInteger; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; - -import static com.starkbank.ellipticcurve.utils.BinaryAscii.*; +import java.util.Objects; public class Der { @@ -14,340 +12,198 @@ private Der() { throw new UnsupportedOperationException("Der is a utility class and cannot be instantiated"); } - /** - * - * @param encodedPieces encodedPieces - * @return ByteString - */ - public static ByteString encodeSequence(ByteString... encodedPieces) { - int totalLen = 0; - ByteString stringPieces = new ByteString(toBytes(0x30)); - for (ByteString p : encodedPieces) { - totalLen += p.length(); - stringPieces.insert(p.getBytes()); - } - stringPieces.insert(1, encodeLength(totalLen).getBytes()); - return stringPieces; - } - - /** - * - * @param length length - * @return ByteString - */ - public static ByteString encodeLength(int length) { - assert length >= 0; - if (length < 0x80) { - return new ByteString(toBytes(length)); - } - String hexString = String.format("%x", length); - if (hexString.length() % 2 != 0) { - hexString = "0" + hexString; + static public final class DerFieldType { + static final public String Integer = "integer"; + static final public String BitString = "bitString"; + static final public String OctetString = "octetString"; + static final public String Null = "null"; + static final public String Object = "object"; + static final public String PrintableString = "printableString"; + static final public String UtcTime = "utcTime"; + static final public String Sequence = "sequence"; + static final public String Set = "set"; + static final public String OidContainer = "oidContainer"; + static final public String PublicKeyPointContainer = "publicKeyPointContainer"; + } + + static private final HashMap hexTagToType = new HashMap() { + { + put("02", DerFieldType.Integer); + put("03", DerFieldType.BitString); + put("04", DerFieldType.OctetString); + put("05", DerFieldType.Null); + put("06", DerFieldType.Object); + put("13", DerFieldType.PrintableString); + put("17", DerFieldType.UtcTime); + put("30", DerFieldType.Sequence); + put("31", DerFieldType.Set); + put("a0", DerFieldType.OidContainer); + put("a1", DerFieldType.PublicKeyPointContainer); + } + }; + + static private final HashMap typeToHexTag = new HashMap() { + { + for (String key : hexTagToType.keySet()) { + put(hexTagToType.get(key), key); + } } - ByteString s = new ByteString(binaryFromHex(hexString)); - s.insert(0, toBytes((0x80 | s.length()))); - return s; - - } + }; - /** - * - * @param r r - * @return ByteString - */ - public static ByteString encodeInteger(BigInteger r) { - assert r.compareTo(BigInteger.ZERO) >= 0; - String h = String.format("%x", r); - if (h.length() % 2 != 0) { - h = "0" + h; + static public String encodeConstructed(String... encodedValues) { + StringBuilder stringPieces = new StringBuilder(""); + for (String p : encodedValues) { + stringPieces.append(p); } - ByteString s = new ByteString(binaryFromHex(h)); - short num = s.getShort(0); - if (num <= 0x7F) { - s.insert(0, toBytes(s.length())); - s.insert(0, toBytes(0x02)); - return s; - } - int length = s.length(); - s.insert(0, toBytes(0x00)); - s.insert(0, toBytes((length + 1))); - s.insert(0, toBytes(0x02)); - return s; + return encodePrimitive(DerFieldType.Sequence, stringPieces.toString()); } - /** - * - * @param n n - * @return ByteString - */ - public static ByteString encodeNumber(long n) { - ByteString b128Digits = new ByteString(); - while (n != 0) { - b128Digits.insert(0, toBytes((int) (n & 0x7f) | 0x80)); - n = n >> 7; - } - if (b128Digits.isEmpty()) { - b128Digits.insert(toBytes(0)); - } - int lastIndex = b128Digits.length() - 1; - b128Digits.replace(lastIndex, (byte) (b128Digits.getShort(lastIndex) & 0x7f)); - return b128Digits; + static public String encodePrimitive(String tagType, Object value) { + if(Objects.equals(tagType, DerFieldType.Integer)) value = encodeInteger(new BigInteger((String) value)); + if(Objects.equals(tagType, DerFieldType.Object)) value = Oid.oidToHex((long []) value); + + return "" + typeToHexTag.get(tagType) + generateLengthBytes((String) value) + value; } - /** - * - * @param pieces pieces - * @return ByteString - */ - public static ByteString encodeOid(long... pieces) { - long first = pieces[0]; - long second = pieces[1]; - assert first <= 2; - assert second <= 39; - ByteString body = new ByteString(); - for (int i = 2; i < pieces.length; i++) { - body.insert(encodeNumber(pieces[i]).getBytes()); + static private String encodeInteger(BigInteger number) { + String hexadecimal = Binary.hexFromInt(number.abs()); + if(number.signum() == -1) { + int bitCount = 4 * hexadecimal.length(); + BigInteger twosComplement = number.add(BigInteger.valueOf((long) Math.pow(2, bitCount))); + return Binary.hexFromInt(twosComplement); } - body.insert(0, toBytes((int) (40 * first + second))); - body.insert(0, encodeLength(body.length()).getBytes()); - body.insert(0, toBytes(0x06)); - return body; - } - - /** - * - * @param s s - * @return ByteString - */ - public static ByteString encodeBitString(ByteString s) { - s.insert(0, encodeLength(s.length()).getBytes()); - s.insert(0, toBytes(0x03)); - return s; + char firstChar = hexadecimal.charAt(0); + String bits = Binary.bitsFromHex(String.valueOf(firstChar)); + if(bits.charAt(0) == '1') hexadecimal = "00" + hexadecimal; + return hexadecimal; } - /** - * - * @param s s - * @return ByteString - */ - public static ByteString encodeOctetString(ByteString s) { - s.insert(0, encodeLength(s.length()).getBytes()); - s.insert(0, toBytes(0x04)); - return s; - } + static public Object[] parse(String hexadecimal) throws Exception { + if(Objects.equals(hexadecimal, "")) return new Object[]{}; + String typeByte = hexadecimal.substring(0, 2); + hexadecimal = hexadecimal.substring(2); - /** - * - * @param tag tag - * @param value value - * @return ByteString - */ - public static ByteString encodeConstructed(long tag, ByteString value) { - value.insert(0, encodeLength(value.length()).getBytes()); - value.insert(0, toBytes((int) (0xa0 + tag))); - return value; - } + int[] lengthArray = readLengthBytes(hexadecimal); + int length = lengthArray[0]; + int lengthBytes = lengthArray[1]; - /** - * - * @param string string - * @return int[] - */ - public static int[] readLength(ByteString string) { - short num = string.getShort(0); - if ((num & 0x80) == 0) { - return new int[]{num & 0x7f, 1}; - } + String content = hexadecimal.substring(lengthBytes, lengthBytes + length); + hexadecimal = hexadecimal.substring(lengthBytes + length); + if(content.length() < length) throw new Exception("missing bytes in DER parse"); - int llen = num & 0x7f; - if (llen > string.length() - 1) { - throw new RuntimeException("ran out of length bytes"); - } - return new int[]{Integer.valueOf(hexFromBinary(string.substring(1, 1 + llen)), 16), 1 + llen}; - } - - /** - * - * @param string string - * @return int[] - */ - public static int[] readNumber(ByteString string) { - int number = 0; - int llen = 0; - for (; ; ) { - if (llen > string.length()) { - throw new RuntimeException("ran out of length bytes"); + HashMap tagData = getTagData(typeByte); + + if(tagData.get("isConstructed").equals(true)) { + + Object[] nextContent = parse(hexadecimal); + if(nextContent.length == 0) { + return new Object[]{ parse(content) }; } - number = number << 7; - short d = string.getShort(llen); - number += (d & 0x7f); - llen += 1; - if ((d & 0x80) == 0) + return new Object[]{ parse(content), nextContent[0] }; + } + + List contentArray = new ArrayList(); + switch((String) tagData.get("type")) { + case DerFieldType.Null: + contentArray.add(parseNull(content)); + break; + case DerFieldType.Object: + contentArray.add(parseOid(content)); + break; + case DerFieldType.UtcTime: + contentArray.add(parseTime(content)); + break; + case DerFieldType.Integer: + contentArray.add(parseInteger(content)); + break; + case DerFieldType.PrintableString: + contentArray.add(longFromString(content)); + break; + default: + contentArray.add(parseAny(content)); break; } - return new int[]{number, llen}; - } - /** - * - * @param string string - * @return ByteString[] - */ - public static ByteString[] removeSequence(ByteString string) { - short n = string.getShort(0); - if (n != 0x30) { - throw new RuntimeException(String.format("wanted sequence (0x30), got 0x%02x", n)); - } - int[] l = readLength(string.substring(1)); - long endseq = 1 + l[0] + l[1]; - return new ByteString[]{string.substring(1 + l[1], (int) endseq), string.substring((int) endseq)}; - } + if(hexadecimal.length() != 0) contentArray.add(parse(hexadecimal)); - /** - * - * @param string string - * @return Object[] - */ - public static Object[] removeInteger(ByteString string) { - short n = string.getShort(0); - if (n != 0x02) { - throw new RuntimeException(String.format("wanted integer (0x02), got 0x%02x", n)); - } - int[] l = readLength(string.substring(1)); - int length = l[0]; - int llen = l[1]; - ByteString numberbytes = string.substring(1 + llen, 1 + llen + length); - ByteString rest = string.substring(1 + llen + length); - short nbytes = numberbytes.getShort(0); - assert nbytes < 0x80; - return new Object[]{new BigInteger(hexFromBinary(numberbytes), 16), rest}; + return contentArray.toArray(); } - /** - * - * @param string string - * @return Object[] - */ - public static Object[] removeObject(ByteString string) { - int n = string.getShort(0); - if (n != 0x06) { - throw new RuntimeException(String.format("wanted object (0x06), got 0x%02x", n)); - } - int[] l = readLength(string.substring(1)); - int length = l[0]; - int lengthlength = l[1]; - ByteString body = string.substring(1 + lengthlength, 1 + lengthlength + length); - ByteString rest = string.substring(1 + lengthlength + length); - List numbers = new ArrayList(); - while (!body.isEmpty()) { - l = readNumber(body); - n = l[0]; - int ll = l[1]; - numbers.add(n); - body = body.substring(ll); - } - long n0 = Integer.valueOf(numbers.remove(0).toString()); - long first = n0 / 40; - long second = n0 - (40 * first); - numbers.add(0, first); - numbers.add(1, second); - long[] numbersArray = new long[numbers.size()]; - for (int i = 0; i < numbers.size(); i++) { - numbersArray[i] = Long.valueOf(numbers.get(i).toString()); - } - return new Object[]{numbersArray, rest}; + static private String parseAny(String hexadecimal) { + return hexadecimal; } - /** - * - * @param string string - * @return ByteString - */ - public static ByteString[] removeBitString(ByteString string) { - short n = string.getShort(0); - if (n != 0x03) { - throw new RuntimeException(String.format("wanted bitstring (0x03), got 0x%02x", n)); - } - int[] l = readLength(string.substring(1)); - int length = l[0]; - int llen = l[1]; - ByteString body = string.substring(1 + llen, 1 + llen + length); - ByteString rest = string.substring(1 + llen + length); - return new ByteString[]{body, rest}; + static private List parseOid(String hexadecimal) { + return Oid.oidFromHex(hexadecimal); } - /** - * - * @param string string - * @return ByteString[] - */ - public static ByteString[] removeOctetString(ByteString string) { - short n = string.getShort(0); - if (n != 0x04) { - throw new RuntimeException(String.format("wanted octetstring (0x04), got 0x%02x", n)); - } - int[] l = readLength(string.substring(1)); - int length = l[0]; - int llen = l[1]; - ByteString body = string.substring(1 + llen, 1 + llen + length); - ByteString rest = string.substring(1 + llen + length); - return new ByteString[]{body, rest}; + // TODO _parseTime - This function is not implemented yet + static private String parseTime(String hexadecimal) { + return longFromString(hexadecimal); + } + + static private String longFromString(String hexadecimal) { + return Binary.byteFromHex(hexadecimal).toString(); } - /** - * - * @param string string - * @return Object[] - */ - public static Object[] removeConstructed(ByteString string) { - short s0 = string.getShort(0); - if ((s0 & 0xe0) != 0xa0) { - throw new RuntimeException(String.format("wanted constructed tag (0xa0-0xbf), got 0x%02x", s0)); - } - int tag = s0 & 0x1f; - int[] l = readLength(string.substring(1)); - int length = l[0]; - int llen = l[1]; - ByteString body = string.substring(1 + llen, 1 + llen + length); - ByteString rest = string.substring(1 + llen + length); - return new Object[]{tag, body, rest}; + static private String parseNull(String hexadecimal) { + return null; } - /** - * - * @param pem pem - * @return ByteString - */ - public static ByteString fromPem(String pem) { - String[] pieces = pem.split("\n"); - StringBuilder d = new StringBuilder(); - for (String p : pieces) { - if (!p.isEmpty() && !p.startsWith("-----")) { - d.append(p.trim()); - } - } - try { - return new ByteString(Base64.decode(d.toString())); - } catch (IOException e) { - throw new IllegalArgumentException("Corrupted pem string! Could not decode base64 from it"); + static private BigInteger parseInteger(String hexadecimal) { + BigInteger integer = Binary.intFromHex(hexadecimal); + String bits = Binary.bitsFromHex(hexadecimal.charAt(0)); + if(bits.charAt(0) == '0') return integer; + int bitCount = 4 * hexadecimal.length(); + return integer.subtract(BigInteger.valueOf((long) Math.pow(2, bitCount))); + } + + static private int[] readLengthBytes(String hexadecimal) throws Exception { + int lengthBytes = 2; + int lengthIndicator = Binary.intFromHex(hexadecimal.substring(0, lengthBytes)).intValue(); + boolean isShortForm = lengthIndicator < 128; + if(isShortForm) { + int length = 2 * lengthIndicator; + return new int[] {length, lengthBytes}; } + int lengthLength = lengthIndicator - 128; + if(lengthLength == 0) throw new Exception("indefinite length encoding located in DER"); + lengthBytes += 2 * lengthLength; + int length = Binary.intFromHex(hexadecimal.substring(2, lengthBytes)).intValue() * 2; + return new int[] {length, lengthBytes}; + } + + static private String generateLengthBytes(String hexadecimal) { + BigInteger size = BigInteger.valueOf(hexadecimal.length()).divide(BigInteger.valueOf(2)); + String length = Binary.hexFromInt(size); + if(size.compareTo(BigInteger.valueOf(128)) < 0) return Binary.padLeftZeros(length, 2); + BigInteger lengthLength = BigInteger.valueOf(length.length()).divide(BigInteger.valueOf(2)).add(BigInteger.valueOf(128)); + return Binary.hexFromInt(lengthLength) + length; } - /** - * - * @param der der - * @param name name - * @return String - */ - public static String toPem(ByteString der, String name) { - String b64 = Base64.encodeBytes(der.getBytes()); - StringBuilder lines = new StringBuilder(); - lines.append(String.format("-----BEGIN %s-----\n", name)); - for (int start = 0; start < b64.length(); start += 64) { - int end = start + 64 > b64.length() ? b64.length() : start + 64; - lines.append(String.format("%s\n", b64.substring(start, end))); - } - lines.append(String.format("-----END %s-----\n", name)); - return lines.toString(); + static private HashMap getTagData(String tag) { + char[] bits = Binary.bitsFromHex(tag).toCharArray(); + char bits8 = bits[0]; + char bits7 = bits[1]; + char bits6 = bits[2]; + + HashMap> tagHashMap = new HashMap>(); + HashMap param0 = new HashMap(); + param0.put("0", "universal"); + param0.put("1", "application"); + HashMap param1 = new HashMap(); + param1.put("0", "context-specific"); + param1.put("1", "private"); + tagHashMap.put("0", param0); + tagHashMap.put("1", param1); + + String tagClass = tagHashMap.get(String.valueOf(bits8)).get(String.valueOf(bits7)); + Boolean isConstructed = bits6 == '1'; + + HashMap data = new HashMap(); + data.put("class", tagClass); + data.put("isConstructed", isConstructed); + data.put("type", hexTagToType.get(tag)); + return data; } } \ No newline at end of file diff --git a/src/main/java/com/starkbank/ellipticcurve/utils/File.java b/src/main/java/com/starkbank/ellipticcurve/utils/File.java index 3de6338..1b6bea4 100644 --- a/src/main/java/com/starkbank/ellipticcurve/utils/File.java +++ b/src/main/java/com/starkbank/ellipticcurve/utils/File.java @@ -45,8 +45,6 @@ public static byte[] readBytes(String fileName) { e.printStackTrace(); } - return content; } - -} \ No newline at end of file +} diff --git a/src/main/java/com/starkbank/ellipticcurve/utils/Oid.java b/src/main/java/com/starkbank/ellipticcurve/utils/Oid.java new file mode 100644 index 0000000..0623247 --- /dev/null +++ b/src/main/java/com/starkbank/ellipticcurve/utils/Oid.java @@ -0,0 +1,53 @@ +package com.starkbank.ellipticcurve.utils; +import java.util.ArrayList; +import java.util.List; + +import static java.lang.Math.floorDiv; + +public class Oid { + + static public List oidFromHex(String hexadecimal) { + String firstByte = hexadecimal.substring(0, 2); + String remainingBytes = hexadecimal.substring(2); + int firstByteInt = Binary.intFromHex(firstByte).intValue(); + List oid = new ArrayList(); + oid.add(floorDiv(firstByteInt, 40)); + oid.add(firstByteInt % 40); + int oidInt = 0; + while( remainingBytes.length() > 0 ) { + String byteString = remainingBytes.substring(0, 2); + remainingBytes = remainingBytes.substring(2); + int byteInt = Binary.intFromHex(byteString).intValue(); + if (byteInt >= 128){ + oidInt = (128 * oidInt) + (byteInt - 128); + continue; + } + oidInt = (128 * oidInt) + byteInt; + oid.add(oidInt); + oidInt = 0; + } + return oid; + } + + static public String oidToHex(long[] oid) { + List oidList = new ArrayList(); + for(long e : oid){ oidList.add(e); } + StringBuilder hexadecimal = new StringBuilder(Binary.hexFromInt(40 * oidList.get(0) + oidList.get(1))); + for (Long number : oidList.subList(2, oidList.size())) { + hexadecimal.append(oidNumberToHex(number)); + } + return hexadecimal.toString(); + } + + static private String oidNumberToHex(long number) { + String hexadecimal = ""; + int endDelta = 0; + while (number > 0) { + hexadecimal = Binary.hexFromInt((number % 128) + endDelta) + hexadecimal; + number = floorDiv(number, 128); + endDelta = 128; + } + return !hexadecimal.equals("") ? hexadecimal : "00"; + } + +} diff --git a/src/main/java/com/starkbank/ellipticcurve/utils/Pem.java b/src/main/java/com/starkbank/ellipticcurve/utils/Pem.java new file mode 100644 index 0000000..ce7cc8d --- /dev/null +++ b/src/main/java/com/starkbank/ellipticcurve/utils/Pem.java @@ -0,0 +1,32 @@ +package com.starkbank.ellipticcurve.utils; + +public class Pem { + + static public String getPemContent(String pem, String template) { + String[] piecesTemplate = template.split("\n%s"); + String[] piecesPem = pem.split("\n"); + StringBuilder content = new StringBuilder(); + boolean flag = false; + for (String pemContent : piecesPem) { + if (pemContent.equals(piecesTemplate[0])) { + flag = true; + continue; + } + if (pemContent.equals(piecesTemplate[1])) { + flag = false; + continue; + } + if (flag) content.append(pemContent); + } + return content.toString(); + } + + static public String createPem(String content, String template) { + StringBuilder lines = new StringBuilder(); + for (int start = 0; start < content.length(); start += 64) { + int end = Math.min(start + 64, content.length()); + lines.append(String.format("%s\n", content.substring(start, end))); + } + return String.format(template, lines.toString()); + } +} diff --git a/src/main/java/com/starkbank/ellipticcurve/utils/RandomInteger.java b/src/main/java/com/starkbank/ellipticcurve/utils/RandomInteger.java index 06f978c..e673b3f 100644 --- a/src/main/java/com/starkbank/ellipticcurve/utils/RandomInteger.java +++ b/src/main/java/com/starkbank/ellipticcurve/utils/RandomInteger.java @@ -16,4 +16,5 @@ public static BigInteger between(BigInteger start, BigInteger end) { Random random = new SecureRandom(); return new BigInteger(end.toByteArray().length * 8 - 1, random).abs().add(start); } + } diff --git a/src/test/java/com/starkbank/ellipticcurve/CompPublicKeyTest.java b/src/test/java/com/starkbank/ellipticcurve/CompPublicKeyTest.java new file mode 100644 index 0000000..248b9a7 --- /dev/null +++ b/src/test/java/com/starkbank/ellipticcurve/CompPublicKeyTest.java @@ -0,0 +1,49 @@ +package com.starkbank.ellipticcurve; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + + +public class CompPublicKeyTest { + + @Test + public void testBatch() throws Exception { + for(int i = 0; i < 1000; i++) { + PrivateKey privateKey = new PrivateKey(); + PublicKey publicKey = privateKey.publicKey(); + String publicKeyString = publicKey.toCompressed(); + + PublicKey recoveredPublicKey = PublicKey.fromCompressed(publicKeyString); + + assertEquals(publicKey.point.x, recoveredPublicKey.point.x); + assertEquals(publicKey.point.y, recoveredPublicKey.point.y); + } + } + + @Test + public void testFromCompressedEven() throws Exception { + String publicKeyCompressed = "0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2"; + PublicKey publicKey = PublicKey.fromCompressed(publicKeyCompressed); + assertEquals(publicKey.toPem(), "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEUpclctRl0BbUxQGIe43zA+7j7WAsBWse\nsJJg36DaCrKIdC9NyX2e22/ZRrq8AC/fsG8myvEXuUBe15J1dj/bHA==\n-----END PUBLIC KEY-----"); + } + + @Test + public void testFromCompressedOdd() throws Exception { + String publicKeyCompressed = "0318ed2e1ec629e2d3dae7be1103d4f911c24e0c80e70038f5eb5548245c475f50"; + PublicKey publicKey = PublicKey.fromCompressed(publicKeyCompressed); + assertEquals(publicKey.toPem(), "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEGO0uHsYp4tPa574RA9T5EcJODIDnADj1\n61VIJFxHX1BMIg0B4cpBnLG6SzOTthXpndIKpr8HEHj3D9lJAI50EQ==\n-----END PUBLIC KEY-----"); + } + + @Test + public void testToCompressedEven() throws Exception { + PublicKey publicKey = PublicKey.fromPem("-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEUpclctRl0BbUxQGIe43zA+7j7WAsBWse\nsJJg36DaCrKIdC9NyX2e22/ZRrq8AC/fsG8myvEXuUBe15J1dj/bHA==\n-----END PUBLIC KEY-----"); + String publicKeyCompressed = publicKey.toCompressed(); + assertEquals(publicKeyCompressed, "0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2"); + } + + @Test + public void testToCompressedOdd() throws Exception { + PublicKey publicKey = PublicKey.fromPem("-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEGO0uHsYp4tPa574RA9T5EcJODIDnADj1\n61VIJFxHX1BMIg0B4cpBnLG6SzOTthXpndIKpr8HEHj3D9lJAI50EQ==\n-----END PUBLIC KEY-----"); + String publicKeyCompressed = publicKey.toCompressed(); + assertEquals(publicKeyCompressed, "0318ed2e1ec629e2d3dae7be1103d4f911c24e0c80e70038f5eb5548245c475f50"); + } +} diff --git a/src/test/java/com/starkbank/ellipticcurve/CurveTest.java b/src/test/java/com/starkbank/ellipticcurve/CurveTest.java new file mode 100644 index 0000000..2252894 --- /dev/null +++ b/src/test/java/com/starkbank/ellipticcurve/CurveTest.java @@ -0,0 +1,100 @@ +package com.starkbank.ellipticcurve; +import org.junit.Test; +import static org.junit.Assert.assertTrue; +import java.math.BigInteger; + + +public class CurveTest { + + @Test + public void testPemConversion() throws Exception { + Curve newCurve = new Curve( + BigInteger.ZERO, + BigInteger.valueOf(7), + new BigInteger("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", 16), + new BigInteger("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16), + new BigInteger("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 16), + new BigInteger("483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", 16), + "secp256k1", + new long[]{1, 3, 132, 0, 10} + ); + + PrivateKey privateKey1 = new PrivateKey(newCurve); + PublicKey publicKey1 = privateKey1.publicKey(); + + String privateKeyPem = privateKey1.toPem(); + String publicKeyPem = publicKey1.toPem(); + + PrivateKey privateKey2 = PrivateKey.fromPem(privateKeyPem); + PublicKey publicKey2 = PublicKey.fromPem(publicKeyPem); + + String message = "test"; + + String signatureBase64 = Ecdsa.sign(message, privateKey2).toBase64(); + Signature signature = Signature.fromBase64(signatureBase64); + + assertTrue(Ecdsa.verify(message, signature, publicKey2)); + } + + @Test + public void testAddNewCurve() throws Exception { + Curve newCurve = new Curve( + new BigInteger("f1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c00", 16), + new BigInteger("ee353fca5428a9300d4aba754a44c00fdfec0c9ae4b1a1803075ed967b7bb73f", 16), + new BigInteger("f1fd178c0b3ad58f10126de8ce42435b3961adbcabc8ca6de8fcf353d86e9c03", 16), + new BigInteger("f1fd178c0b3ad58f10126de8ce42435b53dc67e140d2bf941ffdd459c6d655e1", 16), + new BigInteger("b6b3d4c356c139eb31183d4749d423958c27d2dcaf98b70164c97a2dd98f5cff", 16), + new BigInteger("6142e0f7c8b204911f9271f0f3ecef8c2701c307e8e4c9e183115a1554062cfb", 16), + "frp256v1", + new long[]{1, 2, 250, 1, 223, 101, 256, 1} + ); + Curve.add(newCurve); + PrivateKey privateKey1 = new PrivateKey(newCurve); + PublicKey publicKey1 = privateKey1.publicKey(); + + String privateKeyPem = privateKey1.toPem(); + String publicKeyPem = publicKey1.toPem(); + + PrivateKey privateKey2 = PrivateKey.fromPem(privateKeyPem); + PublicKey publicKey2 = PublicKey.fromPem(publicKeyPem); + + String message = "test"; + + String signatureBase64 = Ecdsa.sign(message, privateKey2).toBase64(); + Signature signature = Signature.fromBase64(signatureBase64); + + assertTrue(Ecdsa.verify(message, signature, publicKey2)); + } + + @Test + public void testUnsupportedCurve() throws Exception { + Curve newCurve = new Curve( + new BigInteger("a9fb57dba1eea9bc3e660a909d838d726e3bf623d52620282013481d1f6e5374", 16), + new BigInteger("662c61c430d84ea4fe66a7733d0b76b7bf93ebc4af2f49256ae58101fee92b04", 16), + new BigInteger("a9fb57dba1eea9bc3e660a909d838d726e3bf623d52620282013481d1f6e5377", 16), + new BigInteger("a9fb57dba1eea9bc3e660a909d838d718c397aa3b561a6f7901e0e82974856a7", 16), + new BigInteger("a3e8eb3cc1cfe7b7732213b23a656149afa142c47aafbc2b79a191562e1305f4", 16), + new BigInteger("2d996c823439c56d7f7b22e14644417e69bcb6de39d027001dabe8f35b25c9be", 16), + "brainpoolP256t1", + new long[]{1, 3, 36, 3, 3, 2, 8, 1, 1, 8} + ); + + PrivateKey privateKey1 = new PrivateKey(newCurve); + PublicKey publicKey1 = privateKey1.publicKey(); + + String privateKeyPem = privateKey1.toPem(); + String publicKeyPem = publicKey1.toPem(); + + try { + PrivateKey privateKey2 = PrivateKey.fromPem(privateKeyPem); + } catch (Error e) { + assertTrue(e.getMessage().contains("Unknown curve")); + } + + try { + PublicKey publicKey2 = PublicKey.fromPem(publicKeyPem); + } catch (Error e) { + assertTrue(e.getMessage().contains("Unknown curve")); + } + } +} diff --git a/src/test/java/com/starkbank/ellipticcurve/OpenSSLTest.java b/src/test/java/com/starkbank/ellipticcurve/OpenSSLTest.java index 85354b0..35bfef1 100644 --- a/src/test/java/com/starkbank/ellipticcurve/OpenSSLTest.java +++ b/src/test/java/com/starkbank/ellipticcurve/OpenSSLTest.java @@ -1,15 +1,12 @@ package com.starkbank.ellipticcurve; -import com.starkbank.ellipticcurve.utils.ByteString; import org.junit.Test; -import java.io.IOException; -import java.net.URISyntaxException; import static org.junit.Assert.assertTrue; public class OpenSSLTest { @Test - public void testAssign() throws URISyntaxException, IOException { + public void testAssign() throws Exception { // Generated by:openssl ecparam -name secp256k1 - genkey - out privateKey.pem String privateKeyPem = Utils.readFileAsString("privateKey.pem"); @@ -25,11 +22,11 @@ public void testAssign() throws URISyntaxException, IOException { } @Test - public void testVerifySignature() throws IOException, URISyntaxException { + public void testVerifySignature() throws Exception { // openssl ec -in privateKey.pem - pubout - out publicKey.pem String publicKeyPem = Utils.readFileAsString("publicKey.pem"); // openssl dgst -sha256 -sign privateKey.pem -out signature.binary message.txt - ByteString signatureBin = new ByteString(Utils.readFileAsBytes("signature.binary")); + byte[] signatureBin = Utils.readFileAsBytes("signature.binary"); String message = Utils.readFileAsString("message.txt"); diff --git a/src/test/java/com/starkbank/ellipticcurve/PrivateKeyTest.java b/src/test/java/com/starkbank/ellipticcurve/PrivateKeyTest.java index a3fa823..cad184f 100644 --- a/src/test/java/com/starkbank/ellipticcurve/PrivateKeyTest.java +++ b/src/test/java/com/starkbank/ellipticcurve/PrivateKeyTest.java @@ -1,5 +1,4 @@ package com.starkbank.ellipticcurve; -import com.starkbank.ellipticcurve.utils.ByteString; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -7,7 +6,7 @@ public class PrivateKeyTest { @Test - public void testPemConversion() { + public void testPemConversion() throws Exception { PrivateKey privateKey1 = new PrivateKey(); String pem = privateKey1.toPem(); PrivateKey privateKey2 = PrivateKey.fromPem(pem); @@ -16,9 +15,9 @@ public void testPemConversion() { } @Test - public void testDerConversion() { + public void testDerConversion() throws Exception { PrivateKey privateKey1 = new PrivateKey(); - ByteString der = privateKey1.toDer(); + byte[] der = privateKey1.toDer(); PrivateKey privateKey2 = PrivateKey.fromDer(der); assertEquals(privateKey1.secret, privateKey2.secret); assertEquals(privateKey1.curve, privateKey2.curve); @@ -27,7 +26,7 @@ public void testDerConversion() { @Test public void testStringConversion() { PrivateKey privateKey1 = new PrivateKey(); - ByteString string = privateKey1.toByteString(); + String string = privateKey1.toString(); PrivateKey privateKey2 = PrivateKey.fromString(string); assertEquals(privateKey1.secret, privateKey2.secret); assertEquals(privateKey1.curve, privateKey2.curve); diff --git a/src/test/java/com/starkbank/ellipticcurve/PublicKeyTest.java b/src/test/java/com/starkbank/ellipticcurve/PublicKeyTest.java index 638b790..e21d44a 100644 --- a/src/test/java/com/starkbank/ellipticcurve/PublicKeyTest.java +++ b/src/test/java/com/starkbank/ellipticcurve/PublicKeyTest.java @@ -1,5 +1,4 @@ package com.starkbank.ellipticcurve; -import com.starkbank.ellipticcurve.utils.ByteString; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -7,7 +6,7 @@ public class PublicKeyTest { @Test - public void testPemConversion() { + public void testPemConversion() throws Exception { PrivateKey privateKey = new PrivateKey(); PublicKey publicKey1 = privateKey.publicKey(); String pem = publicKey1.toPem(); @@ -18,10 +17,10 @@ public void testPemConversion() { } @Test - public void testDerConversion() { + public void testDerConversion() throws Exception { PrivateKey privateKey = new PrivateKey(); PublicKey publicKey1 = privateKey.publicKey(); - ByteString der = publicKey1.toDer(); + byte[] der = publicKey1.toDer(); PublicKey publicKey2 = PublicKey.fromDer(der); assertEquals(publicKey1.point.x, publicKey2.point.x); assertEquals(publicKey1.point.y, publicKey2.point.y); @@ -32,7 +31,7 @@ public void testDerConversion() { public void testStringConversion() { PrivateKey privateKey = new PrivateKey(); PublicKey publicKey1 = privateKey.publicKey(); - ByteString string = publicKey1.toByteString(); + String string = publicKey1.toString(); PublicKey publicKey2 = PublicKey.fromString(string); assertEquals(publicKey1.point.x, publicKey2.point.x); assertEquals(publicKey1.point.y, publicKey2.point.y); diff --git a/src/test/java/com/starkbank/ellipticcurve/SignatureTest.java b/src/test/java/com/starkbank/ellipticcurve/SignatureTest.java index 38bca1d..d127421 100644 --- a/src/test/java/com/starkbank/ellipticcurve/SignatureTest.java +++ b/src/test/java/com/starkbank/ellipticcurve/SignatureTest.java @@ -1,5 +1,4 @@ package com.starkbank.ellipticcurve; -import com.starkbank.ellipticcurve.utils.ByteString; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -7,13 +6,13 @@ public class SignatureTest { @Test - public void testDerConversion() { + public void testDerConversion() throws Exception { PrivateKey privateKey = new PrivateKey(); String message = "This is a text message"; Signature signature1 = Ecdsa.sign(message, privateKey); - ByteString der = signature1.toDer(); + byte[] der = signature1.toDer(); Signature signature2 = Signature.fromDer(der); @@ -22,7 +21,7 @@ public void testDerConversion() { } @Test - public void testBase64Conversion() { + public void testBase64Conversion() throws Exception { PrivateKey privateKey = new PrivateKey(); String message = "This is a text message"; @@ -30,7 +29,7 @@ public void testBase64Conversion() { String base64 = signature1.toBase64(); - Signature signature2 = Signature.fromBase64(new ByteString(base64.getBytes())); + Signature signature2 = Signature.fromBase64(base64); assertEquals(signature1.r, signature2.r); assertEquals(signature1.s, signature2.s); diff --git a/src/test/java/com/starkbank/ellipticcurve/SignatureWithRecoveryIdTest.java b/src/test/java/com/starkbank/ellipticcurve/SignatureWithRecoveryIdTest.java new file mode 100644 index 0000000..3569091 --- /dev/null +++ b/src/test/java/com/starkbank/ellipticcurve/SignatureWithRecoveryIdTest.java @@ -0,0 +1,38 @@ +package com.starkbank.ellipticcurve; +import org.junit.Test; +import static org.junit.Assert.assertEquals; + + +public class SignatureWithRecoveryIdTest { + + @Test + public void testDerConversion() throws Exception { + PrivateKey privateKey = new PrivateKey(); + String message = "This is a text message"; + + Signature signature1 = Ecdsa.sign(message, privateKey); + + byte[] der = signature1.toDer(true); + + Signature signature2 = Signature.fromDer(der, true); + + assertEquals(signature1.r, signature2.r); + assertEquals(signature1.s, signature2.s); + assertEquals(signature1.recoveryId, signature2.recoveryId); + } + + @Test + public void testBase64Conversion() throws Exception { + PrivateKey privateKey = new PrivateKey(); + String message = "This is a text message"; + + Signature signature1 = Ecdsa.sign(message, privateKey); + + String base64 = signature1.toBase64(); + + Signature signature2 = Signature.fromBase64(base64); + + assertEquals(signature1.r, signature2.r); + assertEquals(signature1.s, signature2.s); + } +} diff --git a/src/test/java/com/starkbank/ellipticcurve/Utils.java b/src/test/java/com/starkbank/ellipticcurve/Utils.java index 6dc5afa..71c4b1c 100644 --- a/src/test/java/com/starkbank/ellipticcurve/Utils.java +++ b/src/test/java/com/starkbank/ellipticcurve/Utils.java @@ -2,16 +2,18 @@ import java.io.IOException; import java.io.RandomAccessFile; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; class Utils { static String readFileAsString(String path) throws URISyntaxException, IOException { - return new String(readFileAsBytes(path), "ASCII"); + return new String(readFileAsBytes(path), StandardCharsets.US_ASCII); } static byte[] readFileAsBytes(String path) throws URISyntaxException { - return read(ClassLoader.getSystemClassLoader().getResource(path).toURI().getPath()); + return read(Objects.requireNonNull(ClassLoader.getSystemClassLoader().getResource(path)).toURI().getPath()); } private static byte[] read(String path) {