mirror of
				https://github.com/therootcompany/greenlock.js.git
				synced 2024-11-16 17:29:00 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			508 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			508 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	

 | 
						|
 | 
						|

 | 
						|
 | 
						|
Greenlock™ for node.js
 | 
						|
=====
 | 
						|
 | 
						|
Greenlock provides Free SSL, Free Wildcard SSL, and Fully Automated HTTPS <br>
 | 
						|
<small>certificates issued by Let's Encrypt v2 via [ACME](https://git.coolaj86.com/coolaj86/acme-v2.js)</small>
 | 
						|
 | 
						|

 | 
						|

 | 
						|

 | 
						|

 | 
						|
 | 
						|
| Sponsored by [ppl](https://ppl.family) |
 | 
						|
Greenlock works
 | 
						|
in the [Commandline](https://git.coolaj86.com/coolaj86/greenlock-cli.js) (cli),
 | 
						|
as a [Web Server](https://git.coolaj86.com/coolaj86/greenlock-server.js),
 | 
						|
in [Web Browsers](https://git.coolaj86.com/coolaj86/greenlock.html) (WebCrypto),
 | 
						|
and with **node.js** ([npm](https://www.npmjs.com/package/greenlock)).
 | 
						|
 | 
						|
Features
 | 
						|
========
 | 
						|
 | 
						|
  - [x] Actively Maintained and Supported
 | 
						|
  - [x] Automatic HTTPS
 | 
						|
    - [x] Free SSL
 | 
						|
    - [x] Free Wildcard SSL
 | 
						|
    - [x] Multiple domain support (up to 100 altnames per SAN)
 | 
						|
    - [x] Dynamic Virtual Hosting (vhost)
 | 
						|
    - [x] Automatical renewal (10 to 14 days before expiration)
 | 
						|
  - [x] Great ACME support via [acme.js](https://git.coolaj86.com/coolaj86/acme-v2.js)
 | 
						|
    - [x] "dry run" with self-diagnostics
 | 
						|
    - [x] ACME draft 11
 | 
						|
    - [x] Let's Encrypt v2
 | 
						|
    - [x] Let's Encrypt v1
 | 
						|
  - [x] [Commandline](https://git.coolaj86.com/coolaj86/greenlock-cli.js) (cli) Utilities
 | 
						|
    - [x] Works with `bash`, `fish`, `zsh`, `cmd.exe`, `PowerShell`, and more
 | 
						|
  - [x] [Browser](https://git.coolaj86.com/coolaj86/greenlock.html) Support
 | 
						|
  - [x] Full node.js support, with modules for
 | 
						|
    - [x] [http/https](https://git.coolaj86.com/coolaj86/greenlock-express.js/src/branch/master/examples/https-server.js), [Express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js), [cluster](https://git.coolaj86.com/coolaj86/greenlock-cluster.js), [hapi](https://git.coolaj86.com/coolaj86/greenlock-hapi.js), [Koa](https://git.coolaj86.com/coolaj86/greenlock-koa.js), [rill](https://git.coolaj86.com/coolaj86/greenlock-rill.js), [restify](https://git.coolaj86.com/coolaj86/greenlock-restify.js), spdy, etc
 | 
						|
  - [x] Great for securing your Raspberry Pi
 | 
						|
  - [x] Extensible Plugin Support
 | 
						|
    - [x] AWS S3, AWS Route53, Azure, CloudFlare, Consul, Digital Ocean, etcd, Redis
 | 
						|
 | 
						|
Greenlock.js for Middleware
 | 
						|
------
 | 
						|
 | 
						|
Documentation for using Greenlock with
 | 
						|
[http/https](https://git.coolaj86.com/coolaj86/greenlock-express.js/src/branch/master/examples/https-server.js),
 | 
						|
[Express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js),
 | 
						|
[cluster](https://git.coolaj86.com/coolaj86/greenlock-cluster.js),
 | 
						|
[hapi](https://git.coolaj86.com/coolaj86/greenlock-hapi.js),
 | 
						|
[Koa](https://git.coolaj86.com/coolaj86/greenlock-koa.js),
 | 
						|
[rill](https://git.coolaj86.com/coolaj86/greenlock-rill.js).
 | 
						|
[restify](https://git.coolaj86.com/coolaj86/greenlock-restify.js).
 | 
						|
 | 
						|
Table of Contents
 | 
						|
=================
 | 
						|
 | 
						|
  * Install
 | 
						|
  * Simple Examples
 | 
						|
  * Example with ALL OPTIONS
 | 
						|
  * API
 | 
						|
  * Developer API
 | 
						|
  * Change History
 | 
						|
  * License
 | 
						|
 | 
						|
Install
 | 
						|
=======
 | 
						|
 | 
						|
```bash
 | 
						|
npm install --save greenlock@2.x
 | 
						|
```
 | 
						|
 | 
						|
**Note**: Ignore errors related to `ursa`. It is an optional dependency used when available.
 | 
						|
For many people it will not install properly, but it's only necessary on ARM devices (i.e. Raspberry Pi).
 | 
						|
 | 
						|
### Production vs Staging
 | 
						|
 | 
						|
If at first you don't succeed, stop and switch to staging.
 | 
						|
 | 
						|
I've implemented a "dry run" loopback test with self diagnostics
 | 
						|
so it's pretty safe to start off with the production URLs
 | 
						|
and be far less likely to hit the bad request rate limits.
 | 
						|
 | 
						|
However, if your first attempt to get a certificate fails
 | 
						|
I'd recommend switching to the staging acme server to debug -
 | 
						|
unless you're very clear on what the failure was and how to fix it.
 | 
						|
 | 
						|
```
 | 
						|
{ server: 'https://acme-staging-v02.api.letsencrypt.org/directory' }
 | 
						|
```
 | 
						|
 | 
						|
 | 
						|
Easy as 1, 2, 3... 4
 | 
						|
=====
 | 
						|
 | 
						|
Greenlock is built to incredibly easy to use, without sacrificing customization or extensibility.
 | 
						|
 | 
						|
The following examples range from just a few lines of code for getting started,
 | 
						|
to more robust examples that you might start with for an enterprise-grade use of the ACME api.
 | 
						|
 | 
						|
* Automatic HTTPS (for single sites)
 | 
						|
* Fully Automatic HTTPS (for multi-domain vhosts)
 | 
						|
* Manual HTTPS (for API integration)
 | 
						|
 | 
						|
Automatic HTTPS
 | 
						|
---------------
 | 
						|
 | 
						|
**Note**: For (fully) automatic HTTPS you may prefer
 | 
						|
the [Express.js module](https://git.coolaj86.com/coolaj86/greenlock-express.js)
 | 
						|
 | 
						|
This works for most people, but it's not as fun as some of the other examples.
 | 
						|
 | 
						|
Great when
 | 
						|
 | 
						|
 - [x] You only need a limited number of certificates
 | 
						|
 - [x] You want to use the bare node http and https modules without fluff
 | 
						|
 | 
						|
```js
 | 
						|
////////////////////
 | 
						|
// INIT GREENLOCK //
 | 
						|
////////////////////
 | 
						|
 | 
						|
var path = require('path');
 | 
						|
var os = require('os')
 | 
						|
var Greenlock = require('greenlock');
 | 
						|
 | 
						|
var greenlock = Greenlock.create({
 | 
						|
  agreeTos: true                      // Accept Let's Encrypt v2 Agreement
 | 
						|
, email: 'user@example.com'           // IMPORTANT: Change email and domains
 | 
						|
, approveDomains: [ 'example.com' ]
 | 
						|
, communityMember: false              // Optionally get important updates (security, api changes, etc)
 | 
						|
                                      // and submit stats to help make Greenlock better
 | 
						|
, version: 'draft-11'
 | 
						|
, server: 'https://acme-v02.api.letsencrypt.org/directory'
 | 
						|
, configDir: path.join(os.homedir(), 'acme/etc')
 | 
						|
});
 | 
						|
 | 
						|
////////////////////
 | 
						|
// CREATE SERVERS //
 | 
						|
////////////////////
 | 
						|
 | 
						|
var redir = require('redirect-https')();
 | 
						|
require('http').createServer(greenlock.middleware(redir)).listen(80);
 | 
						|
 | 
						|
require('https').createServer(greenlock.tlsOptions, function (req, res) {
 | 
						|
  res.end('Hello, Secure World!');
 | 
						|
}).listen(443);
 | 
						|
```
 | 
						|
 | 
						|
Fully Automatic HTTPS
 | 
						|
------------
 | 
						|
 | 
						|
**Note**: For (fully) automatic HTTPS you may prefer
 | 
						|
the [Express.js module](https://git.coolaj86.com/coolaj86/greenlock-express.js)
 | 
						|
 | 
						|
Great when
 | 
						|
 | 
						|
 - [x] You have a growing number of domains
 | 
						|
 - [x] You're integrating into your own hosting solution
 | 
						|
 - [x] Customize ACME http-01 or dns-01 challenge
 | 
						|
 | 
						|
```js
 | 
						|
////////////////////
 | 
						|
// INIT GREENLOCK //
 | 
						|
////////////////////
 | 
						|
 | 
						|
var path = require('path');
 | 
						|
var os = require('os')
 | 
						|
var Greenlock = require('greenlock');
 | 
						|
 | 
						|
var greenlock = Greenlock.create({
 | 
						|
  version: 'draft-11'
 | 
						|
, server: 'https://acme-v02.api.letsencrypt.org/directory'
 | 
						|
 | 
						|
  // approve a growing list of domains
 | 
						|
, approveDomains: approveDomains
 | 
						|
 | 
						|
  // If you wish to replace the default account and domain key storage plugin
 | 
						|
, store: require('le-store-certbot').create({
 | 
						|
    configDir: path.join(os.homedir(), 'acme/etc')
 | 
						|
  , webrootPath: '/tmp/acme-challenges'
 | 
						|
  })
 | 
						|
});
 | 
						|
 | 
						|
 | 
						|
/////////////////////
 | 
						|
// APPROVE DOMAINS //
 | 
						|
/////////////////////
 | 
						|
 | 
						|
var http01 = require('le-challenge-fs').create({ webrootPath: '/tmp/acme-challenges' });
 | 
						|
function approveDomains(opts, certs, cb) {
 | 
						|
  // This is where you check your database and associated
 | 
						|
  // email addresses with domains and agreements and such
 | 
						|
 | 
						|
  // Opt-in to submit stats and get important updates
 | 
						|
  opts.communityMember = true;
 | 
						|
 | 
						|
  // If you wish to replace the default challenge plugin, you may do so here
 | 
						|
  opts.challenges = { 'http-01': http01 };
 | 
						|
 | 
						|
  // The domains being approved for the first time are listed in opts.domains
 | 
						|
  // Certs being renewed are listed in certs.altnames
 | 
						|
  if (certs) {
 | 
						|
    opts.domains = certs.altnames;
 | 
						|
  }
 | 
						|
  else {
 | 
						|
    opts.email = 'john.doe@example.com';
 | 
						|
    opts.agreeTos = true;
 | 
						|
  }
 | 
						|
 | 
						|
  // NOTE: you can also change other options such as `challengeType` and `challenge`
 | 
						|
  // opts.challengeType = 'http-01';
 | 
						|
  // opts.challenge = require('le-challenge-fs').create({});
 | 
						|
 | 
						|
  cb(null, { options: opts, certs: certs });
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
////////////////////
 | 
						|
// CREATE SERVERS //
 | 
						|
////////////////////
 | 
						|
 | 
						|
var redir = require('redirect-https')();
 | 
						|
require('http').createServer(greenlock.middleware(redir)).listen(80);
 | 
						|
 | 
						|
require('https').createServer(greenlock.tlsOptions, function (req, res) {
 | 
						|
  res.end('Hello, Secure World!');
 | 
						|
}).listen(443);
 | 
						|
```
 | 
						|
 | 
						|
Manual HTTPS
 | 
						|
-------------
 | 
						|
 | 
						|
Here's a taste of the API that you might use if building a commandline tool or API integration
 | 
						|
that doesn't use node's SNICallback.
 | 
						|
 | 
						|
```
 | 
						|
 | 
						|
 | 
						|
/////////////////////
 | 
						|
// SET USER PARAMS //
 | 
						|
/////////////////////
 | 
						|
 | 
						|
var opts = {
 | 
						|
  domains: [ 'example.com'        // CHANGE EMAIL AND DOMAINS
 | 
						|
           , 'www.example.com' ]
 | 
						|
, email: 'user@example.com'
 | 
						|
, agreeTos: true                  // Accept Let's Encrypt v2 Agreement
 | 
						|
, communityMember: true           // Help make Greenlock better by submitting
 | 
						|
                                  // stats and getting updates
 | 
						|
};
 | 
						|
 | 
						|
 | 
						|
////////////////////
 | 
						|
// INIT GREENLOCK //
 | 
						|
////////////////////
 | 
						|
 | 
						|
var greenlock = require('greenlock').create({
 | 
						|
  version: 'draft-11'
 | 
						|
, server: 'https://acme-v02.api.letsencrypt.org/directory'
 | 
						|
, configDir: '/tmp/acme/etc'
 | 
						|
});
 | 
						|
 | 
						|
 | 
						|
///////////////////
 | 
						|
// GET TLS CERTS //
 | 
						|
///////////////////
 | 
						|
 | 
						|
greenlock.register(opts).then(function (certs) {
 | 
						|
  console.log(certs);
 | 
						|
  // privkey, cert, chain, expiresAt, issuedAt, subject, altnames
 | 
						|
}, function (err) {
 | 
						|
  console.error(err);
 | 
						|
});
 | 
						|
```
 | 
						|
 | 
						|
The domain key and ssl certificates you get back can be used in a webserver like this:
 | 
						|
 | 
						|
```js
 | 
						|
var tlsOptions = { key: certs.privkey, cert: certs.cert + '\r\n' + certs.chain };
 | 
						|
require('https').createServer(tlsOptions, function (req, res) {
 | 
						|
  res.end('Hello, Secure World!');
 | 
						|
}).listen(443);
 | 
						|
```
 | 
						|
 | 
						|
Example with ALL OPTIONS
 | 
						|
=========
 | 
						|
 | 
						|
The configuration consists of 3 components:
 | 
						|
 | 
						|
* Storage Backend (search npm for projects starting with 'le-store-')
 | 
						|
* ACME Challenge Handlers (search npm for projects starting with 'le-challenge-')
 | 
						|
* Letsencryt Config (this is all you)
 | 
						|
 | 
						|
```javascript
 | 
						|
'use strict';
 | 
						|
 | 
						|
var Greenlock = require('greenlock');
 | 
						|
var greenlock;
 | 
						|
 | 
						|
 | 
						|
// Storage Backend
 | 
						|
var leStore = require('le-store-certbot').create({
 | 
						|
  configDir: '~/acme/etc'                                 // or /etc/letsencrypt or wherever
 | 
						|
, debug: false
 | 
						|
});
 | 
						|
 | 
						|
 | 
						|
// ACME Challenge Handlers
 | 
						|
var leHttpChallenge = require('le-challenge-fs').create({
 | 
						|
  webrootPath: '~/acme/var/'                              // or template string such as
 | 
						|
, debug: false                                            // '/srv/www/:hostname/.well-known/acme-challenge'
 | 
						|
});
 | 
						|
 | 
						|
 | 
						|
function leAgree(opts, agreeCb) {
 | 
						|
  // opts = { email, domains, tosUrl }
 | 
						|
  agreeCb(null, opts.tosUrl);
 | 
						|
}
 | 
						|
 | 
						|
greenlock = Greenlock.create({
 | 
						|
  version: 'draft-11'                                     // 'draft-11' or 'v01'
 | 
						|
                                                          // 'draft-11' is for Let's Encrypt v2 otherwise known as ACME draft 11
 | 
						|
                                                          // 'v02' is an alias for 'draft-11'
 | 
						|
                                                          // 'v01' is for the pre-spec Let's Encrypt v1
 | 
						|
  //
 | 
						|
  // staging API
 | 
						|
  //server: 'https://acme-staging-v02.api.letsencrypt.org/directory'
 | 
						|
 | 
						|
  //
 | 
						|
  // production API
 | 
						|
  server: 'https://acme-v02.api.letsencrypt.org/directory'
 | 
						|
 | 
						|
, store: leStore                                          // handles saving of config, accounts, and certificates
 | 
						|
, challenges: {
 | 
						|
    'http-01': leHttpChallenge                            // handles /.well-known/acme-challege keys and tokens
 | 
						|
  }
 | 
						|
, challengeType: 'http-01'                                // default to this challenge type
 | 
						|
, agreeToTerms: leAgree                                   // hook to allow user to view and accept LE TOS
 | 
						|
//, sni: require('le-sni-auto').create({})                // handles sni callback
 | 
						|
 | 
						|
                                                          // renewals happen at a random time within this window
 | 
						|
, renewWithin: 14 * 24 * 60 * 60 * 1000                   // certificate renewal may begin at this time
 | 
						|
, renewBy:     10 * 24 * 60 * 60 * 1000                   // certificate renewal should happen by this time
 | 
						|
 | 
						|
, debug: false
 | 
						|
//, log: function (debug) {console.log.apply(console, args);} // handles debug outputs
 | 
						|
});
 | 
						|
 | 
						|
 | 
						|
// If using express you should use the middleware
 | 
						|
// app.use('/', greenlock.middleware());
 | 
						|
//
 | 
						|
// Otherwise you should see the test file for usage of this:
 | 
						|
// greenlock.challenges['http-01'].get(opts.domain, key, val, done)
 | 
						|
 | 
						|
 | 
						|
 | 
						|
// Check in-memory cache of certificates for the named domain
 | 
						|
greenlock.check({ domains: [ 'example.com' ] }).then(function (results) {
 | 
						|
  if (results) {
 | 
						|
    // we already have certificates
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
 | 
						|
  // Register Certificate manually
 | 
						|
  greenlock.register({
 | 
						|
 | 
						|
    domains: ['example.com']                                // CHANGE TO YOUR DOMAIN (list for SANS)
 | 
						|
  , email: 'user@email.com'                                 // CHANGE TO YOUR EMAIL
 | 
						|
  , agreeTos: ''                                            // set to tosUrl string (or true) to pre-approve (and skip agreeToTerms)
 | 
						|
  , rsaKeySize: 2048                                        // 2048 or higher
 | 
						|
  , challengeType: 'http-01'                                // http-01, tls-sni-01, or dns-01
 | 
						|
 | 
						|
  }).then(function (results) {
 | 
						|
 | 
						|
    console.log('success');
 | 
						|
 | 
						|
  }, function (err) {
 | 
						|
 | 
						|
    // Note: you must either use greenlock.middleware() with express,
 | 
						|
    // manually use greenlock.challenges['http-01'].get(opts, domain, key, val, done)
 | 
						|
    // or have a webserver running and responding
 | 
						|
    // to /.well-known/acme-challenge at `webrootPath`
 | 
						|
    console.error('[Error]: node-greenlock/examples/standalone');
 | 
						|
    console.error(err.stack);
 | 
						|
 | 
						|
  });
 | 
						|
 | 
						|
});
 | 
						|
```
 | 
						|
 | 
						|
Here's what `results` looks like:
 | 
						|
 | 
						|
```javascript
 | 
						|
{ privkey: ''     // PEM encoded private key
 | 
						|
, cert: ''        // PEM encoded cert
 | 
						|
, chain: ''       // PEM encoded intermediate cert
 | 
						|
, issuedAt: 0     // notBefore date (in ms) parsed from cert
 | 
						|
, expiresAt: 0    // notAfter date (in ms) parsed from cert
 | 
						|
, subject: ''     // example.com
 | 
						|
, altnames: []    // example.com,www.example.com
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
API
 | 
						|
---
 | 
						|
 | 
						|
The full end-user API is exposed in the example above and includes all relevant options.
 | 
						|
 | 
						|
```
 | 
						|
greenlock.register(opts)
 | 
						|
greenlock.check(opts)
 | 
						|
```
 | 
						|
 | 
						|
### Helper Functions
 | 
						|
 | 
						|
We do expose a few helper functions:
 | 
						|
 | 
						|
* Greenlock.validDomain(hostname) // returns '' or the hostname string if it's a valid ascii or punycode domain name
 | 
						|
 | 
						|
TODO fetch domain tld list
 | 
						|
 | 
						|
### Template Strings
 | 
						|
 | 
						|
The following variables will be tempalted in any strings passed to the options object:
 | 
						|
 | 
						|
* `~/` replaced with `os.homedir()` i.e. `/Users/aj`
 | 
						|
* `:hostname` replaced with the first domain in the list i.e. `example.com`
 | 
						|
 | 
						|
Developer API
 | 
						|
-------------
 | 
						|
 | 
						|
If you are developing an `le-store-*` or `le-challenge-*` plugin you need to be aware of
 | 
						|
additional internal API expectations.
 | 
						|
 | 
						|
**IMPORTANT**:
 | 
						|
 | 
						|
Use `v2.0.0` as your initial version - NOT v0.1.0 and NOT v1.0.0 and NOT v3.0.0.
 | 
						|
This is to indicate that your module is compatible with v2.x of node-greenlock.
 | 
						|
 | 
						|
Since the public API for your module is defined by node-greenlock the major version
 | 
						|
should be kept in sync.
 | 
						|
 | 
						|
### store implementation
 | 
						|
 | 
						|
See <https://git.coolaj86.com/coolaj86/le-store-SPEC.js>
 | 
						|
 | 
						|
* getOptions()
 | 
						|
* accounts.
 | 
						|
  * checkKeypair(opts, cb)
 | 
						|
  * check(opts, cb)
 | 
						|
  * setKeypair(opts, keypair, cb)
 | 
						|
  * set(opts, reg, cb)
 | 
						|
* certificates.
 | 
						|
  * checkKeypair(opts, cb)
 | 
						|
  * check(opts, cb)
 | 
						|
  * setKeypair(opts, keypair, cb)
 | 
						|
  * set(opts, reg, cb)
 | 
						|
 | 
						|
### challenge implementation
 | 
						|
 | 
						|
See https://git.coolaj86.com/coolaj86/le-challenge-fs.js
 | 
						|
 | 
						|
* `.set(opts, domain, key, value, cb);`         // opts will be saved with domain/key
 | 
						|
* `.get(opts, domain, key, cb);`                // opts will be retrieved by domain/key
 | 
						|
* `.remove(opts, domain, key, cb);`             // opts will be retrieved by domain/key
 | 
						|
 | 
						|
Change History
 | 
						|
==============
 | 
						|
* v2.2 - Let's Encrypt v2 Support
 | 
						|
  * v2.2.11 - documentation updates
 | 
						|
  * v2.2.10 - don't let SNICallback swallow approveDomains errors 6286883fc2a6ebfff711a540a2e4d92f3ac2907c
 | 
						|
  * v2.2.8 - communityMember option support
 | 
						|
  * v2.2.7 - bugfix for wildcard support
 | 
						|
  * v2.2.5 - node v6.x compat
 | 
						|
  * v2.2.4 - don't promisify all of `dns`
 | 
						|
  * v2.2.3 - `renewWithin` default to 14 days
 | 
						|
  * v2.2.2 - replace git dependency with npm
 | 
						|
  * v2.2.1 - April 2018 **Let's Encrypt v2** support
 | 
						|
* v2.1.17 - Nov 5th 2017 migrate back to personal repo
 | 
						|
* v2.1.9 - Jan 18th 2017 renamed to greenlock
 | 
						|
* v2.0.2 - Aug 9th 2016 update readme
 | 
						|
* v2.0.1 - Aug 9th 2016
 | 
						|
  * major refactor
 | 
						|
  * simplified API
 | 
						|
  * modular plugins
 | 
						|
  * knock out bugs
 | 
						|
* v1.5.0 now using letiny-core v2.0.0 and rsa-compat
 | 
						|
* v1.4.x I can't remember... but it's better!
 | 
						|
* v1.1.0 Added letiny-core, removed node-letsencrypt-python
 | 
						|
* v1.0.2 Works with node-letsencrypt-python
 | 
						|
* v1.0.0 Thar be dragons
 | 
						|
 | 
						|
LICENSE
 | 
						|
=======
 | 
						|
 | 
						|
Dual-licensed MIT and Apache-2.0
 | 
						|
 | 
						|
See LICENSE
 | 
						|
 | 
						|
Greenlock™ is a trademark of AJ ONeal
 |