Create spec to secure communication with the WebView #320

Open
yaronyg opened this Issue Nov 12, 2015 · 5 comments

Comments

Projects
None yet
3 participants
@yaronyg
Member

yaronyg commented Nov 12, 2015

We need to define for iOS and Android (and hopefully soon UWP) how we secure communications between the WebView and the JXCore instance so random apps on the device can't just connect to localhost and send random commands to our node code. See http://thaliproject.org/SecuringCordovaAndNodeJs/ for details.

@yaronyg yaronyg self-assigned this Nov 12, 2015

@yaronyg yaronyg added ready 1 - Ready and removed 1 - Ready labels Nov 12, 2015

@yaronyg yaronyg added Security 0 - Icebox and removed 2 - Ready labels Jan 11, 2016

@yaronyg yaronyg added Icebox and removed ready labels Feb 9, 2016

@yaronyg yaronyg referenced this issue Feb 11, 2016

Merged

#318 ACL Spec #520

@yaronyg yaronyg removed their assignment Feb 23, 2016

@mohlsen

This comment has been minimized.

Show comment
Hide comment
@mohlsen

mohlsen Apr 28, 2016

Member

@dakr0 has been working on this for @project_stanton, using salti-admin as part of the process. We would be happy to write something up for this. @yaronyg how would you like us to post the spec?

Member

mohlsen commented Apr 28, 2016

@dakr0 has been working on this for @project_stanton, using salti-admin as part of the process. We would be happy to write something up for this. @yaronyg how would you like us to post the spec?

@yaronyg

This comment has been minimized.

Show comment
Hide comment
Member

yaronyg commented Apr 28, 2016

@yaronyg yaronyg added this to the V1 milestone Aug 3, 2016

@yaronyg yaronyg added 1 - Backlog and removed 0 - Icebox labels Aug 4, 2016

@yaronyg yaronyg added P0 and removed Icebox labels Aug 8, 2016

@yaronyg

This comment has been minimized.

Show comment
Hide comment
@yaronyg

yaronyg Aug 8, 2016

Member

Where is the spec on this?

Member

yaronyg commented Aug 8, 2016

Where is the spec on this?

@larryonoff

This comment has been minimized.

Show comment
Hide comment
@larryonoff

larryonoff Aug 9, 2016

Contributor

@yaronyg please see below the document that captures the investigation results available so far and detail the next steps necessary to find a suitable solution to the issue.

Suggested approach

As of today, we've evaluated the potential approaches outlined in Thali documentation:

  1. Secret in the URL
  2. Secret in a cookie
  3. Secret in a HTTP header
  4. TLS mutual authentication

The solutions 1-3 all have potential issues (including, but not limited to those described in Thali documentation), therefore we believe that proceeding with solution 4 TLS mutual authentication - is the most promising. This approach should work as follows:

  1. App generates client and server certificates when it starts and saves them into a shared storage.
  2. When node.js server starts, it uses server certificates generated on step 1.
  3. WebView uses client certificates generated on step 1 by sending them with any request to node.js server.

Each of these steps has a number of open issues, which are described below.

Further steps necessary

In order to provide a definite recommendation on a solution, we need to continue investigation of the applicability of TLS mutual authentication approach to JXcore to WebView paring process and create an actual proof of concept. This will involve covering the following points:

  1. Investigate certificate generation to allocate this to a correct system module. Decide when exactly should the certificates be generated (on first launch only or on each launch).
  2. Verify that it is possible to organize a shared local storage for WebView and node.js server.
  3. Configure JXcore server to use the generated certificate at startup.
  4. Extensively investigate the issue of support for client certificate authorization by different WebViews on iOS and Android. Based on the platforms official documentation, we expect that there will be no issues with support of different iOS versions (refer to URLLoadingSystem/AuthenticationChallenges) - however, it still needs to be confirmed in the proof of concept. What concerns the Android - it's going to present a number of challenges. According to official documentation, client certificate authorization for WebView is available only for Android 5.0 and up (i.e. API level 21 - WebKit/WebViewClient/onReceivedClientCertRequest). However, there are several workarounds we could try to make it work on Android 4.0 and up.
  5. Verify the necessity of implementing custom WebViews for iOS and Android for handling client certificate authorization (may or may not be necessary based on the outcomes of point 4 above).
  6. If the experimental approaches for lower version of Android described in the point above do not work, it will be necessary to investigate alternative solutions for this class of devices (e.g. a combination of HTTPS and secrets in HTTP headers).
Contributor

larryonoff commented Aug 9, 2016

@yaronyg please see below the document that captures the investigation results available so far and detail the next steps necessary to find a suitable solution to the issue.

Suggested approach

As of today, we've evaluated the potential approaches outlined in Thali documentation:

  1. Secret in the URL
  2. Secret in a cookie
  3. Secret in a HTTP header
  4. TLS mutual authentication

The solutions 1-3 all have potential issues (including, but not limited to those described in Thali documentation), therefore we believe that proceeding with solution 4 TLS mutual authentication - is the most promising. This approach should work as follows:

  1. App generates client and server certificates when it starts and saves them into a shared storage.
  2. When node.js server starts, it uses server certificates generated on step 1.
  3. WebView uses client certificates generated on step 1 by sending them with any request to node.js server.

Each of these steps has a number of open issues, which are described below.

Further steps necessary

In order to provide a definite recommendation on a solution, we need to continue investigation of the applicability of TLS mutual authentication approach to JXcore to WebView paring process and create an actual proof of concept. This will involve covering the following points:

  1. Investigate certificate generation to allocate this to a correct system module. Decide when exactly should the certificates be generated (on first launch only or on each launch).
  2. Verify that it is possible to organize a shared local storage for WebView and node.js server.
  3. Configure JXcore server to use the generated certificate at startup.
  4. Extensively investigate the issue of support for client certificate authorization by different WebViews on iOS and Android. Based on the platforms official documentation, we expect that there will be no issues with support of different iOS versions (refer to URLLoadingSystem/AuthenticationChallenges) - however, it still needs to be confirmed in the proof of concept. What concerns the Android - it's going to present a number of challenges. According to official documentation, client certificate authorization for WebView is available only for Android 5.0 and up (i.e. API level 21 - WebKit/WebViewClient/onReceivedClientCertRequest). However, there are several workarounds we could try to make it work on Android 4.0 and up.
  5. Verify the necessity of implementing custom WebViews for iOS and Android for handling client certificate authorization (may or may not be necessary based on the outcomes of point 4 above).
  6. If the experimental approaches for lower version of Android described in the point above do not work, it will be necessary to investigate alternative solutions for this class of devices (e.g. a combination of HTTPS and secrets in HTTP headers).
@mohlsen

This comment has been minimized.

Show comment
Hide comment
@mohlsen

mohlsen Aug 9, 2016

Member

We also never posted @dakr0's article which handled everything before @larryonoff's research.
Probably still needs reviewed, editing, and cleanup. But wanted to post.


How to secure communication for a Cordova-based application between a client side and an internal server based on JXCore

Lets assume that we build a hybrid application based on the most popular framework:
Cordova, and the application consists of:

  • A client side (such as HTML and JavaScript).

    Its content is embedded in a native application, stored in files, managed and presented on the WebView by Cordova's
    mechanism. In this way we don't need any Web server.

  • An internal server side.

    It's implemented using Node.js provided by JXCore.

    The server side supplies some functionality that requires communication between these two parts, but we
    also need another communication channel apart from the JXCore bridge (for example, if we want to run the PouchDB
    server and have access to the databases from the client side only).

    The problem is: how can we do it in a secure way?

How about using HTTPS?

You may ask why not just use the HTTPS protocol? It seems that it should be the preferred solution!

Yes it should, but it would require generating a self-signed certificate for the internal HTTPS server.
Since our hybrid application runs in the WebView, using self-signed certificates may be a bit tricky,
because such certificates are not accepted by default. Additionally, our application must somehow supply
a private key on the internal server.

So, is there any other solution to meet our needs?

HTTP, SALTI-Admin and JXCore bridge

It turns out that the solution may be provided by the SALTI-Admin library.

The library allows you to generate a simple token and provides mechanism (implemented as Express Middleware)
that verifies if the given request is authorized to access to a resource. To communicate between the client side and
the internal server side we can use the HTTP protocol and requests with the Authorization header that should contain
a valid SALTI token.

A simple implementation on the internal server side may look like this:

var express = require('express');
var cors    = require('cors');
var saltiAdmin = require('salti-admin');

var SALTI_TOKEN = 'salti-token';
var app = express();

var config = {
    port: 3000
};

// Setting CORS to accept requests from well-known origins only
// ...

// Generating a SALTI token and activating middleware
saltiAdmin.generateSecret(function (err, token) {
    if (!err) {

        app.set(SALTI_TOKEN, token);

        // Now we can create middleware that will use the given token.
        // In this way all the requests will have to be authorized.
        app.use(saltiAdmin.acl({ secret: token }));

        // Sample request handler
        app.get('/test', function (req, res) {
            res.json({ testOk: true });
        });

    } else {
        console.error('Cannot generate SALTI token: ', err);
    }
});


app.listen(config.port);

For now all the requests must have an appropriate authorization token passed in req.headers.authorization.

But how can we provide the token to the client side in a secure way?

The solution may be JXCore bridge. So we have to register a function on the server side like this:

Mobile('getToken').registerSync(function () {
    var token = app.get(SALTI_TOKEN);
    return token ? saltiAdmin.DEFAULT_PREFIX + token : token;
});

And now we can implement an appropriate function on the client side by using Cordova JXcore Plugin:

var saltiToken;
// ...

jxcore('getToken').call(function(token, err) {
    if (!err) {
        saltiToken = token;
    } else {
        // Report an error
        // ...
    }
});
// ...

If you use AngularJs, you can implement it as a service (that is a singleton), like this:

angular.module('myApp')

.factory('saltiAuthorization', function ($q) {
    "use strict";

    var saltiToken;

    function getToken() {
        if (saltiToken) {
            return $q.when(saltiToken);
        } else if (typeof jxcore !== "undefined") {
            var deferred = $q.defer();

            // Gets token by using JXCore bridge
            jxcore('getToken').call(function(token, err) {
                if (!err) {
                    saltiToken = token;
                    deferred.resolve(token);
                } else {
                    deferred.reject(err);
                }
            });

            return deferred.promise;
        }

        return $q.when();
    }

    return {
        getToken: getToken
    };
});

Since the client side is now able to get a valid SALTI token, the only thing left to do is to pass the token to
the header of a request. Assuming that our application is built on top of AngularJs, we can configure each HTTP
request in the $http service or, which is more elegant, implement an HTTP interceptor like this:

angular.module('myApp')

.config(function($httpProvider){
    "use strict";

    if (typeof jxcore !== "undefined") {
        $httpProvider.interceptors.push('saltiAuthorizationInterceptor');
    }

})

.factory('saltiAuthorizationInterceptor',
    function ($q, saltiAuthorization) {
        "use strict";

        var internalServerUrl = 'http://localhost:3000';

        return {
            request: function (config) {
                if (config &&
                    config.url &&
                    config.url.startsWith(internalServerUrl)) {

                    return saltiAuthorization.getToken()
                        .then(function (saltiToken) {
                            if (saltiToken && config.headers) {
                                config.headers.Authorization = saltiToken;
                            }
                            return config;
                        });

                }

                return $q.when(config);
            }
        };
    }
);

But what if we also want to use PouchDB?

Then the implementation on the server side may look like this:

// ...

var PouchDB = require('pouchdb');

// Configuring PouchDB
// ...

// ...

saltiAdmin.generateSecret(function (err, token) {
    if (!err) {

        app.set(SALTI_TOKEN, token);

        // Now we can create middleware that will use the given token.
        // In this way all the requests will have to be authorized.
        app.use(saltiAdmin.acl({ secret: token }));

        // Sample request handler
        app.get('/test', function (req, res) {
            res.json({ testOk: true });
        });

        app.use('/dbs', require('express-pouchdb')(PouchDB, pouchOptions));

    } else {
        console.error('Cannot generate SALTI token: ', err);
    }
});

// ...

And on the client side we need to configure ajax.headers in the PouchDB options:

pouchOptions.ajax.headers = {
    'Authorization' : saltiToken
};

var db = new PouchDB('http://localhost:3000/dbs/myDb', pouchOptions);

In the examples presented above we use the same SALTI token for several resources (just for the purposes of this case
study), but in the real-life environment we should generate different tokens for different resources.

We also should remember that our internal server should be protected from being accessed via HTTP by any device except
the one on which the application runs. In this case, apart from using an appropriate CORS definition that allows
the server to accept requests from well-known origin (in our case from localhost), we can implement an additional
mechanism to make the protection stronger. There are many ways to do it, but let’s take a look at one of them. Since
we send the requests from the client side to the internal server that is seen as localhost, we can just verify if
an incoming request refers to localhost with a valid port number and whether it comes from localhost.
To cover the first case we can verify req.hostname, like this:

// ...

var allowedHost = 'localhost' + (config.port !== 80 ? ':' + config.port : '');

// ...

// A middleware that verifies if requests come to localhost
app.use(function (req, res, next) {
    if (req.hostname === allowedHost) {
        next();
    } else {
        res.sendStatus(403);
    }
});

For the second case we can verify whether req.ip is equal to 127.0.0.1. So the extended version of the middleware
presented above may look like this:

// A middleware that verifies if requests come from/to localhost
app.use(function (req, res, next) {
    if (req.hostname === allowedHost && req.ip === '127.0.0.1') {
        next();
    } else {
        res.sendStatus(403);
    }
});

It should be noted that the presented solution doesn't protect from some "bad" application that can run on the same
device as our application and hence send acceptable requests with a valid req.hostname and req.ip.

Member

mohlsen commented Aug 9, 2016

We also never posted @dakr0's article which handled everything before @larryonoff's research.
Probably still needs reviewed, editing, and cleanup. But wanted to post.


How to secure communication for a Cordova-based application between a client side and an internal server based on JXCore

Lets assume that we build a hybrid application based on the most popular framework:
Cordova, and the application consists of:

  • A client side (such as HTML and JavaScript).

    Its content is embedded in a native application, stored in files, managed and presented on the WebView by Cordova's
    mechanism. In this way we don't need any Web server.

  • An internal server side.

    It's implemented using Node.js provided by JXCore.

    The server side supplies some functionality that requires communication between these two parts, but we
    also need another communication channel apart from the JXCore bridge (for example, if we want to run the PouchDB
    server and have access to the databases from the client side only).

    The problem is: how can we do it in a secure way?

How about using HTTPS?

You may ask why not just use the HTTPS protocol? It seems that it should be the preferred solution!

Yes it should, but it would require generating a self-signed certificate for the internal HTTPS server.
Since our hybrid application runs in the WebView, using self-signed certificates may be a bit tricky,
because such certificates are not accepted by default. Additionally, our application must somehow supply
a private key on the internal server.

So, is there any other solution to meet our needs?

HTTP, SALTI-Admin and JXCore bridge

It turns out that the solution may be provided by the SALTI-Admin library.

The library allows you to generate a simple token and provides mechanism (implemented as Express Middleware)
that verifies if the given request is authorized to access to a resource. To communicate between the client side and
the internal server side we can use the HTTP protocol and requests with the Authorization header that should contain
a valid SALTI token.

A simple implementation on the internal server side may look like this:

var express = require('express');
var cors    = require('cors');
var saltiAdmin = require('salti-admin');

var SALTI_TOKEN = 'salti-token';
var app = express();

var config = {
    port: 3000
};

// Setting CORS to accept requests from well-known origins only
// ...

// Generating a SALTI token and activating middleware
saltiAdmin.generateSecret(function (err, token) {
    if (!err) {

        app.set(SALTI_TOKEN, token);

        // Now we can create middleware that will use the given token.
        // In this way all the requests will have to be authorized.
        app.use(saltiAdmin.acl({ secret: token }));

        // Sample request handler
        app.get('/test', function (req, res) {
            res.json({ testOk: true });
        });

    } else {
        console.error('Cannot generate SALTI token: ', err);
    }
});


app.listen(config.port);

For now all the requests must have an appropriate authorization token passed in req.headers.authorization.

But how can we provide the token to the client side in a secure way?

The solution may be JXCore bridge. So we have to register a function on the server side like this:

Mobile('getToken').registerSync(function () {
    var token = app.get(SALTI_TOKEN);
    return token ? saltiAdmin.DEFAULT_PREFIX + token : token;
});

And now we can implement an appropriate function on the client side by using Cordova JXcore Plugin:

var saltiToken;
// ...

jxcore('getToken').call(function(token, err) {
    if (!err) {
        saltiToken = token;
    } else {
        // Report an error
        // ...
    }
});
// ...

If you use AngularJs, you can implement it as a service (that is a singleton), like this:

angular.module('myApp')

.factory('saltiAuthorization', function ($q) {
    "use strict";

    var saltiToken;

    function getToken() {
        if (saltiToken) {
            return $q.when(saltiToken);
        } else if (typeof jxcore !== "undefined") {
            var deferred = $q.defer();

            // Gets token by using JXCore bridge
            jxcore('getToken').call(function(token, err) {
                if (!err) {
                    saltiToken = token;
                    deferred.resolve(token);
                } else {
                    deferred.reject(err);
                }
            });

            return deferred.promise;
        }

        return $q.when();
    }

    return {
        getToken: getToken
    };
});

Since the client side is now able to get a valid SALTI token, the only thing left to do is to pass the token to
the header of a request. Assuming that our application is built on top of AngularJs, we can configure each HTTP
request in the $http service or, which is more elegant, implement an HTTP interceptor like this:

angular.module('myApp')

.config(function($httpProvider){
    "use strict";

    if (typeof jxcore !== "undefined") {
        $httpProvider.interceptors.push('saltiAuthorizationInterceptor');
    }

})

.factory('saltiAuthorizationInterceptor',
    function ($q, saltiAuthorization) {
        "use strict";

        var internalServerUrl = 'http://localhost:3000';

        return {
            request: function (config) {
                if (config &&
                    config.url &&
                    config.url.startsWith(internalServerUrl)) {

                    return saltiAuthorization.getToken()
                        .then(function (saltiToken) {
                            if (saltiToken && config.headers) {
                                config.headers.Authorization = saltiToken;
                            }
                            return config;
                        });

                }

                return $q.when(config);
            }
        };
    }
);

But what if we also want to use PouchDB?

Then the implementation on the server side may look like this:

// ...

var PouchDB = require('pouchdb');

// Configuring PouchDB
// ...

// ...

saltiAdmin.generateSecret(function (err, token) {
    if (!err) {

        app.set(SALTI_TOKEN, token);

        // Now we can create middleware that will use the given token.
        // In this way all the requests will have to be authorized.
        app.use(saltiAdmin.acl({ secret: token }));

        // Sample request handler
        app.get('/test', function (req, res) {
            res.json({ testOk: true });
        });

        app.use('/dbs', require('express-pouchdb')(PouchDB, pouchOptions));

    } else {
        console.error('Cannot generate SALTI token: ', err);
    }
});

// ...

And on the client side we need to configure ajax.headers in the PouchDB options:

pouchOptions.ajax.headers = {
    'Authorization' : saltiToken
};

var db = new PouchDB('http://localhost:3000/dbs/myDb', pouchOptions);

In the examples presented above we use the same SALTI token for several resources (just for the purposes of this case
study), but in the real-life environment we should generate different tokens for different resources.

We also should remember that our internal server should be protected from being accessed via HTTP by any device except
the one on which the application runs. In this case, apart from using an appropriate CORS definition that allows
the server to accept requests from well-known origin (in our case from localhost), we can implement an additional
mechanism to make the protection stronger. There are many ways to do it, but let’s take a look at one of them. Since
we send the requests from the client side to the internal server that is seen as localhost, we can just verify if
an incoming request refers to localhost with a valid port number and whether it comes from localhost.
To cover the first case we can verify req.hostname, like this:

// ...

var allowedHost = 'localhost' + (config.port !== 80 ? ':' + config.port : '');

// ...

// A middleware that verifies if requests come to localhost
app.use(function (req, res, next) {
    if (req.hostname === allowedHost) {
        next();
    } else {
        res.sendStatus(403);
    }
});

For the second case we can verify whether req.ip is equal to 127.0.0.1. So the extended version of the middleware
presented above may look like this:

// A middleware that verifies if requests come from/to localhost
app.use(function (req, res, next) {
    if (req.hostname === allowedHost && req.ip === '127.0.0.1') {
        next();
    } else {
        res.sendStatus(403);
    }
});

It should be noted that the presented solution doesn't protect from some "bad" application that can run on the same
device as our application and hence send acceptable requests with a valid req.hostname and req.ip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment