Skip to content
Open

WIP #118

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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ composer.lock
dev/
clover*
*.phar
./coverage/
27 changes: 4 additions & 23 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
dist: trusty # fix for travis not updating their HHVM images
language: php
php:
- 5.6
- 7.0
- 7.1
- 7.2
- hhvm

# only build master branch (and PRs)
branches:
Expand All @@ -25,13 +21,14 @@ install:
- sh -c 'if [ "${TEST_FRAMEWORK}" != "" ]; then composer global require fxp/composer-asset-plugin; fi'

script:
- ls -lsaht /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt
- echo -n | openssl s_client -showcerts -connect api.blocktrail.com:443 2>/dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'
# run tests when phpcs is not enabled
- if [[ "${RUN_PHPUNIT}" != "false" ]]; then
export RUN_PHPUNIT="true";
fi
- sh -c 'if [ "${PHPCS}" = "true" ]; then export RUN_PHPUNIT=false; fi'
- sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit -c ./phpunit.xml; fi'
- sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit -c ./phpunit-recovery.xml; fi';
- sh -c 'if [ "${RUN_PHPUNIT}" = "true" ]; then ./vendor/bin/phpunit --stop-on-failure -c ./phpunit.xml; fi'

# run phpcs when enabled
- sh -c 'if [ "${PHPCS}" = "true" ]; then ./vendor/bin/phpcs --standard=./phpcs.xml -n ./src/ && echo "PHPCS OK"; fi'
Expand All @@ -46,9 +43,6 @@ script:
fi
fi

# check coverage if report was generated (requires xdebug enabled)
- sh -c 'if [ -e clover.xml ]; then php coverage-checker.php clover.xml 70; fi'

after_script:
- |
if [ "${WITH_XDEBUG}" = "true" ]; then
Expand All @@ -72,17 +66,4 @@ env:
# BLOCKTRAIL_SDK_BTCCOM_API_ENDPOINT
- secure: "hJDN/XyeCJVi86R08zPN/xpI/7U+SU6tWmfnVp4wF6cyZmdDY6UztSa5FaS/RRh0mF2M66PoW00VxyoZ7dj0avZrZWwILYdBpwiIWCg2Ay7rkBGbE/kiWuMfOAVpf8x3zO/ArunJC7spiWW+amTFxk0sutkz84bLxyQahsPmVgE="
matrix:
- PHPCS=false WITH_XDEBUG=false # default disable phpcs run and disable xdebug

matrix:
exclude:
# exclude the php7.0 job without xdebug so we can add one with xdebug
- php: 7.0
env: PHPCS=false WITH_XDEBUG=false
include:
# php7.0 run with xdebug for code coverage (slooooow)
- php: 7.0
env: PHPCS=false WITH_XDEBUG=true
# php5.6 run with phpcs (won't run tests, only phpcs)
- php: 7.0
env: PHPCS=true WITH_XDEBUG=false
- PHPCS=false WITH_XDEBUG=true # default disable phpcs run and disable xdebug
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"spomky-labs/php-aes-gcm": "v1.2.0"
},
"require-dev": {
"phpunit/phpunit": "4.3.*",
"phpunit/phpunit": "5.*",
"mockery/mockery": "1.0.*",
"squizlabs/php_codesniffer": "2.*",
"php-coveralls/php-coveralls": "^1.0"
}
Expand Down
2 changes: 1 addition & 1 deletion phpunit-recovery.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
>
<testsuites>
<testsuite name="Tests">
<file>./tests/WalletRecoveryTest.php</file>
<file>./tests/IntegrationTests/WalletRecoveryTest.php</file>
</testsuite>
</testsuites>
</phpunit>
5 changes: 4 additions & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
<testsuite name="Tests">
<directory>./tests/</directory>
<directory>./tests/V3Crypt/</directory>
<exclude>./tests/WalletRecoveryTest.php</exclude>
<directory>./tests/Wallet/</directory>
<directory>./tests/Network/</directory>
<directory>./tests/IntegrationTests/</directory>
<exclude>./tests/IntegrationTests/WalletRecoveryTest.php</exclude>
</testsuite>
</testsuites>

Expand Down
124 changes: 88 additions & 36 deletions src/BlocktrailSDK.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = fa
list ($apiNetwork, $testnet) = Util::parseApiNetwork($network, $testnet);

if (is_null($apiEndpoint)) {
$apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://api.blocktrail.com";
$apiEndpoint = getenv('BLOCKTRAIL_SDK_API_ENDPOINT') ?: "https://wallet-api.btc.com";
$apiEndpoint = "{$apiEndpoint}/{$apiVersion}/{$apiNetwork}/";
}

Expand Down Expand Up @@ -201,7 +201,7 @@ public function getDataRestClient() {
* @param RestClientInterface $restClient
*/
public function setRestClient(RestClientInterface $restClient) {
$this->client = $restClient;
$this->blocktrailClient = $restClient;
}

/**
Expand All @@ -226,7 +226,7 @@ public function addressTransactions($address, $page = 1, $limit = 20, $sortDir =
$queryString = [
'page' => $page,
'limit' => $limit,
'sort_dir' => $sortDir
'sort_dir' => $sortDir,
];
$response = $this->dataClient->get($this->converter->getUrlForAddressTransactions($address), $this->converter->paginationParams($queryString));
return $this->converter->convertAddressTxs($response->body());
Expand Down Expand Up @@ -660,7 +660,7 @@ protected function createNewWalletV1($options) {
} else {
// create new primary seed
/** @var HierarchicalKey $primaryPrivateKey */
list($primaryMnemonic, , $primaryPrivateKey) = $this->newPrimarySeed($options['passphrase']);
list($primaryMnemonic, , $primaryPrivateKey) = $this->newV1PrimarySeed($options['passphrase']);
if ($storePrimaryMnemonic !== false) {
$storePrimaryMnemonic = true;
}
Expand Down Expand Up @@ -699,7 +699,7 @@ protected function createNewWalletV1($options) {
$backupPublicKey = null;
if (!isset($options['backup_mnemonic']) && !isset($options['backup_public_key'])) {
/** @var HierarchicalKey $backupPrivateKey */
list($backupMnemonic, , ) = $this->newBackupSeed();
list($backupMnemonic, , ) = $this->newV1BackupSeed();
} else if (isset($options['backup_mnemonic'])) {
$backupMnemonic = $options['backup_mnemonic'];
} elseif (isset($options['backup_public_key'])) {
Expand Down Expand Up @@ -764,11 +764,11 @@ protected function createNewWalletV1($options) {
];
}

public static function randomBits($bits) {
return self::randomBytes($bits / 8);
public function randomBits($bits) {
return $this->randomBytes($bits / 8);
}

public static function randomBytes($bytes) {
public function randomBytes($bytes) {
return (new Random())->bytes($bytes)->getBinary();
}

Expand Down Expand Up @@ -798,7 +798,7 @@ protected function createNewWalletV2($options) {
$backupSeed = null;

if (!isset($options['primary_private_key'])) {
$primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : self::randomBits(256);
$primarySeed = isset($options['primary_seed']) ? $options['primary_seed'] : $this->newV2PrimarySeed();
}

if ($storeDataOnServer) {
Expand All @@ -807,20 +807,17 @@ protected function createNewWalletV2($options) {
throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
}

$secret = bin2hex(self::randomBits(256)); // string because we use it as passphrase
$encryptedSecret = CryptoJSAES::encrypt($secret, $options['passphrase']);
list($secret, $encryptedSecret) = $this->newV2Secret($options['passphrase']);
} else {
$secret = $options['secret'];
}

$encryptedPrimarySeed = CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
$recoverySecret = bin2hex(self::randomBits(256));

$recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);
$encryptedPrimarySeed = $this->newV2EncryptedPrimarySeed($primarySeed, $secret);
list($recoverySecret, $recoveryEncryptedSecret) = $this->newV2RecoverySecret($secret);
}

if (!isset($options['backup_public_key'])) {
$backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : self::randomBits(256);
$backupSeed = isset($options['backup_seed']) ? $options['backup_seed'] : $this->newV2BackupSeed();
}

if (isset($options['primary_private_key'])) {
Expand Down Expand Up @@ -928,7 +925,7 @@ protected function createNewWalletV3($options) {
}
$primarySeed = $options['primary_seed'];
} else {
$primarySeed = new Buffer(self::randomBits(256));
$primarySeed = $this->newV3PrimarySeed();
}
}

Expand All @@ -938,33 +935,27 @@ protected function createNewWalletV3($options) {
throw new \InvalidArgumentException("Can't encrypt data without a passphrase");
}

$secret = new Buffer(self::randomBits(256));
$encryptedSecret = Encryption::encrypt($secret, new Buffer($options['passphrase']), KeyDerivation::DEFAULT_ITERATIONS)
->getBuffer();
list($secret, $encryptedSecret) = $this->newV3Secret($options['passphrase']);
} else {
if (!$options['secret'] instanceof Buffer) {
throw new \RuntimeException('Secret must be provided as a Buffer');
throw new \InvalidArgumentException('Secret must be provided as a Buffer');
}

$secret = $options['secret'];
}

$encryptedPrimarySeed = Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)
->getBuffer();
$recoverySecret = new Buffer(self::randomBits(256));

$recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)
->getBuffer();
$encryptedPrimarySeed = $this->newV3EncryptedPrimarySeed($primarySeed, $secret);
list($recoverySecret, $recoveryEncryptedSecret) = $this->newV3RecoverySecret($secret);
}

if (!isset($options['backup_public_key'])) {
if (isset($options['backup_seed'])) {
if (!$options['backup_seed'] instanceof Buffer) {
throw new \RuntimeException('Backup seed must be an instance of Buffer');
throw new \InvalidArgumentException('Backup seed must be an instance of Buffer');
}
$backupSeed = $options['backup_seed'];
} else {
$backupSeed = new Buffer(self::randomBits(256));
$backupSeed = $this->newV3BackupSeed();
}
}

Expand Down Expand Up @@ -1041,6 +1032,61 @@ protected function createNewWalletV3($options) {
];
}

public function newV2PrimarySeed() {
return $this->randomBits(256);
}

public function newV2BackupSeed() {
return $this->randomBits(256);
}

public function newV2Secret($passphrase) {
$secret = bin2hex($this->randomBits(256)); // string because we use it as passphrase
$encryptedSecret = CryptoJSAES::encrypt($secret, $passphrase);

return [$secret, $encryptedSecret];
}

public function newV2EncryptedPrimarySeed($primarySeed, $secret) {
return CryptoJSAES::encrypt(base64_encode($primarySeed), $secret);
}

public function newV2RecoverySecret($secret) {
$recoverySecret = bin2hex($this->randomBits(256));
$recoveryEncryptedSecret = CryptoJSAES::encrypt($secret, $recoverySecret);

return [$recoverySecret, $recoveryEncryptedSecret];
}

public function newV3PrimarySeed() {
return new Buffer($this->randomBits(256));
}

public function newV3BackupSeed() {
return new Buffer($this->randomBits(256));
}

public function newV3Secret($passphrase) {
$secret = new Buffer($this->randomBits(256));
$encryptedSecret = Encryption::encrypt($secret, new Buffer($passphrase), KeyDerivation::DEFAULT_ITERATIONS)
->getBuffer();

return [$secret, $encryptedSecret];
}

public function newV3EncryptedPrimarySeed(Buffer $primarySeed, Buffer $secret) {
return Encryption::encrypt($primarySeed, $secret, KeyDerivation::SUBKEY_ITERATIONS)
->getBuffer();
}

public function newV3RecoverySecret(Buffer $secret) {
$recoverySecret = new Buffer($this->randomBits(256));
$recoveryEncryptedSecret = Encryption::encrypt($secret, $recoverySecret, KeyDerivation::DEFAULT_ITERATIONS)
->getBuffer();

return [$recoverySecret, $recoveryEncryptedSecret];
}

/**
* @param array $bip32Key
* @throws BlocktrailSDKException
Expand Down Expand Up @@ -1241,10 +1287,10 @@ public function initWallet($options) {

if (array_key_exists('check_backup_key', $options)) {
if (!is_string($options['check_backup_key'])) {
throw new \RuntimeException("check_backup_key should be a string (the xpub)");
throw new \InvalidArgumentException("check_backup_key should be a string (the xpub)");
}
if ($options['check_backup_key'] !== $data['backup_public_key'][0]) {
throw new \RuntimeException("Backup key returned from server didn't match our own");
throw new \InvalidArgumentException("Backup key returned from server didn't match our own");
}
}

Expand Down Expand Up @@ -1381,7 +1427,7 @@ public function deleteWallet($identifier, $checksumAddress, $signature, $force =
*
* @return array [mnemonic, seed, key]
*/
protected function newBackupSeed() {
protected function newV1BackupSeed() {
list($backupMnemonic, $backupSeed, $backupPrivateKey) = $this->generateNewSeed("");

return [$backupMnemonic, $backupSeed, $backupPrivateKey];
Expand All @@ -1397,7 +1443,7 @@ protected function newBackupSeed() {
* @return array [mnemonic, seed, key]
* @TODO: require a strong password?
*/
protected function newPrimarySeed($passphrase) {
protected function newV1PrimarySeed($passphrase) {
list($primaryMnemonic, $primarySeed, $primaryPrivateKey) = $this->generateNewSeed($passphrase);

return [$primaryMnemonic, $primarySeed, $primaryPrivateKey];
Expand Down Expand Up @@ -1438,7 +1484,7 @@ protected function generateNewSeed($passphrase = "", $forceEntropy = null) {
* @return string
* @throws \Exception
*/
protected function generateNewMnemonic($forceEntropy = null) {
public function generateNewMnemonic($forceEntropy = null) {
if ($forceEntropy === null) {
$random = new Random();
$entropy = $random->bytes(512 / 8);
Expand Down Expand Up @@ -1524,7 +1570,7 @@ public function sendTransaction($identifier, $rawTransaction, $paths, $checkFee
$data['base_transaction'] = $rawTransaction['base_transaction'];
$data['signed_transaction'] = $rawTransaction['signed_transaction'];
} else {
throw new \RuntimeException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
throw new \InvalidArgumentException("Invalid value for transaction. For segwit transactions, pass ['base_transaction' => '...', 'signed_transaction' => '...']");
}
} else {
$data['raw_transaction'] = $rawTransaction;
Expand Down Expand Up @@ -1592,6 +1638,8 @@ public function coinSelection($identifier, $outputs, $lockUTXO = false, $allowZe
RestClient::AUTH_HTTP_SIG
);

\var_export(self::jsonDecode($response->body(), true));

return self::jsonDecode($response->body(), true);
}

Expand Down Expand Up @@ -1823,7 +1871,7 @@ public function verifyMessage($message, $address, $signature) {
$adapter = Bitcoin::getEcAdapter();
$addr = \BitWasp\Bitcoin\Address\AddressFactory::fromString($address);
if (!$addr instanceof PayToPubKeyHashAddress) {
throw new \RuntimeException('Can only verify a message with a pay-to-pubkey-hash address');
throw new \InvalidArgumentException('Can only verify a message with a pay-to-pubkey-hash address');
}

/** @var CompactSignatureSerializerInterface $csSerializer */
Expand Down Expand Up @@ -1984,4 +2032,8 @@ public static function normalizeBIP32Key($key) {
throw new \Exception("Bad Input");
}
}

public function shuffle($arr) {
\shuffle($arr);
}
}
4 changes: 2 additions & 2 deletions src/BlocktrailSDKInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
*/
interface BlocktrailSDKInterface {

public function __construct($apiKey, $apiSecret, $network = 'BTC', $testnet = false, $apiVersion = 'v1', $apiEndpoint = null);

/**
* enable CURL debugging output
*
Expand Down Expand Up @@ -640,4 +638,6 @@ public static function toSatoshi($btc);
* @return string[]
*/
public static function sortMultisigKeys(array $pubKeys);

public function shuffle($arr);
}
Loading