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
2 changes: 1 addition & 1 deletion lib/Controller/PushController.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ public function registerDevice(string $pushTokenHash, string $devicePublicKey, s
$key = $this->identityProof->getKey($user);

$deviceIdentifier = json_encode([$user->getCloudId(), $token->getId()]);
openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512);
$deviceIdentifier = base64_encode(hash('sha512', $deviceIdentifier, true));
openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512);

$appType = 'unknown';
if ($this->request->isUserAgent([
Expand Down
109 changes: 105 additions & 4 deletions tests/Integration/features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ class FeatureContext implements Context, SnippetAcceptingContext {
/** @var string */
protected $lastEtag;

/** @var resource */
protected $deviceKey;

/** @var string[] */
protected $appPasswords;

/**
* FeatureContext constructor.
*/
Expand Down Expand Up @@ -221,6 +227,99 @@ public function deleteAllNotification($api) {
$this->sendingTo('DELETE', '/apps/notifications/api/' . $api . '/notifications');
}

/**
* @Then /^user "([^"]*)" unregisters from push notifications/
*
* @param string $user
*/
public function unregisterForPushNotifications(string $user) {
$currentUser = $this->currentUser;
$this->setCurrentUser($user);
$this->sendingToWith('DELETE', '/apps/notifications/api/v2/push?format=json');
$this->setCurrentUser($currentUser);
}

/**
* @Then /^user "([^"]*)" registers for push notifications with$/
*
* @param string $user
* @param TableNode|null $formData
*/
public function registerForPushNotifications(string $user, TableNode $formData) {
$data = $formData->getRowsHash();

if ($data['devicePublicKey'] === 'VALID_KEY') {
$config = [
'digest_alg' => 'sha512',
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
$this->deviceKey = openssl_pkey_new($config);
$keyDetails = openssl_pkey_get_details($this->deviceKey);
$publicKey = $keyDetails['key'];

$data['devicePublicKey'] = $publicKey;
}

$currentUser = $this->currentUser;
$this->setCurrentUser($user);
$this->sendingToWith('POST', '/apps/notifications/api/v2/push?format=json', $data);
$this->setCurrentUser($currentUser);
}

/**
* @Then /^can validate the response and signature$/
*/
public function validateResponseAndSignature(): void {
$response = $this->getArrayOfNotificationsResponded($this->response);

Assert::assertStringStartsWith('-----BEGIN PUBLIC KEY-----' . "\n", $response['publicKey']);
Assert::assertStringEndsWith('-----END PUBLIC KEY-----' . "\n", $response['publicKey']);
Assert::assertNotEmpty($response['deviceIdentifier'], 'Device identifier should not be empty');
Assert::assertNotEmpty($response['signature'], 'Signature should not be empty');

$result = openssl_verify($response['deviceIdentifier'], base64_decode($response['signature']), $response['publicKey'], OPENSSL_ALGO_SHA512);
Assert::assertEquals(true, $result, 'Failed to verify the signature');
}

/**
* @Then /^user "([^"]*)" creates an app password$/
*
* @param string $user
*/
public function createAppPassword(string $user) {
$currentUser = $this->currentUser;
$this->setCurrentUser($user);
$this->sendingToWith('GET', '/core/getapppassword?format=json');
$this->setCurrentUser($currentUser);

$response = $this->getArrayOfNotificationsResponded($this->response);
Assert::assertNotEquals('', $response['apppassword']);
$this->appPasswords[$user] = $response['apppassword'];
}

/**
* @Then /^user "([^"]*)" forgets the app password$/
*
* @param string $user
*/
public function removeAppPassword(string $user) {
unset($this->appPasswords[$user]);
}

/**
* @Then /^error "([^"]*)" is expected with status code ([0-9]*)$/
*
* @param string $error
* @param int $statusCode
*/
public function expectedErrorOnLastRequest(string $error, int $statusCode) {
$this->assertStatusCode($this->response, $statusCode);
$response = $this->getArrayOfNotificationsResponded($this->response);

Assert::assertEquals($error, $response['message']);
}

/**
* @Then /^status code is ([0-9]*)$/
*
Expand Down Expand Up @@ -347,15 +446,17 @@ public function sendingTo(string $verb, string $url) {
* @When /^sending "([^"]*)" to "([^"]*)" with$/
* @param string $verb
* @param string $url
* @param TableNode $body
* @param TableNode|array|null $body
* @param array $headers
*/
public function sendingToWith(string $verb, string $url, TableNode $body = null, array $headers = []) {
public function sendingToWith(string $verb, string $url, $body = null, array $headers = []) {
$fullUrl = $this->baseUrl . 'ocs/v2.php' . $url;
$client = new Client();
$options = [];
if ($this->currentUser === 'admin') {
$options['auth'] = ['admin', 'admin'];
if (isset($this->appPasswords[$this->currentUser])) {
$options['auth'] = [$this->currentUser, $this->appPasswords[$this->currentUser]];
} elseif ($this->currentUser === 'admin') {
$options['auth'] = [$this->currentUser, 'admin'];
} else {
$options['auth'] = [$this->currentUser, '123456'];
}
Expand Down
64 changes: 64 additions & 0 deletions tests/Integration/features/push-registration.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Feature: Push registration
Background:
Given user "test1" exists
Given as user "test1"

Scenario: Invalid push token hash
Given user "test1" registers for push notifications with
| pushTokenHash | 12345 |
| devicePublicKey | INVALID_KEY |
| proxyServer | nextcloud |
Then error "INVALID_PUSHTOKEN_HASH" is expected with status code 400

Scenario: Invalid device key
Given user "test1" registers for push notifications with
| pushTokenHash | 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 |
| devicePublicKey | INVALID_KEY |
| proxyServer | nextcloud |
Then error "INVALID_DEVICE_KEY" is expected with status code 400

Scenario: Invalid proxy server
Given user "test1" registers for push notifications with
| pushTokenHash | 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 |
| devicePublicKey | VALID_KEY |
| proxyServer | nextcloud |
Then error "INVALID_PROXY_SERVER" is expected with status code 400

Scenario: Invalid session token: not using an app password
Given user "test1" registers for push notifications with
| pushTokenHash | 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 |
| devicePublicKey | VALID_KEY |
| proxyServer | https://push-notifications.nextcloud.com/ |
Then error "INVALID_SESSION_TOKEN" is expected with status code 400

Scenario: Successful registration
Given user "test1" creates an app password
Given user "test1" registers for push notifications with
| pushTokenHash | 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 |
| devicePublicKey | VALID_KEY |
| proxyServer | https://push-notifications.nextcloud.com/ |
Then status code is 201
And can validate the response and signature

Scenario: Unregistering from push notifications without app password
Given user "test1" forgets the app password
Given user "test1" unregisters from push notifications
Then error "INVALID_SESSION_TOKEN" is expected with status code 400

Scenario: Unregistering from push notifications successfully
Given user "test1" creates an app password
Given user "test1" registers for push notifications with
| pushTokenHash | 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678 |
| devicePublicKey | VALID_KEY |
| proxyServer | https://push-notifications.nextcloud.com/ |
Then status code is 201
And can validate the response and signature
Given user "test1" unregisters from push notifications
Then status code is 202
Given user "test1" unregisters from push notifications
Then status code is 200

Scenario: Unregistering from push notifications without registering
Given user "test1" creates an app password
Given user "test1" unregisters from push notifications
Then status code is 200
4 changes: 2 additions & 2 deletions tests/Unit/Controller/PushControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ public function dataRegisterDevice() {
[
'publicKey' => $this->userPublicKey,
'deviceIdentifier' => 'XUCEZ1EHvTUcVhIvrQQQ1XcP0ZD2BFdFqw4EYbOhBfiEgXgirurR4x/ve4GSSyfivvbQOdOkZUM+g4m+tSb0Ew==',
'signature' => 'LRhbXO71WYX9qqDbQX7C+87YaaFfWoT/vG0DlaXdBz6+lhyOA0dw/1Ggz3fd7RerCQ0MfgnnTyxO+cSeRpUaPdA2yPjfoiPpfYA5SOJQGF3comS/HYna3fHiFDbOoM3BJOnjvqiSZdxA/ICdyl2mEEC5wO7AZ4OZKBTa5XfL7eSCXZLEv1YldqcLOStbXrI7voDQocTMJxoQZI/j8BVcf2i3D6F454aXIFDrYYzC2PQY+CKJoXZW0m0RMWaTM2B8tBmFFwrmaGLDqcjjpd33TsTtsV5DB7WimffLBPpOuGV4Z1Kiagp/mxpPLz2NImNV79mDX9gY3ZppCZTwChP5qQ==',
'signature' => 'X9+J7NNLfG9Ft6C36zrYLVJ5aH5euIROzdV937hsU81jL7WvOwzBfc7bImzxU3Bnev5wEKwkw7Ts/2q/+UUkOxgtEZinp52s87S5obKtsVXsczHbsqg4p/ueoBPhF17VsP1e8kMtxZ4snk/iArX4Eu1cfaM3+OckmpO0MYXy0rUbYpQPAJo4VgRFKKjFvfEVOj8N74DTIJ+TjRsvvDhJbb9KpeFe3a6Rv9mIo0AqoK+deAbUkWY0aM+74noVXvPtNzExgK4mWJ02+JHEuQEUbCuQsgoBia0vC3fILbwVxHzrieWGEnE7vkRyFEzlkeo7ZSMawDPxsPN5HxwBs2SZig==',
],
Http::STATUS_CREATED,
],
Expand All @@ -317,7 +317,7 @@ public function dataRegisterDevice() {
[
'publicKey' => $this->userPublicKey,
'deviceIdentifier' => 'x9vSImcGjhzR9BfZ/XbbUqqCCNC4bHKsX7vkQWNZRd1/MiY+OuF02fx8K08My0RpkNnwj/rQ/gVSU1oEdFwkww==',
'signature' => 'J9AcdJt5youJmMnBhS+Cc9ytArynIKtCRoNf/m0oOFO/e0hWHqs1NRdQBe81qzYIjf0+bj0Q97X9Xv1rnVJesPkQUbGaa4nAPt+viGSfvzTptjX4LKgqm8B3UkduBA262IcaWgM5P84gUqelkQIC1nIqq/MJTuC6oQ5lUwIV1a92ZurDjhwH4b3f7/ZLTTOTRD0DWN9W/yOyF1qECivgePR3eu+mkcBzXVU/TDZDJic9G7xhqcTnWV6qk+aKyzdNo1tu5W7mF+v5vF6rrGZrq55vPLWAHApTD7P+NFV01BnaCuN7/qGJNVs7m7EH03jpOw7y3jqNMmcmonYrJSMVqg==',
'signature' => 'GFpnv3MO7mcBef2RJ4Ayrl6RQakGM7AvlKhoTr3DUWnv+iBzwGy8YV34HIPoArz4tyqonHRlLsxPYq4ENPfGO99KrIS16z4RUq0wiCBGf+S8/K8lM9cE9EBKE9yrkTsSvZGICEusvxQ+cTfVr30bnavvi1wL1UuxxDBlJebda9FJ9HfaS24j4rT7K78oMguqDVM+4hhr6BMhcpUVV+kTpOaBpluw5pRDwUP3jJBmkkOa57WRKFcu0Lr/XIx/G0c8Si+BAfM//CTMstwp5XDFn4W9EYSStjNrvsULdV+tOKFwnowqts+UFzEDvmZ1g4qIMWUUPBF4/pjaiDqtMojgrA==',
],
Http::STATUS_OK,
],
Expand Down