tsPwGen Package

Author: thosti.

The tsPwGen contributed package generates passwords, PINs, hexadecimal tokens and passphrases, and computes the entropy of a recipe before anything is generated. The package describes itself as “Passwords you can measure before you generate.” This section documents version 1.0.8.0, published under the MIT licence.

A recipe is a PwGenOptions object. Its five presets, Alphanumeric, Complex, Passphrase, Pin and Hex, cover the common cases, and its properties turn a preset into any other recipe. PwGen.Generate draws a result from a recipe. PwGen.Entropy reports the entropy of a recipe in bits without drawing anything. Entropy is the base-2 logarithm of the number of results a recipe can produce, when each result is equally likely: a recipe with 128 bits can produce about 2^128 different results.

Dim o As PwGenOptions = PwGenOptions.Complex
o.Length = 20
Debug.Print CLng(PwGen.Entropy(o))                           ' 125

Dim password As String
If PwGen.TryGenerate(password, o) Then
    Debug.Print Len(password)                                ' 20
End If

Dim phrase As PwGenOptions = PwGenOptions.Passphrase
Debug.Print CLng(PwGen.Entropy(phrase))                      ' 65
Debug.Print UBound(Split(PwGen.Generate(phrase), "-")) + 1   ' 5

The presets

Each preset is a read-only property of PwGenOptions that returns a new recipe. The values below are the ones the presets start with; every property of the new recipe can be changed.

Preset Result Entropy
Alphanumeric 16 characters from the letters and digits, without the five look-alikes 0, O, 1, l and I, so from 57 characters. The password has at least one uppercase letter, one lowercase letter and one digit. 93.3 bits
Complex Like Alphanumeric, with the 20 special characters !#$%&()+,-.;=@[]_{}~ added, so from 77 characters. The password also has at least one special character. 100.3 bits
Passphrase Five words from the English word list, separated by -. 65 bits
Pin 6 digits. The look-alikes are kept, so all ten digits are used. 19.9 bits
Hex 32 characters from 0123456789abcdef. 128 bits

Every function of the package that takes a recipe also accepts Nothing, which means a new Alphanumeric recipe.

Entropy

PwGen.Entropy computes the entropy of a recipe from its settings alone:

  • In random mode, Length * log2(p), where p is the number of different characters the password is drawn from.
  • In wordlist mode, WordCount * log2(n), where n is the number of words in the list. When the words of a passphrase must be different (HasUniqueWords), the sum of log2(n), log2(n - 1), log2(n - 2) and so on, one term for each word.

The separator and the capital letters of a passphrase add nothing. The constraints of random mode, MinPerClass, StartsWithLetter and HasNoConsecutiveRepeats, do not lower the figure, so the entropy of a password drawn under them can be smaller than the figure.

Errors

A recipe that cannot be used does not raise a run-time error. PwGen.Generate returns an empty string, PwGen.Entropy returns 0, and the last error is set. The predeclared object PwGenError reports it: HasError is True, Code is a short name such as InvalidLength, and Message and Origin describe the error and the function that found it. Each function of PwGen clears the last error when it starts, so the last error always belongs to the latest call. PwGen.TryGenerate returns False instead of an empty string when it fails.

The package raises a run-time error in one case: assigning a property on the predeclared object PwGenOptions itself, which is a factory and not a recipe. The error is 5, Invalid procedure call or argument.

Random sources

Characters and words are drawn with a PwGenRandom object. A recipe whose RandomSource is Nothing uses PwGenCryptoSource, which calls the Windows CNG function BCryptGenRandom. Any object that implements the PwGenRandom interface can be assigned to RandomSource instead. A source that returns fixed values makes the result predictable, which the samples on these pages use.

Word lists

The package has two built-in word lists, English and German, of 8192 words each. No word appears twice in a list, and 8192 is 2^13, so each word of a passphrase from a built-in list adds 13 bits. Every word of a list is written with the letters a to z only: the German list spells the umlauts and the sharp s as ae, oe, ue and ss.

Debug.Print PwGen.BuiltInWordCount(pgwEnglish)    ' 8192
Debug.Print PwGen.BuiltInWord(pgwEnglish, 0)      ' abandon
Debug.Print PwGen.BuiltInWord(pgwEnglish, 8191)   ' zurich
Debug.Print PwGen.BuiltInWord(pgwGerman, 0)       ' aal
Debug.Print PwGen.BuiltInWord(pgwGerman, 8191)    ' zypresse

A recipe can use its own list instead, through CustomWords.

Classes and modules

Test code

The package also contains its own test code, which has no pages: the modules Tests, TestsCore, TestsRandom and TestsWordlist, and the class TestsFakeRandom. Tests.RunAll runs three areas and prints the result of each check with Debug.Print, so the lines appear in the DEBUG CONSOLE of the IDE and nowhere else. The Core area (TestsCore) checks the constants, the presets, the guard on the predeclared PwGenOptions object, Clone, the last error and its snapshot, TryGenerate and an invalid mode. The Random area (TestsRandom) checks the entropy formulas, the recipes, Alphabet, IncludeChars and ExcludeChars, MinPerClass, StartsWithLetter, HasNoConsecutiveRepeats, results drawn with a fixed random source, and the error codes of random mode. The Wordlist area (TestsWordlist) checks the built-in lists, the default passphrase, CustomWords, HasUniqueWords, the separator and Capitalize, the entropy of passphrases and the error codes of wordlist mode. Each check prints OK, FAIL or SKIP, a failed check does not stop the run, and the last lines give the counts and the elapsed time and say that the tests passed or failed. In version 1.0.8.0 the run has 349 checks that pass, none that fail and 3 that are skipped, and it ends with a line that says the tests passed. The three skipped checks stand for the error codes EmptyCharset, EmptyWordlist and InvalidRange, which no recipe sets.

TestsFakeRandom implements PwGenRandom for the tests. It returns the lower bound of each interval it is given; after SetAlwaysUpper True it returns the upper bound, and after SetOutOfRange True it returns a value one above the upper bound, which makes the generator report RngFailed.

Internal modules

Six more modules are part of the package’s implementation and have no pages: PwGenEngine (the generator), PwGenCrypto (the CNG call and the shared default random source), PwGenErrorStore (the storage behind PwGenError), PwGenWords (access to the word lists), and PwGenWordsEn and PwGenWordsDe (the data of the two lists). The package describes each of them as not part of its public API. None of them is declared Private, so a project that references the package can name them.

Debug.Print PwGenWords.CountOf(pgwEnglish, "x")   ' 8192

The names are visible to the whole project: the Generate and Entropy functions of PwGenEngine make an unqualified call of the functions of PwGen ambiguous (see Known problems).

Known problems in version 1.0.8.0

Note

The package has four more problems. None of them returns a wrong password.

  • HasNoConsecutiveRepeats fails for small pools. A recipe with HasNoConsecutiveRepeats returns an empty string with the error CannotAvoidRepeats in about one call of four for the pool AB and a Length of 3, although valid passwords exist.
  • An unqualified call is ambiguous. Generate() and Entropy() written without PwGen. fail with TB5073, because the internal module PwGenEngine declares functions with the same names. See PwGen.
  • Tests.SetContinueOnFail has no effect. A failed check never stops Tests.RunAll, whatever the setting.
  • Two error codes are never set. The package declares EmptyCharset and EmptyWordlist, but no recipe produces them. See PwGenError.

The first check of this sample fails on purpose. The second check still runs after SetContinueOnFail False:

Tests.BeginRun
Tests.SetContinueOnFail False
Tests.CheckTrue "first", False
Tests.CheckTrue "second", True
Debug.Print Tests.FailCount()
' Output:
' === tsPwGen tests ==========================================
' FAIL  first
' expected: True
' actual:   False
' OK    second
' 1

See Also

  • Importing a package from TWINSERV – adding a published package to a project
  • Interface statement – declares an interface such as PwGenRandom
  • Implements statement – implements an interface in a class
  • Like operator – compares a string with a pattern, as the samples on these pages do to test the characters of a result