UNPKG

unixcrypt

Version:

Node.js implementation of Unixcrypt, specifically SHA-256 and SHA-512

289 lines (258 loc) 9.96 kB
import { encrypt, verify } from "./index.js" import { describe, it } from "vitest" import { strict as assert } from "node:assert" /** * These tests are copied from the Public Domain reference implementation by Ulrich Drepper * https://www.akkadia.org/drepper/SHA-crypt.txt */ const tests2 = [ [ "$6$saltstring", "Hello world!", "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1", ], [ "$6$rounds=10000$saltstringsaltstring", "Hello world!", "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.", ], [ "$6$rounds=5000$toolongsaltstring", "This is just a test", "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0", ], [ "$6$rounds=1400$anotherlongsaltstring", `a very much longer text to encrypt. This one even stretches over morethan one line.`, "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wPvMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1", ], [ "$6$rounds=77777$short", "we have a short salt string but not a short password", "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0", ], [ "$6$rounds=123456$asaltof16chars..", "a short string", "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1", ], [ "$6$rounds=10$roundstoolow", "the minimum number is still observed", "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX.", ], ] describe("The standard and extended test suites", () => { it("Should pass standard test suite", () => { const data = tests2[0] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) // Tests collected from other sources it("Should pass extended test suite", () => { const data = [ "$6$salt", "pass", "$6$salt$3aEJgflnzWuw1O3tr0IYSmhUY0cZ7iBQeBP392T7RXjLP3TKKu3ddIapQaCpbD4p9ioeGaVIjOHaym7HvCuUm0", ] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should pass extended test suite with rounds specified", () => { const data = [ "$6$rounds=1000$salt", "pass", "$6$rounds=1000$salt$NqhXojlgP5NLvJojBnjQD87i66jhb8s3bZord3hSZoIgbCJqUfJdp7pclsLBBqgn02fAtd/vn4lieLeX5J.h90", ] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) }) describe("salt handling", () => { it("Should properly truncate too long salt strings", () => { const data = tests2[1] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should properly truncate too long salt strings, and propagate rounds-string even if it's the default", () => { const data = tests2[2] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should handle long salt and long password", () => { const data = tests2[3] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should handle short salt with long password", () => { const data = tests2[4] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should handle short salt with shorter password", () => { const data = tests2[5] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) describe("proper handling of a salt value of empty string", () => { it("should properly handle a default number of rounds", () => { const plaintext = "Plaintext password" const salt = "$5$" const computed = encrypt(plaintext, salt) // generated via python3 passlib const expected = "$5$$N4LFaQGbHo.i9hNn66aHdu9x4vZPEBTPaQLsHflcuz6" assert.equal(verify(plaintext, expected), true) assert.equal(computed, expected) }) it("should handle a custom number of rounds", () => { const plaintext = "Plaintext password" const salt = "$5$rounds=4000$" const computed = encrypt(plaintext, salt) // generated via python3 passlib const expected = "$5$rounds=4000$$CHEsdlQ9TAiLmI4PkGkez4Ny1dIgHa.4ZTzCYGhRzK0" assert.equal(verify(plaintext, expected), true) assert.equal(computed, expected) }) }) }) describe("sha256 crypt handling", () => { it("Should handle sha256crypt as well", () => { const data = [ "$5$rounds=5000$3a1afb28e54a0391", "super password", "$5$rounds=5000$3a1afb28e54a0391$0d6RupbpABtxCaH8WWOemYwEcToDVZXX/tHpIy6O1U3", ] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should handle sha256crypt with additional rounds", () => { const data = [ "$5$rounds=10000$b2c0a3ef466b2ec7", "super password", "$5$rounds=10000$b2c0a3ef466b2ec7$2.jZTNfaxIRW5CbTLoXiga/oUEA3bE9E1jgdquXq5R.", ] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should handle sha256crypt with short salt", () => { const data = [ "$5$salt", "super password", "$5$salt$hiNtIdUiCzVfs12fahM0sjQcF6XU0yE5G46VOsYmS4D", ] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should handle sha256crypt with long salt", () => { const data = [ "$5$averylongsaltstring", "super password", "$5$averylongsaltstr$Tm/C6ErlCKkargHckqaFwBcFTdUdps1p.B3SFRCBue8", ] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) }) describe("Miscellaneous", () => { it("Should not allow rounds fewer than 1000", () => { const data = tests2[6] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should handle multibyte characters", () => { const data = [ "$6$a7a5dc2fa314dda0", "asdf£", "$6$a7a5dc2fa314dda0$E1GTcgT52oJFvhETaKwBk26Gy0GIzNQu2Mv4.UZwXp00CQi/8vC3IQKcrmpqbUaM2jFMOcDoShcxo1Mrt/Z5k/", ] const compute = encrypt(data[1], data[0]) assert.equal(compute, data[2]) assert.equal(verify(data[1], data[2]), true) }) it("Should be possible to only specify the SHA-type", () => { const plaintext = "Plaintext password" const salt = "$6" // this should not throw an exception const compute = encrypt(plaintext, salt) assert.equal(verify(plaintext, compute), true) }) it("Should be possible to only specify the SHA-type, and the number of rounds", () => { const plaintext = "Plaintext password" const salt = "$6$rounds=10000" // this should not throw an exception const compute = encrypt(plaintext, salt) assert.equal(verify(plaintext, compute), true) }) it("Should be possible to not specify a salt at all", () => { const plaintext = "Plaintext password" // this should not throw an exception const compute = encrypt(plaintext) assert.equal(verify(plaintext, compute), true) }) }) describe("Invalid inputs", () => { it("Should throw an exception when used with any other crypto than sha256 or sha512", () => { const data = ["$1$4WZnIm8V", "pass", "$1$4WZnIm8V$Sg8KVWIq4rKfNz3Z23jZK0"] assert.throws( () => encrypt(data[1], data[0]), Error, "Only sha256 and sha512 is supported by this library", ) assert.throws( () => verify(data[1], data[2]), Error, "Only sha256 and sha512 is supported by this library", ) }) it("Should throw an exception when salt contains invalid characters", () => { const data = [ "$6$invalid-salt", "asdf£", "$6$invalid-salt$this is moot because the salt is invalid", ] assert.throws(() => encrypt(data[1], data[0]), Error, "Invalid salt string") assert.throws(() => verify(data[1], data[2]), Error, "Invalid salt string") }) it("Should throw an exception when the salt string contains too many '$'-characters", () => { const data = [ "$6$invalid$salt$string", "pass", "$6$invalid$salt$string$this is moot because the salt is invalid", ] assert.throws(() => encrypt(data[1], data[0]), Error, "Invalid salt string") assert.throws(() => verify(data[1], data[2]), Error, "Invalid salt string") }) it("Should throw an exception when the rounds-part of the salt is malformed", () => { const data = [ "$6$round=5000$salt", "pass", "$6$round=5000$salt$this is moot because the salt is invalid", ] assert.throws(() => encrypt(data[1], data[0]), Error, "Invalid salt string") assert.throws(() => verify(data[1], data[2]), Error, "Invalid salt string") }) // LOL. This is not testable. // FATAL ERROR: invalid table size Allocation failed - JavaScript heap out of memory // it("Should be reduce the number of rounds if larger than 999,999,999", () => { // const plaintext = "Plaintext password"; // const salt = "$6$rounds=1000000000$salt"; // const hash = ""; // const compute = encrypt(plaintext, salt); // expect(compute, hash); // expect(verify(plaintext, hash)).toBe(true); // }); })