All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hedera.hashgraph.sdk.Keystore Maven / Gradle / Ivy

There is a newer version: 2.40.0
Show newest version
/*-
 *
 * Hedera Java SDK
 *
 * Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.hedera.hashgraph.sdk;

import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.stream.JsonWriter;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Optional;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.encoders.Hex;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Objects;

/**
 * Internal utility class to serialize / deserialize between java object / json representation.
 */
final class Keystore {
    private static final Gson gson = new Gson();
    private static final JsonParser jsonParser = new JsonParser();

    private byte[] keyBytes;

    private Keystore(byte[] keyBytes) {
        this.keyBytes = keyBytes;
    }

    public Keystore(PrivateKey privateKey) {
        this.keyBytes = privateKey.toBytes();
    }

    public static Keystore fromStream(InputStream stream, String passphrase) throws IOException {
        try {
            JsonObject jsonObject = jsonParser.parse(new InputStreamReader(stream, StandardCharsets.UTF_8))
                .getAsJsonObject();
            return fromJson(jsonObject, passphrase);
        } catch (IllegalStateException e) {
            throw new BadKeyException(Optional.ofNullable(e.getMessage()).orElse("failed to parse Keystore"));
        } catch (JsonIOException e) {
            // RFC (@abonander): I'm all for keeping this as an unchecked exception
            // but I want consistency with export() so this may involve creating our own exception
            // because JsonIOException is kinda leaking implementation details.
            throw (IOException) Objects.requireNonNull(e.getCause());
        } catch (JsonSyntaxException e) {
            throw new BadKeyException(e);
        }
    }

    private static Keystore fromJson(JsonObject object, String passphrase) {
        int version = expectInt(object, "version");

        //noinspection SwitchStatementWithTooFewBranches
        switch (version) {
            case 1:
                return parseKeystoreV1(expectObject(object, "crypto"), passphrase);
            case 2:
                return parseKeystoreV2(expectObject(object, "crypto"), passphrase);
            default:
                throw new BadKeyException("unsupported keystore version: " + version);
        }
    }

    private static Keystore parseKeystoreV1(JsonObject crypto, String passphrase) {
        String ciphertext = expectString(crypto, "ciphertext");
        String ivString = expectString(expectObject(crypto, "cipherparams"), "iv");
        String cipher = expectString(crypto, "cipher");
        String kdf = expectString(crypto, "kdf");
        JsonObject kdfParams = expectObject(crypto, "kdfparams");
        String macString = expectString(crypto, "mac");

        if (!cipher.equals("aes-128-ctr")) {
            throw new BadKeyException("unsupported keystore cipher: " + cipher);
        }

        if (!kdf.equals("pbkdf2")) {
            throw new BadKeyException("unsuppported KDF: " + kdf);
        }

        int dkLen = expectInt(kdfParams, "dkLen");
        String saltStr = expectString(kdfParams, "salt");
        int count = expectInt(kdfParams, "c");
        String prf = expectString(kdfParams, "prf");

        if (!prf.equals("hmac-sha256")) {
            throw new BadKeyException("unsupported KDF hash function: " + prf);
        }

        byte[] cipherBytes = Hex.decode(ciphertext);
        byte[] iv = Hex.decode(ivString);
        byte[] mac = Hex.decode(macString);
        byte[] salt = Hex.decode(saltStr);

        KeyParameter cipherKey = Crypto.deriveKeySha256(passphrase, salt, count, dkLen);

        byte[] testHmac = Crypto.calcHmacSha384(cipherKey, null, cipherBytes);

        if (!MessageDigest.isEqual(mac, testHmac)) {
            throw new BadKeyException("HMAC mismatch; passphrase is incorrect");
        }

        return new Keystore(Crypto.decryptAesCtr128(cipherKey, iv, cipherBytes));
    }

    private static Keystore parseKeystoreV2(JsonObject crypto, String passphrase) {
        String ciphertext = expectString(crypto, "ciphertext");
        String ivString = expectString(expectObject(crypto, "cipherparams"), "iv");
        String cipher = expectString(crypto, "cipher");
        String kdf = expectString(crypto, "kdf");
        JsonObject kdfParams = expectObject(crypto, "kdfparams");
        String macString = expectString(crypto, "mac");

        if (!cipher.equals("aes-128-ctr")) {
            throw new BadKeyException("unsupported keystore cipher: " + cipher);
        }

        if (!kdf.equals("pbkdf2")) {
            throw new BadKeyException("unsuppported KDF: " + kdf);
        }

        int dkLen = expectInt(kdfParams, "dkLen");
        String saltStr = expectString(kdfParams, "salt");
        int count = expectInt(kdfParams, "c");
        String prf = expectString(kdfParams, "prf");

        if (!prf.equals("hmac-sha256")) {
            throw new BadKeyException("unsupported KDF hash function: " + prf);
        }

        byte[] cipherBytes = Hex.decode(ciphertext);
        byte[] iv = Hex.decode(ivString);
        byte[] mac = Hex.decode(macString);
        byte[] salt = Hex.decode(saltStr);

        KeyParameter cipherKey = Crypto.deriveKeySha256(passphrase, salt, count, dkLen);

        byte[] testHmac = Crypto.calcHmacSha384(cipherKey, iv, cipherBytes);

        if (!MessageDigest.isEqual(mac, testHmac)) {
            throw new BadKeyException("HMAC mismatch; passphrase is incorrect");
        }

        return new Keystore(Crypto.decryptAesCtr128(cipherKey, iv, cipherBytes));
    }

    @SuppressFBWarnings(
        value = "DCN_NULLPOINTER_EXCEPTION",
        justification = "This control flow seems reasonable to me"
    )
    private static JsonObject expectObject(JsonObject object, String key) {
        try {
            return object.get(key).getAsJsonObject();
        } catch (ClassCastException | NullPointerException e) {
            throw new Error("expected key '" + key + "' to be an object", e);
        }
    }

    @SuppressFBWarnings(
        value = "DCN_NULLPOINTER_EXCEPTION",
        justification = "This control flow seems reasonable to me"
    )
    private static int expectInt(JsonObject object, String key) {
        try {
            return object.get(key).getAsInt();
        } catch (ClassCastException | NullPointerException e) {
            throw new Error("expected key '" + key + "' to be an integer", e);
        }
    }

    @SuppressFBWarnings(
        value = "DCN_NULLPOINTER_EXCEPTION",
        justification = "This control flow seems reasonable to me"
    )
    private static String expectString(JsonObject object, String key) {
        try {
            return object.get(key).getAsString();
        } catch (ClassCastException | NullPointerException e) {
            throw new Error("expected key '" + key + "' to be a string", e);
        }
    }

    /**
     * Get the decoded key from this keystore as an {@link PrivateKey}.
     *
     * @throws BadKeyException if the key bytes are of an incorrect length for a raw
     *                         private key or private key + public key, or do not represent a DER encoded Ed25519
     *                         private key.
     */
    public PrivateKey getEd25519() {
        return PrivateKey.fromBytes(keyBytes);
    }

    public void export(OutputStream outputStream, String passphrase) throws IOException {
        JsonWriter writer = new JsonWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
        gson.toJson(exportJson(passphrase), writer);
        writer.flush();
    }

    private JsonObject exportJson(String passphrase) {
        JsonObject object = new JsonObject();
        object.addProperty("version", 2);

        JsonObject crypto = new JsonObject();
        crypto.addProperty("cipher", "aes-128-ctr");
        crypto.addProperty("kdf", "pbkdf2");

        byte[] salt = Crypto.randomBytes(Crypto.SALT_LEN);

        KeyParameter cipherKey = Crypto.deriveKeySha256(passphrase, salt, Crypto.ITERATIONS, Crypto.DK_LEN);

        byte[] iv = Crypto.randomBytes(Crypto.IV_LEN);

        byte[] cipherBytes = Crypto.encryptAesCtr128(cipherKey, iv, keyBytes);

        byte[] mac = Crypto.calcHmacSha384(cipherKey, iv, cipherBytes);

        JsonObject cipherParams = new JsonObject();
        cipherParams.addProperty("iv", Hex.toHexString(iv));

        JsonObject kdfParams = new JsonObject();
        kdfParams.addProperty("dkLen", Crypto.DK_LEN);
        kdfParams.addProperty("salt", Hex.toHexString(salt));
        kdfParams.addProperty("c", Crypto.ITERATIONS);
        kdfParams.addProperty("prf", "hmac-sha256");

        crypto.add("cipherparams", cipherParams);
        crypto.addProperty("ciphertext", Hex.toHexString(cipherBytes));
        crypto.add("kdfparams", kdfParams);
        crypto.addProperty("mac", Hex.toHexString(mac));

        object.add("crypto", crypto);

        return object;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy