diff --git a/src/v1/driver.js b/src/v1/driver.js index f9dc16671..ce2c40cbe 100644 --- a/src/v1/driver.js +++ b/src/v1/driver.js @@ -174,10 +174,11 @@ let USER_AGENT = "neo4j-javascript/" + VERSION; * options are as follows: * * { - * // Enable TLS encryption. This is on by default in modern NodeJS installs, + * // Encryption level: one of ENCRYPTION_ON, ENCRYPTION_OFF or ENCRYPTION_NON_LOCAL. + * // ENCRYPTION_NON_LOCAL is on by default in modern NodeJS installs, * // but off by default in the Web Bundle and old (<=1.0.0) NodeJS installs * // due to technical limitations on those platforms. - * encrypted: true|false, + * encrypted: ENCRYPTION_ON|ENCRYPTION_OFF|ENCRYPTION_NON_LOCAL * * // Trust strategy to use if encryption is enabled. There is no mode to disable * // trust other than disabling encryption altogether. The reason for diff --git a/src/v1/internal/ch-dummy.js b/src/v1/internal/ch-dummy.js index ae415051e..6fb55c711 100644 --- a/src/v1/internal/ch-dummy.js +++ b/src/v1/internal/ch-dummy.js @@ -29,6 +29,9 @@ class DummyChannel { constructor(opts) { this.written = []; } + isEncrypted() { + return false; + } write( buf ) { this.written.push(buf); observer.updateInstance(this); diff --git a/src/v1/internal/ch-node.js b/src/v1/internal/ch-node.js index 401147cfa..a77508333 100644 --- a/src/v1/internal/ch-node.js +++ b/src/v1/internal/ch-node.js @@ -23,6 +23,7 @@ import fs from 'fs'; import path from 'path'; import {EOL} from 'os'; import {NodeBuffer} from './buf'; +import {isLocalHost, ENCRYPTION_NON_LOCAL, ENCRYPTION_OFF} from './util'; import {newError} from './../error'; let _CONNECTION_IDGEN = 0; @@ -71,7 +72,7 @@ const TrustStrategy = { "to verify trust for encrypted connections, but have not configured any " + "trustedCertificates. You must specify the path to at least one trusted " + "X.509 certificate for this to work. Two other alternatives is to use " + - "TRUST_ON_FIRST_USE or to disable encryption by setting encrypted=false " + + "TRUST_ON_FIRST_USE or to disable encryption by setting encrypted=\"" + ENCRYPTION_OFF + "\"" + "in your driver configuration.")); return; } @@ -89,7 +90,8 @@ const TrustStrategy = { " the signing certificate, or the server certificate, to the list of certificates trusted by this driver" + " using `neo4j.v1.driver(.., { trustedCertificates:['path/to/certificate.crt']}). This " + " is a security measure to protect against man-in-the-middle attacks. If you are just trying " + - " Neo4j out and are not concerned about encryption, simply disable it using `encrypted=false` in the driver" + + " Neo4j out and are not concerned about encryption, simply disable it using `encrypted=\"" + ENCRYPTION_OFF + + "\"` in the driver" + " options.")); } else { onSuccess(); @@ -115,7 +117,7 @@ const TrustStrategy = { onFailure(newError("You are using a version of NodeJS that does not " + "support trust-on-first use encryption. You can either upgrade NodeJS to " + "a newer version, use `trust:TRUST_SIGNED_CERTIFICATES` in your driver " + - "config instead, or disable encryption using `encrypted:false`.")); + "config instead, or disable encryption using `encrypted:\"" + ENCRYPTION_OFF+ "\"`.")); return; } @@ -140,7 +142,7 @@ const TrustStrategy = { "update the file with the new certificate. You can configure which file the driver " + "should use to store this information by setting `knownHosts` to another path in " + "your driver configuration - and you can disable encryption there as well using " + - "`encrypted:false`.")) + "`encrypted:\"" + ENCRYPTION_OFF + "\"`.")) } }); }); @@ -150,7 +152,9 @@ const TrustStrategy = { }; function connect( opts, onSuccess, onFailure=(()=>null) ) { - if( opts.encrypted === false ) { + //still allow boolean for backwards compatibility + if (opts.encrypted === false || opts.encrypted === ENCRYPTION_OFF || + (opts.encrypted === ENCRYPTION_NON_LOCAL && isLocalHost(opts.host))) { var conn = net.connect(opts.port, opts.host, onSuccess); conn.on('error', onFailure); return conn; @@ -160,7 +164,7 @@ function connect( opts, onSuccess, onFailure=(()=>null) ) { onFailure(newError("Unknown trust strategy: " + opts.trust + ". Please use either " + "trust:'TRUST_SIGNED_CERTIFICATES' or trust:'TRUST_ON_FIRST_USE' in your driver " + "configuration. Alternatively, you can disable encryption by setting " + - "`encrypted:false`. There is no mechanism to use encryption without trust verification, " + + "`encrypted:\"" + ENCRYPTION_OFF + "\"`. There is no mechanism to use encryption without trust verification, " + "because this incurs the overhead of encryption without improving security. If " + "the driver does not verify that the peer it is connected to is really Neo4j, it " + "is very easy for an attacker to bypass the encryption by pretending to be Neo4j.")); @@ -190,6 +194,7 @@ class NodeChannel { this._error = null; this._handleConnectionError = this._handleConnectionError.bind(this); + this._encrypted = opts.encrypted; this._conn = connect(opts, () => { if(!self._open) { return; @@ -219,6 +224,10 @@ class NodeChannel { } } + isEncrypted() { + return this._encrypted; + } + /** * Write the passed in buffer to connection * @param {NodeBuffer} buffer - Buffer to write diff --git a/src/v1/internal/ch-websocket.js b/src/v1/internal/ch-websocket.js index 31f34c005..7c7a67e6d 100644 --- a/src/v1/internal/ch-websocket.js +++ b/src/v1/internal/ch-websocket.js @@ -20,6 +20,7 @@ import {debug} from "./log"; import {HeapBuffer} from "./buf"; import {newError} from './../error'; +import {isLocalHost, ENCRYPTION_NON_LOCAL, ENCRYPTION_ON, ENCRYPTION_OFF} from './util'; /** * Create a new WebSocketChannel to be used in web browsers. @@ -40,15 +41,19 @@ class WebSocketChannel { this._error = null; this._handleConnectionError = this._handleConnectionError.bind(this); + this._encrypted = opts.encrypted; + let scheme = "ws"; - if( opts.encrypted ) { + //Allow boolean for backwards compatibility + if( opts.encrypted === true || opts.encrypted === ENCRYPTION_ON || + (opts.encrypted === ENCRYPTION_NON_LOCAL && !isLocalHost(opts.host))) { if( (!opts.trust) || opts.trust === "TRUST_SIGNED_CERTIFICATES" ) { scheme = "wss"; } else { this._error = newError("The browser version of this driver only supports one trust " + "strategy, 'TRUST_SIGNED_CERTIFICATES'. "+opts.trust+" is not supported. Please " + "either use TRUST_SIGNED_CERTIFICATES or disable encryption by setting " + - "`encrypted:false` in the driver configuration."); + "`encrypted:\"" + ENCRYPTION_OFF + "\"` in the driver configuration."); return; } } @@ -98,6 +103,10 @@ class WebSocketChannel { } } } + + isEncrypted() { + return this._encrypted; + } /** * Write the passed in buffer to connection diff --git a/src/v1/internal/connector.js b/src/v1/internal/connector.js index af8b6df0a..c4d5a1021 100644 --- a/src/v1/internal/connector.js +++ b/src/v1/internal/connector.js @@ -26,6 +26,7 @@ import {alloc, CombinedBuffer} from "./buf"; import GraphType from '../graph-types'; import {int, isInt} from '../integer'; import {newError} from './../error'; +import {ENCRYPTION_NON_LOCAL, ENCRYPTION_OFF, shouldEncrypt} from './util'; let Channel; if( WebSocketChannel.available ) { @@ -401,6 +402,10 @@ class Connection { return !this._isBroken && this._ch._open; } + isEncrypted() { + return this._ch.isEncrypted(); + } + /** * Call close on the channel. * @param {function} cb - Function to call on close. @@ -422,9 +427,9 @@ function connect( url, config = {}) { return new Connection( new Ch({ host: host(url), port: port(url) || 7687, - // Default to using encryption if trust-on-first-use is available - encrypted : (config.encrypted == null) ? hasFeature("trust_on_first_use") : config.encrypted, - // Default to using trust-on-first-use if it is available + // Default to using ENCRYPTION_NON_LOCAL if trust-on-first-use is available + encrypted : shouldEncrypt(config.encrypted, (hasFeature("trust_on_first_use") ? ENCRYPTION_NON_LOCAL : ENCRYPTION_OFF), host(url)), + // Default to using TRUST_ON_FIRST_USE if it is available trust : config.trust || (hasFeature("trust_on_first_use") ? "TRUST_ON_FIRST_USE" : "TRUST_SIGNED_CERTIFICATES"), trustedCertificates : config.trustedCertificates || [], knownHosts : config.knownHosts diff --git a/src/v1/internal/util.js b/src/v1/internal/util.js new file mode 100644 index 000000000..b3e43d04b --- /dev/null +++ b/src/v1/internal/util.js @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2002-2016 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +let LOCALHOST_MATCHER = /^(localhost|127(\.\d+){3})$/i; +let ENCRYPTION_ON = "ENCRYPTION_ON"; +let ENCRYPTION_OFF = "ENCRYPTION_OFF"; +let ENCRYPTION_NON_LOCAL = "ENCRYPTION_NON_LOCAL"; + +function isLocalHost(host) { + return LOCALHOST_MATCHER.test(host); +} + +/* Coerce an encryption setting to a definitive boolean value, + * given a valid default and a target host. If encryption is + * explicitly set on or off, then the mapping is a simple + * conversion to true or false respectively. If set to + * ENCRYPTION_NON_LOCAL then respond according to whether or + * not the host is localhost/127.x.x.x. In all other cases + * (including undefined) then fall back to the default and + * re-evaluate. + */ +function shouldEncrypt(encryption, encryptionDefault, host) { + if (encryption === ENCRYPTION_ON || encryption === true) return true; + if (encryption === ENCRYPTION_OFF || encryption === false) return false; + if (encryption === ENCRYPTION_NON_LOCAL) return !isLocalHost(host); + return shouldEncrypt(encryptionDefault, ENCRYPTION_OFF, host); +} + +export { + isLocalHost, + shouldEncrypt, + ENCRYPTION_ON, + ENCRYPTION_OFF, + ENCRYPTION_NON_LOCAL +} diff --git a/src/v1/session.js b/src/v1/session.js index 9f56c88e3..07e9bfd06 100644 --- a/src/v1/session.js +++ b/src/v1/session.js @@ -40,6 +40,10 @@ class Session { this._hasTx = false; } + isEncrypted() { + return this._conn.isEncrypted(); + } + /** * Run Cypher statement * Could be called with a statement object i.e.: {statement: "MATCH ...", parameters: {param: 1}} diff --git a/test/internal/tls.test.js b/test/internal/tls.test.js index ccd61bd9e..333045c7c 100644 --- a/test/internal/tls.test.js +++ b/test/internal/tls.test.js @@ -20,6 +20,7 @@ var NodeChannel = require('../../lib/v1/internal/ch-node.js'); var neo4j = require("../../lib/v1"); var fs = require("fs"); var hasFeature = require("../../lib/v1/internal/features"); +var isLocalHost = require("../../lib/v1/internal/util").isLocalHost; describe('trust-signed-certificates', function() { @@ -34,15 +35,15 @@ describe('trust-signed-certificates', function() { // Given driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { - encrypted: true, + encrypted: "ENCRYPTION_ON", trust: "TRUST_SIGNED_CERTIFICATES", trustedCertificates: ["test/resources/random.certificate"] }); // When driver.session().run( "RETURN 1").catch( function(err) { - expect( err.message ).toContain( "Server certificate is not trusted" ); - done(); + expect( err.message ).toContain( "Server certificate is not trusted" ); + done(); }); }); @@ -55,7 +56,7 @@ describe('trust-signed-certificates', function() { // Given driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { - encrypted: true, + encrypted: "ENCRYPTION_ON", trust: "TRUST_SIGNED_CERTIFICATES", trustedCertificates: ["build/neo4j/certificates/neo4j.cert"] }); @@ -90,7 +91,7 @@ describe('trust-on-first-use', function() { } driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { - encrypted: true, + encrypted: "ENCRYPTION_ON", trust: "TRUST_ON_FIRST_USE", knownHosts: knownHostsPath }); @@ -115,7 +116,7 @@ describe('trust-on-first-use', function() { var knownHostsPath = "test/resources/random_known_hosts"; driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { - encrypted: true, + encrypted: "ENCRYPTION_ON", trust: "TRUST_ON_FIRST_USE", knownHosts: knownHostsPath }); @@ -128,6 +129,19 @@ describe('trust-on-first-use', function() { }); }); + it('should detect localhost', function() { + expect(isLocalHost('localhost')).toBe(true); + expect(isLocalHost('LOCALHOST')).toBe(true); + expect(isLocalHost('localHost')).toBe(true); + expect(isLocalHost('127.0.0.1')).toBe(true); + expect(isLocalHost('127.0.0.11')).toBe(true); + expect(isLocalHost('127.1.0.0')).toBe(true); + + expect(isLocalHost('172.1.0.0')).toBe(false); + expect(isLocalHost('127.0.0.0.0')).toBe(false); + expect(isLocalHost("google.com")).toBe(false); + }); + afterEach(function(){ if( driver ) { driver.close(); diff --git a/test/v1/examples.test.js b/test/v1/examples.test.js index 01ada8ba1..42056d949 100644 --- a/test/v1/examples.test.js +++ b/test/v1/examples.test.js @@ -344,8 +344,8 @@ describe('examples', function() { var neo4j = neo4jv1; // tag::tls-require-encryption[] var driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { - // In NodeJS, encryption is on by default. In the web bundle, it is off. - encrypted:true + //In NodeJS, encryption is ENCRYPTION_NON_LOCAL on by default. In the web bundle, it is ENCRYPTION_OFF. + encrypted:"ENCRYPTION_ON" }); // end::tls-require-encryption[] driver.close(); @@ -359,7 +359,7 @@ describe('examples', function() { // in NodeJS, trust-on-first-use is the default trust mode. In the browser // it is TRUST_SIGNED_CERTIFICATES. trust: "TRUST_ON_FIRST_USE", - encrypted:true + encrypted:"ENCRYPTION_NON_LOCAL" }); // end::tls-trust-on-first-use[] driver.close(); @@ -374,7 +374,7 @@ describe('examples', function() { // in NodeJS. In the browser bundle the browsers list of trusted // certificates is used, due to technical limitations in some browsers. trustedCertificates : ["path/to/ca.crt"], - encrypted:true + encrypted:"ENCRYPTION_NON_LOCAL" }); // end::tls-signed[] driver.close(); @@ -385,7 +385,7 @@ describe('examples', function() { // tag::connect-with-auth-disabled[] var driver = neo4j.driver("bolt://localhost", { // In NodeJS, encryption is on by default. In the web bundle, it is off. - encrypted:true + encrypted:"ENCRYPTION_NON_LOCAL" }); // end::connect-with-auth-disabled[] driver.close(); diff --git a/test/v1/tck/steps/tlssteps.js b/test/v1/tck/steps/tlssteps.js index 646aa09e8..07c27dafd 100644 --- a/test/v1/tck/steps/tlssteps.js +++ b/test/v1/tck/steps/tlssteps.js @@ -74,7 +74,7 @@ module.exports = function () { "If you trust that this certificate is valid, simply remove the line starting with localhost:7687 in `known_hosts1`, " + "and the driver will update the file with the new certificate. You can configure which file the driver should use " + "to store this information by setting `knownHosts` to another path in your driver configuration - " + - "and you can disable encryption there as well using `encrypted:false`."; + "and you can disable encryption there as well using `encrypted:\"ENCRYPTION_OFF\"`."; if (this.error.message !== expected) { callback(new Error("Given and expected results does not match: " + this.error.message + " Expected " + expected)); } else { @@ -107,7 +107,7 @@ module.exports = function () { this.Given(/^a driver configured to use a trusted certificate$/, function (callback) { this.config = { - encrypted: true, + encrypted: "ENCRYPTION_ON", trust: "TRUST_SIGNED_CERTIFICATES", knownHosts: this.knownHosts1, trustedCertificates: ['./test/resources/root.cert'] @@ -132,7 +132,7 @@ module.exports = function () { //will have to hack a little bit here since the root cert cannot be used by the server since its //common name is not set to localhost this.config = { - encrypted: true, + encrypted: "ENCRYPTION_ON", trust: "TRUST_SIGNED_CERTIFICATES", knownHosts: this.knownHosts1, trustedCertificates: [util.neo4jCert] @@ -156,7 +156,7 @@ module.exports = function () { "certificate, or the server certificate, to the list of certificates trusted by this driver using " + "`neo4j.v1.driver(.., { trustedCertificates:['path/to/certificate.crt']}). This is a security measure to protect " + "against man-in-the-middle attacks. If you are just trying Neo4j out and are not concerned about encryption, " + - "simply disable it using `encrypted=false` in the driver options."; + "simply disable it using `encrypted=\"ENCRYPTION_OFF\"` in the driver options."; if (this.error.message !== expected) { callback(new Error("Given and expected results does not match: " + this.error.message + " Expected " + expected)); } else { @@ -168,7 +168,7 @@ module.exports = function () { return neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { trust: "TRUST_ON_FIRST_USE", knownHosts: hostFile, - encrypted: true + encrypted: "ENCRYPTION_ON" }); } };