Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 44 additions & 11 deletions src/main/java/com/bitpay/sdk/util/KeyUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import org.bitcoinj.base.Base58;
import org.bitcoinj.base.Sha256Hash;
import org.bitcoinj.crypto.ECKey;
Expand Down Expand Up @@ -174,10 +177,7 @@ public static void saveEcKey(ECKey ecKey) throws IOException {
file = new File(KeyUtils.privateKey);
}

FileOutputStream output = new FileOutputStream(file);

output.write(bytes);
output.close();
writeOwnerOnlyFile(file, bytes);
}

/**
Expand Down Expand Up @@ -210,17 +210,16 @@ public static void saveEcKey(
*/
public static void saveEcKeyAsHex(ECKey ecKey) throws IOException {
byte[] bytes = ecKey.toASN1();
PrintWriter file;
File file;

if (KeyUtils.privateKey == null) {
file = new PrintWriter(PrivateKeyFile);
file = new File(PrivateKeyFile);
} else {
file = new PrintWriter(String.valueOf(KeyUtils.privateKey));
file = new File(String.valueOf(KeyUtils.privateKey));
}

String keyHex = bytesToHex(bytes);
file.println(keyHex);
file.close();
writeOwnerOnlyFile(file, (keyHex + System.lineSeparator()).getBytes(StandardCharsets.UTF_8));
}

/**
Expand Down Expand Up @@ -345,4 +344,38 @@ public static String bytesToHex(byte[] bytes) {

return new String(hexChars);
}

/**
* Writes a private key file so only its owner can read and write it.
*
* <p>On file systems with POSIX permissions, the file is created with mode 0600 (or set to 0600
* if it already exists) before the key is written. On other file systems, such as Windows,
* the file keeps the default permissions.</p>
*
* @param file the file to write
* @param content the file content
* @throws IOException the io exception
*/
private static void writeOwnerOnlyFile(
File file,
byte[] content
) throws IOException {
Path path = file.toPath();
boolean isPosix = path.getFileSystem().supportedFileAttributeViews().contains("posix");
Set<PosixFilePermission> ownerOnly = PosixFilePermissions.fromString("rw-------");

if (isPosix) {
if (Files.exists(path)) {
Files.setPosixFilePermissions(path, ownerOnly);
} else {
Files.createFile(path, PosixFilePermissions.asFileAttribute(ownerOnly));
}
}

Files.write(path, content);

if (isPosix) {
Files.setPosixFilePermissions(path, ownerOnly);
}
}
}
44 changes: 42 additions & 2 deletions src/setup/java/BitPaySetup.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.HashMap;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.bitcoinj.crypto.ECKey;

Expand Down Expand Up @@ -125,9 +131,9 @@ public static void main(final String[] args) {
final String jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(configurationFile);

final ObjectWriter writer = mapper.writer(new DefaultPrettyPrinter());
writer.writeValue(
writeOwnerOnlyFile(
new File(Paths.get(".").toAbsolutePath().normalize() + "/output/BitPay.config.json"),
configurationFile);
writer.writeValueAsBytes(configurationFile));

System.out.println("In location:");
System.out.println(Paths.get(".").toAbsolutePath().normalize() + "/output/BitPay.config.json");
Expand Down Expand Up @@ -159,4 +165,38 @@ public static void main(final String[] args) {
+ "in the generated config file."
);
}

/**
* Writes the config file so only its owner can read and write it, because it holds the API tokens.
*
* <p>On file systems with POSIX permissions, the file is created with mode 0600 (or set to 0600
* if it already exists) before it is written. On other file systems, such as Windows, the file
* keeps the default permissions.</p>
*
* @param file the file to write
* @param content the file content
* @throws IOException the io exception
*/
private static void writeOwnerOnlyFile(
final File file,
final byte[] content
) throws IOException {
final Path path = file.toPath();
final boolean isPosix = path.getFileSystem().supportedFileAttributeViews().contains("posix");
final Set<PosixFilePermission> ownerOnly = PosixFilePermissions.fromString("rw-------");

if (isPosix) {
if (Files.exists(path)) {
Files.setPosixFilePermissions(path, ownerOnly);
} else {
Files.createFile(path, PosixFilePermissions.asFileAttribute(ownerOnly));
}
}

Files.write(path, content);

if (isPosix) {
Files.setPosixFilePermissions(path, ownerOnly);
}
}
}
60 changes: 60 additions & 0 deletions src/test/java/com/bitpay/sdk/util/KeyUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assumptions.assumeTrue;


import com.bitpay.sdk.exceptions.BitPayGenericException;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import org.bitcoinj.crypto.ECKey;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

public class KeyUtilsTest {

Expand Down Expand Up @@ -55,4 +64,55 @@ public void it_should_convert_bytes_to_hex() {

assertEquals(expectedHex, actualHex);
}

@Test
public void it_should_save_the_key_file_readable_only_by_its_owner(@TempDir Path dir) throws IOException {
assumePosix();
File file = dir.resolve("bitpay_private_test.key").toFile();
ECKey key = KeyUtils.createEcKey();

KeyUtils.privateKeyExists(file.getPath());
KeyUtils.saveEcKey(key);

assertEquals("rw-------", permissionsOf(file));
assertEquals(key.getPrivateKeyAsHex(), KeyUtils.loadEcKey().getPrivateKeyAsHex());
}

@Test
public void it_should_tighten_an_existing_key_file(@TempDir Path dir) throws IOException {
assumePosix();
File file = dir.resolve("bitpay_private_test.key").toFile();
Files.write(file.toPath(), "old key".getBytes(StandardCharsets.UTF_8));
Files.setPosixFilePermissions(file.toPath(), PosixFilePermissions.fromString("rw-r--r--"));

KeyUtils.privateKeyExists(file.getPath());
KeyUtils.saveEcKey(KeyUtils.createEcKey());

assertEquals("rw-------", permissionsOf(file));
}

@Test
public void it_should_save_the_hex_key_file_readable_only_by_its_owner(@TempDir Path dir) throws IOException {
assumePosix();
File file = dir.resolve("bitpay_private_test.txt").toFile();
ECKey key = KeyUtils.createEcKey();

KeyUtils.privateKeyExists(file.getPath());
KeyUtils.saveEcKeyAsHex(key);

assertEquals("rw-------", permissionsOf(file));
assertEquals(
KeyUtils.loadEcKeyAsHex(key) + System.lineSeparator(),
new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)
);
}

// POSIX permissions do not apply on Windows.
private static void assumePosix() {
assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"));
}

private static String permissionsOf(File file) throws IOException {
return PosixFilePermissions.toString(Files.getPosixFilePermissions(file.toPath()));
}
}
Loading