CVE-2022-39287: Cleartext Transmission of Sensitive Information (CWE-319)
value: tiny-csrf is a Node.js cross site request forgery (CSRF) protection middleware. In versions prior to 1.1.0 cookies were not encrypted and thus CSRF tokens were transmitted in the clear. This issue has been addressed in commit `8eead6d` and the patch with be included in version 1.1.0. Users are advised to upgrade. There are no known workarounds for this issue.
CWE description: The product transmits sensitive or security-critical data in cleartext in a communication channel that can be sniffed by unauthorized actors.
CVSS v3 base score: 6.5
Published: 2022-10-07T20:15Z
Repository: https://github.com/valexandersaulys/tiny-csrf
Language: JavaScript
Files changed: README.md, encryption.js, index.js, package.json, test.js
Commit date: 2022-10-06 12:10:13 -0400
Commit message: Updated to include encrypting cookies, better protection client side

Diff: diff --git a/README.md b/README.md
index ad37525..2cd6ea8 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,18 @@
 
 This is a tiny csrf library meant to replace what `csurf` used to do
 [before it was deleted](https://github.com/expressjs/csurf). It is
 _almost_ a drop-in replacement.  
 
+Note that if you require very specific security needs you may want to
+look elsewhere. This library supports encrypting cookies on the client
+side to prevent malicious attackers from looking in. It does not
+protect against things such as [double submit
+cookies](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie). Those
+setups require greater setup and more know-how. This library aims to
+be simple to setup. 
+
 
 
 ## Installation
 
 ```
@@ -15,30 +23,38 @@ npm i tiny-csrf
 To Use in your app:
 
 ```javascript
 const csurf = require("tiny-csrf");
 const express = require("express");
+const session = require("express-session");
 
 let app = express();
 
+app.use(
+  session({})
+);
 app.use(
   csurf(
+    "123456789iamasecret987654321look",  // secret -- must be 32 bits or chars in length
     ["POST"],    // the request methods we want CSRF protection for
     ["/detail", /\/detail\.*/i]  // any URLs we want to exclude, either as strings or regexp
   )
 );
 
 // declare all your other routes and middleware
 ```
 
-Defaults to only requiring CSRF protection on `POST` requests and
-excludes no URLs. 
+The secret must be 32 bits (e.g. characters) in length and uses 
+[the built-in `crypto.createCipheriv` library built into Node
+](https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options). The
+secret length is enforced by the
+[`AES-256-CBC`](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)
+algorithm. 
 
-This uses the built-in [`crypto`
-library](https://nodejs.org/api/crypto.html#cryptorandomuuidoptions)
-for generating CSRF tokens. This may or may not be sufficient for your
-needs. 
+Defaults to only requiring CSRF protection on `POST`, `PUT`, and `PATCH` requests and
+excludes no URLs. The csrf will be checked for in the body of a
+request via `_csrf`. 
 
 
 ## Examples
 
 ```javascript
@@ -66,5 +82,10 @@ app.get("/", (req, res) => {
 app.post("/", (req, res) => {
   // all invalid or nonexistent CSRF tokens will return 403
   return res.status(200).send("Got it!");
 });
 ```
+
+
+## License
+
+[MIT](#)
diff --git a/encryption.js b/encryption.js
new file mode 100644
index 0000000..f52c48c
--- /dev/null
+++ b/encryption.js
@@ -0,0 +1,57 @@
+const { randomBytes, createCipheriv, createDecipheriv } = require("crypto");
+
+const ALGORITHM = "aes-256-cbc";
+
+const encryptCookie = (cookie, _secret) => {
+  /**
+   * Encrypt a cookie using AES 256 bits
+   * @param {cookie} string the cookie we want to encrypt. Will be visible as plain string to client.
+   * @param {_secret} string the secret that will be stored server-side. Client will never see this.
+   */
+  const iv = randomBytes(16);
+  const _cipher = createCipheriv(ALGORITHM, Buffer.from(_secret), iv);
+  const encrypted = [
+    iv.toString("hex"),
+    ":",
+    _cipher.update(cookie, "utf8", "hex"),
+    _cipher.final("hex")
+  ];
+  return encrypted.join("");
+};
+
+const decryptCookie = (cookie, _secret) => {
+  /**
+   * Decrypt a cookie using AES 256 bits
+   * @param {cookie} string the cookie we want to encrypt. Will be visible as plain string to client.
+   * @param {_secret} string the secret that will be stored server-side. Client will never see this.
+   */
+  const _encryptedArray = cookie.split(":");
+  if (_encryptedArray.length != 2) throw new Error("bad decrypt");
+  const iv = new Buffer(_encryptedArray[0], "hex");
+  const encrypted = new Buffer(_encryptedArray[1], "hex");
+  const decipher = createDecipheriv(ALGORITHM, _secret, iv);
+  const decrypted =
+    decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
+  return decrypted;
+};
+
+const verifyCsrf = (requestCsrf, cookieCsrf, _secret) => {
+  /**
+   * Verify a CSRF token
+   * @param {requestCsrf} string the CSRF coming from client side
+   * @param {cookieCsrf} string the CSRF as stored in the user's cookies
+   * @param {_secret} string the string used to encrypt the CSRF in the first place.
+   */
+  try {
+    const decryptedCookie = decryptCookie(cookieCsrf, _secret);
+    return decryptedCookie === requestCsrf;
+  } catch (err) {
+    return false;
+  }
+};
+
+module.exports = {
+  encryptCookie,
+  decryptCookie,
+  verifyCsrf
+};
diff --git a/index.js b/index.js
index f46212f..76c54ca 100644
--- a/index.js
+++ b/index.js
@@ -1,53 +1,54 @@
 const { randomUUID } = require("crypto");
+const { encryptCookie, verifyCsrf } = require("./encryption");
 
-const csurf = (forbiddenMethods, excludedUrls) => {
-  if (!forbiddenMethods) forbiddenMethods = ["POST"];
+const cookieParams = {
+  httpOnly: true,
+  sameSite: "strict",
+  signed: true,
+  maxAge: 300000
+};
+
+const csurf = (secret, forbiddenMethods, excludedUrls) => {
+  if (!forbiddenMethods) forbiddenMethods = ["POST", "PUT", "PATCH"];
+  if (secret.length != 32)
+    throw new Error("Your secret is not the required 32 characters long");
   return (req, res, next) => {
-    if (!req.cookies || !res.cookie)
+    if (!req.cookies || !res.cookie || !req.signedCookies)
       throw new Error("No Cookie middleware is installed");
     if (
       // if any excludedUrl matches as either string or regexp
       excludedUrls?.filter(
         (x) => x == req.originalUrl || (x.test && x.test(req.originalUrl))
       ).length > 0
     ) {
       req.csrfToken = () => {
-        if (!req.cookies.csrfToken) {
-          const csrfToken = randomUUID();
-          res.cookie("csrfToken", csrfToken);
-          return csrfToken;
-        }
-        return req.cookies.csrfToken;
+        const csrfToken = randomUUID();
+        res.cookie("csrfToken", encryptCookie(csrfToken, secret), cookieParams);
+        return csrfToken;
       };
       return next();
     } else if (forbiddenMethods.includes(req.method)) {
-      const { csrfToken } = req.cookies;
+      const { csrfToken } = req.signedCookies;
       if (
         csrfToken != undefined &&
-        (req.query._csrf === csrfToken ||
-          req.params._csrf === csrfToken ||
-          req.body._csrf === csrfToken)
+        verifyCsrf(req.body?._csrf, csrfToken, secret)
       ) {
-        res.cookie("csrfToken", "");
+        res.cookie("csrfToken", null, cookieParams);
         return next();
       } else {
         throw new Error(
-          `Did not get a CSRF token for ${req.method} ${req.originalUrl}: ${req.body._csrf} v. ${csrfToken}`
+          `Did not get a valid CSRF token for '${req.method} ${req.originalUrl}': ${req.body?._csrf} v. ${csrfToken}`
         );
       }
     } else {
       req.csrfToken = () => {
-        if (!req.cookies.csrfToken) {
-          const csrfToken = randomUUID();
-          res.cookie("csrfToken", csrfToken);
-          return csrfToken;
-        }
-        return req.cookies.csrfToken;
+        const csrfToken = randomUUID();
+        res.cookie("csrfToken", encryptCookie(csrfToken, secret), cookieParams);
+        return csrfToken;
       };
       return next();
     }
   };
 };
 
-// module.exports = csurf(forbiddenMethods, excludedUrls);
 module.exports = csurf;
diff --git a/package.json b/package.json
index 1501229..cdeabbf 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
   "name": "tiny-csrf",
-  "version": "1.0.3",
+  "version": "1.1.0",
   "description": "Tiny CSRF library for use with ExpressJS",
   "main": "index.js",
   "scripts": {
     "test": "mocha test.js",
     "test:watch": "mocha --watch test.js",
diff --git a/test.js b/test.js
index 05f5145..3b19ae4 100644
--- a/test.js
+++ b/test.js
@@ -1,17 +1,61 @@
 const { assert, expect } = require("chai");
 const { describe, before, beforeEach, after, afterEach, it } = require("mocha");
 const sinon = require("sinon");
 const { mockRequest, mockResponse } = require("mock-req-res");
 
+const { randomUUID, randomBytes } = require("crypto");
 const csurf = require("./index");
+const { encryptCookie, decryptCookie, verifyCsrf } = require("./encryption");
+
+describe("Cookie Encryption Tests", () => {
+  before(() => {});
+  after(() => {});
+
+  it("will encrypt and decrypt a cookie", () => {
+    // req.cookies.csrfToken =>  will be scrambled
+    const secret = "123456789iamasecret987654321look";
+    const csrfToken = randomUUID();
+    const encryptedCsrfToken = encryptCookie(csrfToken, secret);
+    assert.notEqual(
+      csrfToken,
+      encryptedCsrfToken,
+      "Both are equal as plain string and shouldn't be"
+    );
+    assert.isTrue(verifyCsrf(csrfToken, encryptedCsrfToken, secret));
+    assert.isFalse(
+      verifyCsrf(
+        csrfToken,
+        encryptedCsrfToken,
+        randomBytes(16).toString("hex")
+      ),
+      "Should not be able to verify without the secret"
+    );
+    assert.isFalse(
+      verifyCsrf(randomUUID(), encryptedCsrfToken, secret),
+      "Should not verify random UUID tokens"
+    );
+    assert.isFalse(
+      verifyCsrf("", encryptedCsrfToken, secret),
+      "Should not verify random blank tokens"
+    );
+    assert.isFalse(
+      verifyCsrf(null, encryptedCsrfToken, secret),
+      "Should not verify random null tokens"
+    );
+  });
+});
 
 describe("Default Options Tests", () => {
   before(() => {
-    this.csrf = csurf(null, []);
+    this.secret = "123456789iamasecret987654321look";
+    this.csrf = csurf(this.secret);
   });
 
+  it("throw internal error if our secret is not long enough", () => {
+    assert.throws(() => csurf("imnotlongenough"));
+  });
   it("throws internal error if we have no cookies", () => {
     const req = mockRequest({
       cookies: null
     });
     const res = mockResponse({
@@ -26,11 +70,11 @@ describe("Default Options Tests", () => {
     }
   });
   it("generates token for non-POST request", () => {
     const req = mockRequest({
       method: "GET",
-      cookies: {},
+      signedCookies: {},
       body: {}
     });
     const res = mockResponse({
       cookie: sinon.stub()
     });
@@ -44,12 +88,12 @@ describe("Default Options Tests", () => {
     assert.isString(res.cookie.getCall(0).args[1]);
   });
   it("allows if the CSRF token is correct", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaa", this.secret)
       },
       body: {
         _csrf: "aaaa"
       }
     });
@@ -62,12 +106,12 @@ describe("Default Options Tests", () => {
     assert.isTrue(next.calledOnce);
   });
   it("does not allow if the CSRF token is incorrect", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaabbb"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaabbb", this.secret)
       },
       body: {
         _csrf: "aaaa"
       }
     });
@@ -78,19 +122,19 @@ describe("Default Options Tests", () => {
     assert.isNotFunction(req.csrfToken);
     try {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });
   it("does not allow if the CSRF token is missing in body", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaabbb"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaabbb", this.secret)
       },
       body: {}
     });
     const res = mockResponse({
       cookie: sinon.stub()
@@ -99,18 +143,18 @@ describe("Default Options Tests", () => {
     assert.isNotFunction(req.csrfToken);
     try {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });
   it("does not allow if the CSRF token was never generated", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {},
+      signedCookies: {},
       body: {}
     });
     const res = mockResponse({
       cookie: sinon.stub()
     });
@@ -118,48 +162,27 @@ describe("Default Options Tests", () => {
     assert.isNotFunction(req.csrfToken);
     try {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });
-  it("will reuse if the CSRF token already exists in a non-POST", () => {
-    const req = mockRequest({
-      method: "GET",
-      cookies: {
-        csrfToken: "aaaabc"
-      },
-      body: {}
-    });
-    const res = mockResponse({
-      cookie: sinon.stub()
-    });
-    const next = sinon.stub();
-    assert.isNotFunction(req.csrfToken);
-    this.csrf(req, res, next);
-    assert.isTrue(next.calledOnce);
-    assert.isFunction(
-      req.csrfToken,
-      "Did not attach a csrfToken function to req"
-    );
-    const sampleToken = req.csrfToken();
-    assert.equal(sampleToken, "aaaabc");
-  });
 });
 
 describe("Tests w/Specified Included Request Methods", () => {
   before(() => {
-    this.csrf = csurf(["POST", "PUT"], []);
+    this.secret = "123456789iamasecret987654321look";
+    this.csrf = csurf(this.secret, ["POST", "PUT"], []);
   });
 
   it("allows if the CSRF token is correct", () => {
     const req = mockRequest({
       method: "PUT",
-      cookies: {
-        csrfToken: "aaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaa", this.secret)
       },
       body: {
         _csrf: "aaaa"
       }
     });
@@ -172,12 +195,12 @@ describe("Tests w/Specified Included Request Methods", () => {
     assert.isTrue(next.calledOnce);
   });
   it("does not allow if the CSRF token is incorrect", () => {
     const req = mockRequest({
       method: "PUT",
-      cookies: {
-        csrfToken: "aaaabbb"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaabbb", this.secret)
       },
       body: {
         _csrf: "aaaa"
       }
     });
@@ -188,19 +211,19 @@ describe("Tests w/Specified Included Request Methods", () => {
     assert.isNotFunction(req.csrfToken);
     try {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });
   it("allows if the method is specified as not included", () => {
     const req = mockRequest({
       method: "GET",
-      cookies: {
-        csrfToken: "aaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaa", this.secret)
       },
       body: {
         _csrf: ""
       }
     });
@@ -214,18 +237,19 @@ describe("Tests w/Specified Included Request Methods", () => {
   });
 });
 
 describe("Tests w/Specified Excluded URLs", () => {
   before(() => {
-    this.csrf = csurf(null, ["/detail", /\/detail\.*/i]);
+    this.secret = "123456789iamasecret987654321look";
+    this.csrf = csurf(this.secret, null, ["/detail", /\/detail\.*/i]);
   });
 
   it("allows if the URL is marked as excluded", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaaa", this.secret)
       },
       body: {
         _csrf: ""
       },
       originalUrl: "/detail"
@@ -239,18 +263,16 @@ describe("Tests w/Specified Excluded URLs", () => {
     assert.isTrue(next.calledOnce);
     assert.isFunction(
       req.csrfToken,
       "Did not add the csrfToken to this request"
     );
-    const sampleToken = req.csrfToken();
-    assert.equal(sampleToken, "aaaaa", "Not reusing csrf tokens");
   });
   it("allows if the URL is marked as excluded as a regexp", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
-        csrfToken: "aaaaa"
+      signedCookies: {
+        csrfToken: encryptCookie("aaaaa", this.secret)
       },
       body: {
         _csrf: ""
       },
       originalUrl: "/detail/603f31d8-9615-40a4-a609-f7bd9059ee6c"
@@ -264,17 +286,15 @@ describe("Tests w/Specified Excluded URLs", () => {
     assert.isTrue(next.calledOnce);
     assert.isFunction(
       req.csrfToken,
       "Did not add the csrfToken to this request"
     );
-    const sampleToken = req.csrfToken();
-    assert.equal(sampleToken, "aaaaa", "Not reusing csrf tokens");
   });
   it("generates a new token if no token is supplied", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {},
+      signedCookies: {},
       body: {
         _csrf: ""
       },
       originalUrl: "/detail"
     });
@@ -293,11 +313,11 @@ describe("Tests w/Specified Excluded URLs", () => {
     assert.isString(sampleToken);
   });
   it("does not allow if the CSRF token is incorrect and the URL is not marked as excluded", () => {
     const req = mockRequest({
       method: "POST",
-      cookies: {
+      signedCookies: {
         csrfToken: "aaaabbb"
       },
       body: {
         _csrf: "aaaa"
       },
@@ -310,10 +330,10 @@ describe("Tests w/Specified Excluded URLs", () => {
     assert.isNotFunction(req.csrfToken);
     try {
       this.csrf(req, res, next);
     } catch (err) {
       assert.equal(err.name, "Error");
-      assert.include(err.message, "Did not get a CSRF token");
+      assert.include(err.message, "Did not get a valid CSRF token");
     }
     assert.isFalse(next.calledOnce);
   });
 });

Vulnerable code: Defaults to only requiring CSRF protection on `POST` requests and
excludes no URLs. 
This uses the built-in [`crypto`
library](https://nodejs.org/api/crypto.html#cryptorandomuuidoptions)
for generating CSRF tokens. This may or may not be sufficient for your
needs. 
const csurf = (forbiddenMethods, excludedUrls) => {
  if (!forbiddenMethods) forbiddenMethods = ["POST"];
    if (!req.cookies || !res.cookie)
        if (!req.cookies.csrfToken) {
          const csrfToken = randomUUID();
          res.cookie("csrfToken", csrfToken);
          return csrfToken;
        }
        return req.cookies.csrfToken;
      const { csrfToken } = req.cookies;
        (req.query._csrf === csrfToken ||
          req.params._csrf === csrfToken ||
          req.body._csrf === csrfToken)
        res.cookie("csrfToken", "");
          `Did not get a CSRF token for ${req.method} ${req.originalUrl}: ${req.body._csrf} v. ${csrfToken}`
        if (!req.cookies.csrfToken) {
          const csrfToken = randomUUID();
          res.cookie("csrfToken", csrfToken);
          return csrfToken;
        }
        return req.cookies.csrfToken;
// module.exports = csurf(forbiddenMethods, excludedUrls);
  "version": "1.0.3",
    this.csrf = csurf(null, []);
      cookies: {},
      cookies: {
        csrfToken: "aaaa"
      cookies: {
        csrfToken: "aaaabbb"
      assert.include(err.message, "Did not get a CSRF token");
      cookies: {
        csrfToken: "aaaabbb"
      assert.include(err.message, "Did not get a CSRF token");
      cookies: {},
      assert.include(err.message, "Did not get a CSRF token");
  it("will reuse if the CSRF token already exists in a non-POST", () => {
    const req = mockRequest({
      method: "GET",
      cookies: {
        csrfToken: "aaaabc"
      },
      body: {}
    });
    const res = mockResponse({
      cookie: sinon.stub()
    });
    const next = sinon.stub();
    assert.isNotFunction(req.csrfToken);
    this.csrf(req, res, next);
    assert.isTrue(next.calledOnce);
    assert.isFunction(
      req.csrfToken,
      "Did not attach a csrfToken function to req"
    );
    const sampleToken = req.csrfToken();
    assert.equal(sampleToken, "aaaabc");
  });
    this.csrf = csurf(["POST", "PUT"], []);
      cookies: {
        csrfToken: "aaaa"
      cookies: {
        csrfToken: "aaaabbb"
      assert.include(err.message, "Did not get a CSRF token");
      cookies: {
        csrfToken: "aaaa"
    this.csrf = csurf(null, ["/detail", /\/detail\.*/i]);
      cookies: {
        csrfToken: "aaaaa"
    const sampleToken = req.csrfToken();
    assert.equal(sampleToken, "aaaaa", "Not reusing csrf tokens");
      cookies: {
        csrfToken: "aaaaa"
    const sampleToken = req.csrfToken();
    assert.equal(sampleToken, "aaaaa", "Not reusing csrf tokens");
      cookies: {},
      cookies: {
      assert.include(err.message, "Did not get a CSRF token");
Fixed code: Note that if you require very specific security needs you may want to
look elsewhere. This library supports encrypting cookies on the client
side to prevent malicious attackers from looking in. It does not
protect against things such as [double submit
cookies](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie). Those
setups require greater setup and more know-how. This library aims to
be simple to setup. 

const session = require("express-session");
app.use(
  session({})
);
    "123456789iamasecret987654321look",  // secret -- must be 32 bits or chars in length
The secret must be 32 bits (e.g. characters) in length and uses 
[the built-in `crypto.createCipheriv` library built into Node
](https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options). The
secret length is enforced by the
[`AES-256-CBC`](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)
algorithm. 
Defaults to only requiring CSRF protection on `POST`, `PUT`, and `PATCH` requests and
excludes no URLs. The csrf will be checked for in the body of a
request via `_csrf`. 


## License

[MIT](#)
const { randomBytes, createCipheriv, createDecipheriv } = require("crypto");

const ALGORITHM = "aes-256-cbc";

const encryptCookie = (cookie, _secret) => {
  /**
   * Encrypt a cookie using AES 256 bits
   * @param {cookie} string the cookie we want to encrypt. Will be visible as plain string to client.
   * @param {_secret} string the secret that will be stored server-side. Client will never see this.
   */
  const iv = randomBytes(16);
  const _cipher = createCipheriv(ALGORITHM, Buffer.from(_secret), iv);
  const encrypted = [
    iv.toString("hex"),
    ":",
    _cipher.update(cookie, "utf8", "hex"),
    _cipher.final("hex")
  ];
  return encrypted.join("");
};

const decryptCookie = (cookie, _secret) => {
  /**
   * Decrypt a cookie using AES 256 bits
   * @param {cookie} string the cookie we want to encrypt. Will be visible as plain string to client.
   * @param {_secret} string the secret that will be stored server-side. Client will never see this.
   */
  const _encryptedArray = cookie.split(":");
  if (_encryptedArray.length != 2) throw new Error("bad decrypt");
  const iv = new Buffer(_encryptedArray[0], "hex");
  const encrypted = new Buffer(_encryptedArray[1], "hex");
  const decipher = createDecipheriv(ALGORITHM, _secret, iv);
  const decrypted =
    decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
  return decrypted;
};

const verifyCsrf = (requestCsrf, cookieCsrf, _secret) => {
  /**
   * Verify a CSRF token
   * @param {requestCsrf} string the CSRF coming from client side
   * @param {cookieCsrf} string the CSRF as stored in the user's cookies
   * @param {_secret} string the string used to encrypt the CSRF in the first place.
   */
  try {
    const decryptedCookie = decryptCookie(cookieCsrf, _secret);
    return decryptedCookie === requestCsrf;
  } catch (err) {
    return false;
  }
};

module.exports = {
  encryptCookie,
  decryptCookie,
  verifyCsrf
};
const { encryptCookie, verifyCsrf } = require("./encryption");
const cookieParams = {
  httpOnly: true,
  sameSite: "strict",
  signed: true,
  maxAge: 300000
};

const csurf = (secret, forbiddenMethods, excludedUrls) => {
  if (!forbiddenMethods) forbiddenMethods = ["POST", "PUT", "PATCH"];
  if (secret.length != 32)
    throw new Error("Your secret is not the required 32 characters long");
    if (!req.cookies || !res.cookie || !req.signedCookies)
        const csrfToken = randomUUID();
        res.cookie("csrfToken", encryptCookie(csrfToken, secret), cookieParams);
        return csrfToken;
      const { csrfToken } = req.signedCookies;
        verifyCsrf(req.body?._csrf, csrfToken, secret)
        res.cookie("csrfToken", null, cookieParams);
          `Did not get a valid CSRF token for '${req.method} ${req.originalUrl}': ${req.body?._csrf} v. ${csrfToken}`
        const csrfToken = randomUUID();
        res.cookie("csrfToken", encryptCookie(csrfToken, secret), cookieParams);
        return csrfToken;
  "version": "1.1.0",
const { randomUUID, randomBytes } = require("crypto");
const { encryptCookie, decryptCookie, verifyCsrf } = require("./encryption");

describe("Cookie Encryption Tests", () => {
  before(() => {});
  after(() => {});

  it("will encrypt and decrypt a cookie", () => {
    // req.cookies.csrfToken =>  will be scrambled
    const secret = "123456789iamasecret987654321look";
    const csrfToken = randomUUID();
    const encryptedCsrfToken = encryptCookie(csrfToken, secret);
    assert.notEqual(
      csrfToken,
      encryptedCsrfToken,
      "Both are equal as plain string and shouldn't be"
    );
    assert.isTrue(verifyCsrf(csrfToken, encryptedCsrfToken, secret));
    assert.isFalse(
      verifyCsrf(
        csrfToken,
        encryptedCsrfToken,
        randomBytes(16).toString("hex")
      ),
      "Should not be able to verify without the secret"
    );
    assert.isFalse(
      verifyCsrf(randomUUID(), encryptedCsrfToken, secret),
      "Should not verify random UUID tokens"
    );
    assert.isFalse(
      verifyCsrf("", encryptedCsrfToken, secret),
      "Should not verify random blank tokens"
    );
    assert.isFalse(
      verifyCsrf(null, encryptedCsrfToken, secret),
      "Should not verify random null tokens"
    );
  });
});
    this.secret = "123456789iamasecret987654321look";
    this.csrf = csurf(this.secret);
  it("throw internal error if our secret is not long enough", () => {
    assert.throws(() => csurf("imnotlongenough"));
  });
      signedCookies: {},
      signedCookies: {
        csrfToken: encryptCookie("aaaa", this.secret)
      signedCookies: {
        csrfToken: encryptCookie("aaaabbb", this.secret)
      assert.include(err.message, "Did not get a valid CSRF token");
      signedCookies: {
        csrfToken: encryptCookie("aaaabbb", this.secret)
      assert.include(err.message, "Did not get a valid CSRF token");
      signedCookies: {},
      assert.include(err.message, "Did not get a valid CSRF token");
    this.secret = "123456789iamasecret987654321look";
    this.csrf = csurf(this.secret, ["POST", "PUT"], []);
      signedCookies: {
        csrfToken: encryptCookie("aaaa", this.secret)
      signedCookies: {
        csrfToken: encryptCookie("aaaabbb", this.secret)
      assert.include(err.message, "Did not get a valid CSRF token");
      signedCookies: {
        csrfToken: encryptCookie("aaaa", this.secret)
    this.secret = "123456789iamasecret987654321look";
    this.csrf = csurf(this.secret, null, ["/detail", /\/detail\.*/i]);
      signedCookies: {
        csrfToken: encryptCookie("aaaaa", this.secret)
      signedCookies: {
        csrfToken: encryptCookie("aaaaa", this.secret)
      signedCookies: {},
      signedCookies: {
      assert.include(err.message, "Did not get a valid CSRF token");