diff --git a/app/Services/Bunq/Id/BunqId.php b/app/Services/Bunq/Id/BunqId.php new file mode 100644 index 0000000000..9752d71ba3 --- /dev/null +++ b/app/Services/Bunq/Id/BunqId.php @@ -0,0 +1,43 @@ +id; + } + + /** + * @param int $id + */ + public function setId(int $id) + { + $this->id = $id; + } + + +} \ No newline at end of file diff --git a/app/Services/Bunq/Id/DeviceServerId.php b/app/Services/Bunq/Id/DeviceServerId.php new file mode 100644 index 0000000000..b8b845fd7e --- /dev/null +++ b/app/Services/Bunq/Id/DeviceServerId.php @@ -0,0 +1,25 @@ +publicKey = $response['server_public_key']; + } + + /** + * @param string $publicKey + */ + public function setPublicKey(string $publicKey) + { + $this->publicKey = $publicKey; + } + + /** + * @return string + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + +} \ No newline at end of file diff --git a/app/Services/Bunq/Object/UserPerson.php b/app/Services/Bunq/Object/UserPerson.php new file mode 100644 index 0000000000..963357ab51 --- /dev/null +++ b/app/Services/Bunq/Object/UserPerson.php @@ -0,0 +1,134 @@ +id = intval($data['id']); + $this->created = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['created']); + $this->updated = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['updated']); + $this->status = $data['status']; + $this->subStatus = $data['sub_status']; + $this->publicUuid = $data['public_uuid']; + $this->displayName = $data['display_name']; + $this->publicNickName = $data['public_nick_name']; + $this->language = $data['language']; + $this->region = $data['region']; + $this->sessionTimeout = intval($data['session_timeout']); + $this->firstName = $data['first_name']; + $this->middleName = $data['middle_name']; + $this->lastName = $data['last_name']; + $this->legalName = $data['legal_name']; + $this->taxResident = $data['tax_resident']; + $this->dateOfBirth = Carbon::createFromFormat('Y-m-d', $data['date_of_birth']); + $this->placeOfBirth = $data['place_of_birth']; + $this->countryOfBirth = $data['country_of_birth']; + $this->nationality = $data['nationality']; + $this->gender = $data['gender']; + $this->versionTos = intval($data['version_terms_of_service']); + $this->documentNumber = $data['document_number']; + $this->documentType = $data['document_type']; + $this->documentCountry = $data['document_country_of_issuance']; + + // create aliases + // create avatar + // create daily limit + // create notification filters + // create address main, postal + // document front, back attachment + // customer, customer_limit + // billing contracts + + // echo '
';
+        //        print_r($data);
+        //        var_dump($this);
+        //        echo '
'; + } + +} \ No newline at end of file diff --git a/app/Services/Bunq/Request/BunqRequest.php b/app/Services/Bunq/Request/BunqRequest.php new file mode 100644 index 0000000000..4226204f0e --- /dev/null +++ b/app/Services/Bunq/Request/BunqRequest.php @@ -0,0 +1,374 @@ + 'X-Bunq-Client-Response-Id', + 'x-bunq-client-request-id' => 'X-Bunq-Client-Request-Id', + ]; + + public function __construct() + { + + + // create a log channel + $this->logger = new Logger('bunq-request'); + $this->logger->pushHandler(new StreamHandler('logs/bunq.log', Logger::DEBUG)); + $this->logger->debug('Hallo dan'); + } + + /** + * + */ + abstract public function call(): void; + + /** + * @return string + */ + public function getServer(): string + { + return $this->server; + } + + /** + * @param string $server + */ + public function setServer(string $server) + { + $this->server = $server; + } + + /** + * @param bool $fake + */ + public function setFake(bool $fake) + { + $this->fake = $fake; + } + + /** + * @param string $privateKey + */ + public function setPrivateKey(string $privateKey) + { + $this->privateKey = $privateKey; + } + + /** + * @param string $secret + */ + public function setSecret(string $secret) + { + $this->secret = $secret; + } + + /** + * @param ServerPublicKey $serverPublicKey + */ + public function setServerPublicKey(ServerPublicKey $serverPublicKey) + { + $this->serverPublicKey = $serverPublicKey; + } + + /** + * @param string $method + * @param string $uri + * @param array $headers + * @param string $data + * + * @return string + */ + protected function generateSignature(string $method, string $uri, array $headers, string $data): string + { + if (strlen($this->privateKey) === 0) { + throw new Exception('No private key present.'); + } + if (strtolower($method) === 'get') { + $data = ''; + } + + $uri = str_replace(['https://api.bunq.com', 'https://sandbox.public.api.bunq.com'], '', $uri); + $toSign = strtoupper($method) . ' ' . $uri . "\n"; + $headersToSign = ['Cache-Control', 'User-Agent']; + ksort($headers); + foreach ($headers as $name => $value) { + if (in_array($name, $headersToSign) || substr($name, 0, 7) === 'X-Bunq-') { + $toSign .= $name . ': ' . $value . "\n"; + } + } + $toSign .= "\n" . $data; + $signature = ''; + + openssl_sign($toSign, $signature, $this->privateKey, OPENSSL_ALGO_SHA256); + $signature = base64_encode($signature); + + return $signature; + } + + protected function getDefaultHeaders(): array + { + return [ + 'X-Bunq-Client-Request-Id' => uniqid('sander'), + 'Cache-Control' => 'no-cache', + 'User-Agent' => 'pre-Firefly III test thing', + 'X-Bunq-Language' => 'en_US', + 'X-Bunq-Region' => 'nl_NL', + 'X-Bunq-Geolocation' => '0 0 0 0 NL', + ]; + } + + /** + * @param string $key + * @param array $response + * + * @return array + */ + protected function getKeyFromResponse(string $key, array $response): array + { + if (isset($response['Response'])) { + foreach ($response['Response'] as $entry) { + $currentKey = key($entry); + $data = current($entry); + if ($currentKey === $key) { + return $data; + } + } + } + + return []; + } + + /** + * @param string $uri + * @param array $data + * @param array $headers + * + * @return array + * @throws Exception + */ + protected function sendSignedBunqGet(string $uri, array $data, array $headers): array + { + if (strlen($this->server) === 0) { + throw new Exception('No bunq server defined'); + } + + $body = json_encode($data); + $fullUri = $this->server . $uri; + $signature = $this->generateSignature('get', $uri, $headers, $body); + $headers['X-Bunq-Client-Signature'] = $signature; + try { + $response = Requests::get($fullUri, $headers); + } catch (Requests_Exception $e) { + return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]]; + } + + $body = $response->body; + $array = json_decode($body, true); + if ($this->isErrorResponse($array)) { + $this->throwResponseError($array); + } + $responseHeaders = $response->headers->getAll(); + $statusCode = $response->status_code; + if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) { + throw new Exception(sprintf('Could not verify signature for request to "%s"', $uri)); + } + $array['ResponseHeaders'] = $responseHeaders; + + return $array; + } + + /** + * @param string $uri + * @param array $data + * @param array $headers + * + * @return array + * @throws Exception + */ + protected function sendSignedBunqPost(string $uri, array $data, array $headers): array + { + $body = json_encode($data); + $fullUri = $this->server . $uri; + $signature = $this->generateSignature('post', $uri, $headers, $body); + $headers['X-Bunq-Client-Signature'] = $signature; + try { + $response = Requests::post($fullUri, $headers, $body); + } catch (Requests_Exception $e) { + return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]]; + } + + $body = $response->body; + $array = json_decode($body, true); + if ($this->isErrorResponse($array)) { + $this->throwResponseError($array); + } + $responseHeaders = $response->headers->getAll(); + $statusCode = $response->status_code; + if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) { + throw new Exception(sprintf('Could not verify signature for request to "%s"', $uri)); + } + $array['ResponseHeaders'] = $responseHeaders; + + return $array; + } + + /** + * @param string $uri + * @param array $data + * @param array $headers + * + * @return array + */ + protected function sendUnsignedBunqPost(string $uri, array $data, array $headers): array + { + $body = json_encode($data); + $fullUri = $this->server . $uri; + try { + $response = Requests::post($fullUri, $headers, $body); + } catch (Requests_Exception $e) { + return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]]; + } + $body = $response->body; + $responseHeaders = $response->headers->getAll(); + $array = json_decode($body, true); + if ($this->isErrorResponse($array)) { + $this->throwResponseError($array); + } + $array['ResponseHeaders'] = $responseHeaders; + + return $array; + } + + /** + * @param array $response + * + * @return bool + */ + private function isErrorResponse(array $response): bool + { + $key = key($response); + if ($key === 'Error') { + return true; + } + + return false; + } + + /** + * @param array $response + * + * @throws Exception + */ + private function throwResponseError(array $response) + { + echo '
' . print_r($response, true) . '

'; + $message = []; + if (isset($response['Error'])) { + foreach ($response['Error'] as $error) { + $message[] = $error['error_description']; + } + } + throw new Exception(join(', ', $message)); + } + + /** + * @param string $body + * @param array $headers + * @param int $statusCode + * + * @return bool + * @throws Exception + */ + private function verifyServerSignature(string $body, array $headers, int $statusCode): bool + { + $this->logger->debug('Going to verify signature for body+headers+status'); + $dataToVerify = $statusCode . "\n"; + $verifyHeaders = []; + + // false when no public key is present + if (is_null($this->serverPublicKey)) { + $this->logger->error('No public key present in class, so return FALSE.'); + + return false; + } + //$this->logger->debug('Given headers', $headers); + foreach ($headers as $header => $value) { + + // skip non-bunq headers or signature + if (substr($header, 0, 7) !== 'x-bunq-' || $header === 'x-bunq-server-signature') { + continue; + } + // need to have upper case variant of header: + if (!isset($this->upperCaseHeaders[$header])) { + throw new Exception(sprintf('No upper case variant for header "%s"', $header)); + } + $header = $this->upperCaseHeaders[$header]; + $verifyHeaders[$header] = $value[0]; + } + // sort verification headers: + ksort($verifyHeaders); + + //$this->logger->debug('Final headers for verification', $verifyHeaders); + + // add them to data to sign: + foreach ($verifyHeaders as $header => $value) { + $dataToVerify .= $header . ': ' . trim($value) . "\n"; + } + + $signature = $headers['x-bunq-server-signature'][0]; + $dataToVerify .= "\n" . $body; + + //$this->logger->debug(sprintf('Signature to verify: "%s"', $signature)); + + $result = openssl_verify($dataToVerify, base64_decode($signature), $this->serverPublicKey->getPublicKey(), OPENSSL_ALGO_SHA256); + + if (is_int($result) && $result < 1) { + $this->logger->error(sprintf('Result of verification is %d, return false.', $result)); + + return false; + } + if (!is_int($result)) { + $this->logger->error(sprintf('Result of verification is a boolean (%d), return false.', $result)); + } + $this->logger->info('Signature is a match, return true.'); + + return true; + } +} \ No newline at end of file diff --git a/app/Services/Bunq/Request/InstallationTokenRequest.php b/app/Services/Bunq/Request/InstallationTokenRequest.php new file mode 100644 index 0000000000..d23e573a18 --- /dev/null +++ b/app/Services/Bunq/Request/InstallationTokenRequest.php @@ -0,0 +1,149 @@ + $this->publicKey,]; + $headers = $this->getDefaultHeaders(); + $response = []; + if ($this->fake) { + $response = json_decode( + '{"Response":[{"Id":{"id":875936}},{"Token":{"id":13172597,"created":"2017-08-05 11:46:07.061740","updated":"2017-08-05 11:46:07.061740","token":"35278fcc8b0615261fe23285e6d2e6ccd05ac4c93454981bd5e985ec453e5b5d"}},{"ServerPublicKey":{"server_public_key":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAon5y6OZGvTN8kIqPBdro\ndG8TWVw6sl34hAWI47NK6Pi7gmnTtd\/k9gfwq56iI4Er8uMM5e4QmjD++XrBIqcw\nHohDVK03li3xsyJPZ4EBSUOkv4VKXKL\/quqlSgDmPnxtT39BowUZl1um5QbTm0hW\npGI\/0bK7jQk7mbEan9yDOpXnczKgfNlo4o+zbFquPdUfA5LE8R8X057dB6ab7eqA\n9Aybo+I6xyrsOOztufg3Yfe5RA6a0Sikqe\/L8HCP+9TJByUI2pwydPou3KONfYhK\n1NQJZ+RCZ6V+jmcuzKe2vq0jhBZd26wNscl48Sm7etJeuBOpHE+MgO24JiTEYlLS\nVQIDAQAB\n-----END PUBLIC KEY-----\n"}}]}', + true + ); + } + if (!$this->fake) { + $response = $this->sendUnsignedBunqPost($uri, $data, $headers); + } + //echo '
' . json_encode($response) . '

'; + + $this->installationId = $this->extractInstallationId($response); + $this->serverPublicKey = $this->extractServerPublicKey($response); + $this->installationToken = $this->extractInstallationToken($response); + + return; + } + + /** + * @return InstallationId + */ + public function getInstallationId(): InstallationId + { + return $this->installationId; + } + + /** + * @return InstallationToken + */ + public function getInstallationToken(): InstallationToken + { + return $this->installationToken; + } + + /** + * @return string + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * @param string $publicKey + */ + public function setPublicKey(string $publicKey) + { + $this->publicKey = $publicKey; + } + + /** + * @return ServerPublicKey + */ + public function getServerPublicKey(): ServerPublicKey + { + return $this->serverPublicKey; + } + + /** + * @param bool $fake + */ + public function setFake(bool $fake) + { + $this->fake = $fake; + } + + /** + * @param array $response + * + * @return InstallationId + */ + private function extractInstallationId(array $response): InstallationId + { + $installationId = new InstallationId; + $data = $this->getKeyFromResponse('Id', $response); + $installationId->setId(intval($data['id'])); + + return $installationId; + + } + + /** + * @param array $response + * + * @return InstallationToken + */ + private function extractInstallationToken(array $response): InstallationToken + { + + $data = $this->getKeyFromResponse('Token', $response); + $installationToken = new InstallationToken($data); + + return $installationToken; + } + + /** + * @param array $response + * + * @return ServerPublicKey + */ + private function extractServerPublicKey(array $response): ServerPublicKey + { + $data = $this->getKeyFromResponse('ServerPublicKey', $response); + $serverPublicKey = new ServerPublicKey($data); + + return $serverPublicKey; + } +} \ No newline at end of file diff --git a/app/Services/Bunq/Token/BunqToken.php b/app/Services/Bunq/Token/BunqToken.php new file mode 100644 index 0000000000..7cb9ba486d --- /dev/null +++ b/app/Services/Bunq/Token/BunqToken.php @@ -0,0 +1,89 @@ +makeTokenFromResponse($response); + } + + /** + * @return Carbon + */ + public function getCreated(): Carbon + { + return $this->created; + } + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @return string + */ + public function getToken(): string + { + return $this->token; + } + + /** + * @return Carbon + */ + public function getUpdated(): Carbon + { + return $this->updated; + } + + /** + * @param array $response + */ + protected function makeTokenFromResponse(array $response): void + { + $this->id = $response['id']; + $this->created = Carbon::createFromFormat('Y-m-d H:i:s.u', $response['created']); + $this->updated = Carbon::createFromFormat('Y-m-d H:i:s.u', $response['updated']); + $this->token = $response['token']; + + return; + } + +} \ No newline at end of file diff --git a/app/Services/Bunq/Token/InstallationToken.php b/app/Services/Bunq/Token/InstallationToken.php new file mode 100644 index 0000000000..59777b0f3a --- /dev/null +++ b/app/Services/Bunq/Token/InstallationToken.php @@ -0,0 +1,24 @@ +