Implementation of high performance RSA encryption and decryption service using Nginx NJS

In the previous article "writing Nginx module for RSA encryption and decryption", I mentioned how to write Nginx module and realize relatively high-performance encryption and decryption with the help of Nginx. Coincidentally, the new version of Nginx has been released and initially has the native "RSA encryption and decryption" capability.

So, let's implement the functions mentioned earlier in a lighter way.

Write in front

With the arrival of Nginx version 1.21.4, NJS has also been upgraded to version 0.7. This version can be said to be a breakthrough version, because this version of NJS adds a WebCrypto API that conforms to the W3C standard.

This means that the era of interface encryption authentication may be over.

The official implementation of this function is mainly through the addition of njs_webcrypto.c encryption and decryption module introduces some OpenSSL capabilities. If your requirements include encryption and decryption for the specified RSA key (with password), NJS can't do it at present. However, you can modify the above code and add the code implementation of "computing part" mentioned in the article "writing Nginx module for RSA encryption and decryption": PEM_ read_ bio_ Add the part of rsaprivatekey that carries the password, and bind some functions to NJS. Finally, remember to clean up the RSA related references.

Fortunately, in most cases, considering the call performance, the encryption and decryption for the business interface is not inclined to use the key with password.

Next, I will introduce how to use the new capability of Nginx NJS to implement an interface service that can automatically encrypt and decrypt RSA according to the business interface content step by step.

Generate RSA certificate using browser

You didn't read the subtitle wrong. This time, we will use the browser instead of "OpenSSL in the traditional command line" to generate our certificate.

Two API s will be used here:

  • SubtleCrypto.generateKey()
  • SubtleCrypto.exportKey()

The document is boring, so the key points are drawn directly here. In the generation algorithm, this paper adopts the asymmetric encryption algorithm RSA-OAEP, which is only supported by the WEB Crypto API. When exporting the generated certificate, you need to select the corresponding export format according to the key type.

RSA key pair generated and exported from browser

In order to facilitate my readers to play, I wrote a simple JavaScript script, copy and paste the content into your browser console (Chrome is recommended), and then execute it. Not surprisingly, your browser will automatically download two files named "rsa.pub" and "rsa.key", which we will use later.

(async () => {
  const ab2str = (buffer) => String.fromCharCode.apply(null, new Uint8Array(buffer));
  const saveFile = async (files) => {
    Object.keys(files).forEach(file => {
      const blob = new Blob([files[file]], { type: 'text/plain' });
      with (document.createElement('a')) { download = file; href = URL.createObjectURL(blob); click(); }
      URL.revokeObjectURL(blob);
    });
  }
  const exportKey = (content) => new Promise(async (resolve) => { await crypto.subtle.exportKey(content.type === "private" ? "pkcs8" : "spki", content).then((data) => resolve(`-----BEGIN ${content.type.toUpperCase()} KEY-----\n${btoa(ab2str(data))}\n-----END ${content.type.toUpperCase()} KEY-----`)); });
  const { privateKey, publicKey } = await crypto.subtle.generateKey({ name: "RSA-OAEP", modulusLength: 4096, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, true, ["encrypt", "decrypt"])
  saveFile({ "rsa.key": await exportKey(privateKey), "rsa.pub": await exportKey(publicKey) });
})();

RSA encryption and decryption using NJS

Although the official documents of Nginx and NJS do not mention how to use the newly added WEB Crypto API, we can see the usage of the interface from the latest test cases in the code warehouse.

We refer to the code of "using NJS to write the basic interface of Nginx" in the previous article "using Docker and Nginx NJS to implement API aggregation service (previous article), and write a" rough "version first to experience the use of NJS for native RSA encryption and decryption of Nginx:

const fs = require('fs');
if (typeof crypto == 'undefined') {
  crypto = require('crypto').webcrypto;
}

function pem_to_der(pem, type) {
  const pemJoined = pem.toString().split('\n').join('');
  const pemHeader = `-----BEGIN ${type} KEY-----`;
  const pemFooter = `-----END ${type} KEY-----`;
  const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
  return Buffer.from(pemContents, 'base64');
}

const rsaKeys = {
  public: fs.readFileSync(`/etc/nginx/script/rsa.pub`),
  private: fs.readFileSync(`/etc/nginx/script/rsa.key`)
}

async function simple(req) {

  const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
  const pkcs8 = await crypto.subtle.importKey("pkcs8", pem_to_der(rsaKeys.private, "PRIVATE"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);

  let originText = "Suppose this is what needs to be encrypted, by soulteary";

  let enc = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, originText);
  let decode = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, pkcs8, enc);

  req.headersOut["Content-Type"] = "text/html;charset=UTF-8";
  req.return(200, [
    '<h2>Original content</h2>',
    `<code>${originText}</code>`,
    '<h2>Encrypted content</h2>',
    `<code>${Buffer.from(enc)}</code>`,
    '<h2>Decrypted content</h2>',
    `<code>${Buffer.from(decode)}</code>`,
  ].join(''));
}

export default { simple };

The above code defines a simple interface "simple", which is used to load the RSA Keys we just generated, and then encrypt and decrypt a specified content (originText). Save the above content as app.js. Let's continue to write a simple Nginx configuration (nginx.conf):

load_module modules/ngx_http_js_module.so;

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;

events { worker_connections 1024; }

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    js_import app from script/app.js;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    keepalive_timeout 65;
    gzip on;

    server {
        listen 80;
        server_name localhost;

        charset utf-8;
        gzip on;

        location / {
            js_content app.simple;
        }
    }
}

For ease of use, here is also a container configuration (docker-compose.yml):

version: '3'

services:

  nginx-rsa-demo:
    image: nginx:1.21.4-alpine
    ports:
      - 8080:80
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./scripts:/etc/nginx/script

Start the container with docker compose up, and then visit localhost:8080 in the browser. You can see the following contents.

RSA encryption and decryption of content using Nginx NJS

By the way, the response time is about ten ms in the notebook container. If you put it in the production environment and add some optimization, it will not be a problem to control it in single digits.

Interface response time

Well, the capability verification is over. Let's make a little transformation and optimization to realize the fully automatic RSA encryption and decryption function in the gateway product.

Building a gateway with RSA encryption and decryption capabilities

Here's how to use NJS of Nginx to encrypt and decrypt requests. Let's write the Nginx configuration section first.

Adjust the NJS export function used by Nginx configuration

Considering the convenience of debugging, we split the "entry point" (Interface) into three. You can adjust it according to the actual use scenario, such as adding IP access restrictions and additional authentication functions at the entrance, or canceling the "unified entrance", and directly using the two main encryption and decryption interfaces as the "entry point" of the program:

server {
    listen 80;
    server_name localhost;

    charset utf-8;
    gzip on;

    location / {
        js_content app.entrypoint;
    }

    location /api/encrypt {
        js_content app.encrypt;
    }

    location /api/decrypt {
        js_content app.decrypt;
    }
}

After writing the Nginx configuration, you can start dinner: write the NJS program.

Adjust NJS program: adjust export function

After the Nginx configuration is modified, the export function in NJS also needs to be adjusted:

export default { encrypt, decrypt, entrypoint };

After modifying the export function, we will implement the functions of the three interface functions in turn.

Implement NJS program: default entry function

Because the development and debugging of NJS is still in a very inconvenient state, let's write the entry function first to facilitate the debugging process (app.js):

function debug(req) {
  req.headersOut["Content-Type"] = "text/html;charset=UTF-8";
  req.return(200, JSON.stringify(req, null, 4));
}

function encrypt(req) {
  debug(req)
}

function decrypt(req) {
  debug(req)
}

function entrypoint(r) {
  r.headersOut["Content-Type"] = "text/html;charset=UTF-8";
  switch (r.method) {
    case 'GET':
      return r.return(200, [
        '<form action="/" method="post">',
        '<input name="data" value=""/>',
        '<input type="radio" name="action" id="encrypt" value="encrypt" checked="checked"/><label for="encrypt">Encrypt</label>',
        '<input type="radio" name="action" id="decrypt" value="decrypt"/><label for="decrypt">Decrypt</label>',
        '<button type="submit">Submit</button>',
        '</form>'
      ].join('<br>'));
    case 'POST':
      var body = r.requestBody;
      if (r.headersIn['Content-Type'] != 'application/x-www-form-urlencoded' || !body.length) {
        r.return(401, "Unsupported method\n");
      }

      var params = body.trim().split('&').reduce(function (prev, item) {
        var tmp = item.split('=');
        var key = decodeURIComponent(tmp[0]).trim();
        var val = decodeURIComponent(tmp[1]).trim();
        if (key === 'data' || key === 'action') {
          if (val) {
            prev[key] = val;
          }
        }
        return prev;
      }, {});

      if (!params.action || (params.action != 'encrypt' && params.action != 'decrypt')) {
        return r.return(400, 'Invalid Params: `action`.');
      }

      if (!params.data) {
        return r.return(400, 'Invalid Params: `data`.');
      }

      function response_cb(res) {
        r.return(res.status, res.responseBody);
      }

      return r.subrequest(`/api/${params.action}`, { method: 'POST' }, response_cb)
    default:
      return r.return(400, "Unsupported method\n");
  }
}

export default { encrypt, decrypt, entrypoint };

In the above 60 lines of code, what functions have we implemented?

  • A simple Web form interface is used to receive "encryption and decryption actions" and "data to be encrypted and decrypted" in our debugging and development process.
  • According to the selected action, the "encryption and decryption" operation is automatically performed, and the processing results of the specific encryption and decryption interface are returned.
  • Simple Mock has an encryption and decryption interface. At present, we actually call a function called debug to print our submission.

Using the browser access interface, you can see this simple submission interface:

Simple debugging page made with NJS

Write something casually in the text box in the debugging form and submit it. You can see that the function runs as expected and the submitted content is printed correctly:

The function runs as expected

Next, let's implement the RSA encryption function of NJS.

Implementation of NJS program: RSA encryption function

With reference to the previous article and a little adjustment, it is not difficult to implement this encryption function. About five lines are enough.

async function encrypt(req) {
  const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
  const result = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, req.requestText);
  req.return(200, Buffer.from(result));
}

Run Nginx again and submit the content. You can see that the data has been successfully encrypted by RSA.

Default output of NJS RSA encryption function

Because the content encrypted by RSA by default is not readable, generally, if plaintext is transmitted, we will set a layer of Base64 to show it. Therefore, we need to make some adjustments to this function and the function in the previous step. First, take the entry function "cut".

function entrypoint(r) {
  r.headersOut["Content-Type"] = "text/html;charset=UTF-8";

  switch (r.method) {
    case 'GET':
      return r.return(200, [
        '<form action="/" method="post">',
        '<input name="data" value=""/>',
        '<input type="radio" name="action" id="encrypt" value="encrypt" checked="checked"/><label for="encrypt">Encrypt</label>',
        '<input type="radio" name="action" id="decrypt" value="decrypt"/><label for="decrypt">Decrypt</label>',
        '<input type="radio" name="base64" id="base64-on" value="on" checked="checked"/><label for="base64-on">Base64 On</label>',
        '<input type="radio" name="base64" id="base64-off" value="off" /><label for="base64-off">Base64 Off</label>',
        '<button type="submit">Submit</button>',
        '</form>'
      ].join('<br>'));
    case 'POST':
      var body = r.requestBody;
      if (r.headersIn['Content-Type'] != 'application/x-www-form-urlencoded' || !body.length) {
        r.return(401, "Unsupported method\n");
      }

      var params = body.trim().split('&').reduce(function (prev, item) {
        var tmp = item.split('=');
        var key = decodeURIComponent(tmp[0]).trim();
        var val = decodeURIComponent(tmp[1]).trim();
        if (key === 'data' || key === 'action' || key === 'base64') {
          if (val) {
            prev[key] = val;
          }
        }
        return prev;
      }, {});

      if (!params.action || (params.action != 'encrypt' && params.action != 'decrypt')) {
        return r.return(400, 'Invalid Params: `action`.');
      }

      if (!params.base64 || (params.base64 != 'on' && params.base64 != 'off')) {
        return r.return(400, 'Invalid Params: `base64`.');
      }

      if (!params.data) {
        return r.return(400, 'Invalid Params: `data`.');
      }

      function response_cb(res) {
        r.return(res.status, res.responseBody);
      }

      return r.subrequest(`/api/${params.action}${params.base64 === 'on' ? '?base64=1' : ''}`, { method: 'POST', body: params.data }, response_cb)
    default:
      return r.return(400, "Unsupported method\n");
  }
}

We added an option of whether to enable Base64 encoding at the debugging entry, and added an additional one when calling the encryption and decryption interface when Base64 encoding is enabled? Request parameter for base64=1.

The transformation of encryption function is also very simple, almost ten lines:

async function encrypt(req) {
  const needBase64 = req.uri.indexOf('base64=1') > -1;
  const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
  const result = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, req.requestText);
  if (needBase64) {
    req.return(200, Buffer.from(result).toString("base64"));
  } else {
    req.headersOut["Content-Type"] = "application/octet-stream";
    req.return(200, Buffer.from(result));
  }
}

Restart the Nginx service and select Base64 encoding. You can see that the output results have met expectations.

Default output of NJS RSA encryption function after Base64

Copy and save the content for later use. Let's then implement the RSA decryption function.

Implementation of NJS program: RSA decryption function

With the RSA encryption function, it is easier to write the decryption function. Here, the disassembly steps are not the same as the encryption function. Just take into account the option type of "whether to enable Base64".

async function decrypt(req) {
  const needBase64 = req.uri.indexOf('base64=1') > -1;
  const pkcs8 = await crypto.subtle.importKey("pkcs8", pem_to_der(rsaKeys.private, "PRIVATE"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);
  const encrypted = needBase64 ? Buffer.from(req.requestText, 'base64') : Buffer.from(req.requestText);
  const result = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, pkcs8, encrypted);
  req.return(200, Buffer.from(result));
}

Submit the RSA encryption result after Base64 in the previous step. You can see that the encrypted content in the previous article can be decrypted correctly.

NJS calculates RSA decryption results

With the above foundation, let's toss about automatic encryption and decryption.

Building a gateway with automatic encryption and decryption capability

In order to simulate the real business scenario, we have to adjust the Nginx configuration and container configuration respectively.

Adjust Nginx configuration: simulate service interface

Adjust the Nginx configuration first.

First simulate two new services and set their output contents as original data and data encrypted by RSA. To keep it simple, we still use NJS to simulate the response content of the server interface:

server {
    listen 8081;
    server_name localhost;

    charset utf-8;
    gzip on;

    location / {
        js_content mock.mockEncData;
    }
}

server {
    listen 8082;
    server_name localhost;

    charset utf-8;
    gzip on;

    location / {
        js_content mock.mockRawData;
    }
}

In order to use NJS in the simulation service, remember to add an additional NJS script reference declaration in the Nginx global configuration:

js_import mock from script/mock.js;

To facilitate local debugging, we can also adjust the container orchestration configuration to expose the interfaces of the above two services:

version: '3'

services:

  nginx-api-demo:
    image: nginx:1.21.4-alpine
    restart: always
    ports:
      - 8080:80
      - 8081:8081
      - 8082:8082
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./scripts:/etc/nginx/script

Implement NJS program: write business simulation interface

Referring to the above, you can quickly write two business interfaces, which will output the original data to be encrypted later and the data encrypted by RSA. In order to simulate the real scene, random functions are used to calculate three different contents randomly.

function randomPick() {
    const powerWords = ['Su Yang blog', 'Focus on hard core', 'Share fun'];
    return powerWords[Math.floor(Math.random() * powerWords.length)];
}

function mockRawData(r) {
    r.headersOut["Content-Type"] = "text/html;charset=UTF-8";
    r.return(200, randomPick());
}

const fs = require('fs');
if (typeof crypto == 'undefined') {
    crypto = require('crypto').webcrypto;
}

function pem_to_der(pem, type) {
    const pemJoined = pem.toString().split('\n').join('');
    const pemHeader = `-----BEGIN ${type} KEY-----`;
    const pemFooter = `-----END ${type} KEY-----`;
    const pemContents = pemJoined.substring(pemHeader.length, pemJoined.length - pemFooter.length);
    return Buffer.from(pemContents, 'base64');
}

const publicKey = fs.readFileSync(`/etc/nginx/script/rsa.pub`);

async function mockEncData(r) {
    const spki = await crypto.subtle.importKey("spki", pem_to_der(publicKey, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
    const result = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, randomPick());

    r.headersOut["Content-Type"] = "text/html;charset=UTF-8";
    r.headersOut["Encode-State"] = "ON";
    r.return(200, Buffer.from(result).toString("base64"));
}

export default { mockEncData, mockRawData };

When everything is ready, we can visit different ports and see that the "business interface" is ready. Here, the data type is distinguished by adding an encode state request header to the encrypted data. If you don't want to add additional fields, you can also identify the response data type in the content type.

Simulate business interface with NJS

Adjust gateway Nginx configuration: aggregate service interface

There are two ways to actually use the service. One is that the service interface calls the gateway encryption and decryption function in our previous article to encrypt and decrypt the data, and then respond. The other is the gateway aggregation service interface, which adjusts the corresponding output results according to the data response type.

This paper selects the latter scheme, which can achieve rapid horizontal expansion with traifik, so as to improve the service response ability.

Because the sub requests of NJS have request source restrictions, in order to interact with business data, two interfaces need to be added in the Nginx configuration of the gateway to proxy the business data that needs to be encrypted or decrypted at the remote end.

location /remote/need-encrypt {
    proxy_pass http://localhost:8082/;
}

location /remote/need-decrypt {
    proxy_pass http://localhost:8081/;
}

After configuration, you can pass http://localhost:8080/remote/need-encrypt and http://localhost:8080/remote/need-encrypt accesses the contents in the previous section.

At the same time, in order to access the automatic encryption and decryption interface, we need to add another interface to call NJS function for automatic data encryption and decryption. (for actual business use and pursuit of ultimate performance, it can be divided into two)

location /auto{
    js_content app.auto;
}

Implementation of NJS program: automatic encryption and decryption of business data

Let's first implement an automatic data processing system that can process data according to our specified data source (encrypted data and decrypted data).

async function auto(req) {
  req.headersOut["Content-Type"] = "text/html;charset=UTF-8";

  let remoteAPI = "";
  switch (req.args.action) {
    case "encrypt":
      remoteAPI = "/remote/need-encrypt";
      break;
    case "decrypt":
    default:
      remoteAPI = "/remote/need-decrypt";
      break;
  }

  async function autoCalc(res) {
    const isEncoded = res.headersOut['Encode-State'] == "ON";
    const remoteRaw = res.responseText;
    if (isEncoded) {
      const pkcs8 = await crypto.subtle.importKey("pkcs8", pem_to_der(rsaKeys.private, "PRIVATE"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);
      const encrypted = Buffer.from(remoteRaw, 'base64');
      const result = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, pkcs8, encrypted);
      req.return(200, Buffer.from(result));
    } else {
      const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
      const dataEncrypted = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, remoteRaw);
      req.return(200, Buffer.from(dataEncrypted).toString("base64"));
    }
  }

  req.subrequest(remoteAPI, { method: "GET" }, autoCalc)
}


export default { encrypt, decrypt, entrypoint, auto };

Restart Nginx and access the proxy remote data interface / remote / need encrypt and the automatically encrypted gateway interface respectively. You can see that the program has been able to run as expected.

NJS automatically encrypts the service interface data according to the request

In order to make the program more intelligent and achieve the complete automation of data encryption and decryption, a simple adjustment can be made to let the program access the original data not according to the parameters we specify, but randomly. (in order to visually verify the behavior, we will also adjust the output here)

async function auto(req) {
  req.headersOut["Content-Type"] = "text/html;charset=UTF-8";

  function randomSource() {
    const sources = ["/remote/need-encrypt", "/remote/need-decrypt"];
    return sources[Math.floor(Math.random() * sources.length)];
  }

  async function autoCalc(res) {
    const isEncoded = res.headersOut['Encode-State'] == "ON";
    const remoteRaw = res.responseText;
    if (isEncoded) {
      const pkcs8 = await crypto.subtle.importKey("pkcs8", pem_to_der(rsaKeys.private, "PRIVATE"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"]);
      const encrypted = Buffer.from(remoteRaw, 'base64');
      const result = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, pkcs8, encrypted);
      req.return(200, [
        "<h2>Original content</h2>",
        `<code>${remoteRaw}</code>`,
        "<h2>Processed content</h2>",
        `<code>${Buffer.from(result)}</code>`
      ].join(""));
    } else {
      const spki = await crypto.subtle.importKey("spki", pem_to_der(rsaKeys.public, "PUBLIC"), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
      const dataEncrypted = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, spki, remoteRaw);
      req.return(200, [
        "<h2>Original content</h2>",
        `<code>${remoteRaw}</code>`,
        "<h2>Processed content</h2>",
        `<code>${Buffer.from(dataEncrypted).toString("base64")}</code>`
      ].join(""));
    }
  }

  req.subrequest(randomSource(), { method: "GET" }, autoCalc)
}

Restart Nginx again and refresh several times to see the results of RSA encryption and decryption automatically according to the content.

NJS realizes automatic encryption and decryption of RSA content

Others: interface safety considerations

During actual use, in addition to adding additional authentication and frequency restrictions before business, it is also recommended to use internal to limit the "scope" of Nginx interface according to the actual situation, so that the data source and basic computing interface can only be accessed internally by NJS program.

location /remote/need-encrypt {
    internal;
    proxy_pass http://localhost:8082/;
}

location /remote/need-decrypt {
    internal;
    proxy_pass http://localhost:8081/;
}

location /api/encrypt {
    internal;
    js_content app.encrypt;
}

location /api/decrypt {
    internal;
    js_content app.decrypt;
}

Others: if you pursue more efficient computing

In order to demonstrate the above, we have Base64 coded the calculation results. Considering the ultra-high pressure in the actual production environment, we generally haggle over the complexity of function calculation, so we can consider hard coding the certificate into the code and removing unnecessary Base64 as much as possible (only open in debugging mode).

last

There are few references about NJS on the Internet. I hope this article will become a link between you and NJS.

The above contents are stored on GitHub and can be taken by interested students.

--EOF

Posted on Wed, 24 Nov 2021 22:46:00 -0500 by FeeBle