Skip to content

Commit

Permalink
feat: Add GetFilteredWordList to asset, this allows us to filter the …
Browse files Browse the repository at this point in the history
…word list as we are creating the word list. Instead of creating an intermediate slice containing all words and creating another slice with filtered words

feat!: add GenerateDigit to RNGService interface
fix: issue where if you used a random seperator it wouldn't remove edge characters when it should have
doc: add comments for public methods and other code where appropriate
  • Loading branch information
eljamo committed Dec 7, 2023
1 parent ad54316 commit 70fd828
Show file tree
Hide file tree
Showing 17 changed files with 549 additions and 136 deletions.
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,51 @@
# libpass

Library code which can be used to create tools which generate memorable passwords, used by [mempass](https://github.com/eljamo/mempass)
A Go library specifically designed for generating secure and memorable passwords. It serves as the backbone for [mempass](https://https://github.com/eljamo/mempass)

## Basic Usage

```
package main
import (
"fmt"
"github.com/eljamo/libpass/v4/config"
"github.com/eljamo/libpass/v4/service"
)
func main() {
specialCharacters := []string{
"!", "@", "$", "%", "^", "&", "*", "-", "+", "=", ":", "|", "~", "?", "/", ".", ";",
}
config := &config.Config{
WordList: "EN",
NumPasswords: 3,
NumWords: 3,
WordLengthMin: 4,
WordLengthMax: 8,
CaseTransform: "RANDOM",
SeparatorCharacter: "RANDOM",
SeparatorAlphabet: specialCharacters,
PaddingDigitsBefore: 2,
PaddingDigitsAfter: 2,
PaddingType: "FIXED",
PaddingCharacter: "RANDOM",
SymbolAlphabet: specialCharacters,
PaddingCharactersBefore: 2,
PaddingCharactersAfter: 2,
}
svc, err := service.NewPasswordGeneratorService(config)
if err != nil {
fmt.Println(err)
}
pws, err := svc.Generate()
if err != nil {
fmt.Println(err)
}
fmt.Println(pws)
}
```
87 changes: 71 additions & 16 deletions asset/asset.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package asset

import (
"bufio"
"embed"
"encoding/json"
"fmt"
"os"
"path"
"strings"

"github.com/eljamo/libpass/v3/config"
"github.com/eljamo/libpass/v4/config"
)

//go:embed preset/* word_list/*
var Files embed.FS

var files embed.FS
var fileMap = map[string]map[string]string{
config.PresetKey: {
config.AppleID: "appleid.json",
Expand Down Expand Up @@ -42,53 +43,107 @@ var fileMap = map[string]map[string]string{

func keyToFile(key, fileType string) (string, bool) {
file, ok := fileMap[fileType][strings.ToUpper(key)]

return file, ok
}

func loadFileData(filePath string, readerFunc func(string) ([]byte, error)) (string, error) {
func loadJSONFileData(filePath string, readerFunc func(string) ([]byte, error)) (string, error) {
data, err := readerFunc(filePath)
if err != nil {
return "", fmt.Errorf("failed to read file '%s': %w", filePath, err)
return "", fmt.Errorf("failed to read file (%s): %w", filePath, err)
}

var parsed any
if err := json.Unmarshal(data, &parsed); err != nil {
return "", fmt.Errorf("invalid JSON content in '%s': %w", filePath, err)
return "", fmt.Errorf("invalid JSON content in (%s): %w", filePath, err)
}

jsonData, err := json.Marshal(parsed)
if err != nil {
return "", fmt.Errorf("error marshaling JSON data from '%s': %w", filePath, err)
return "", fmt.Errorf("marshaling JSON data for %s failed: %w", filePath, err)
}

return string(jsonData), nil
}

// LoadJSONFile reads a JSON file from the given file path and returns its
// content as a string. It handles file reading and JSON unmarshalling. In case
// of any error during these operations, an error is returned
func LoadJSONFile(filePath string) (string, error) {
return loadFileData(filePath, os.ReadFile)
return loadJSONFileData(filePath, os.ReadFile)
}

func GetWordList(key string) ([]string, error) {
func getWordListFilePath(key string) (string, error) {
fileName, ok := keyToFile(key, config.WordListKey)
if !ok {
return nil, fmt.Errorf("invalid word list key '%s'", key)
return "", fmt.Errorf("invalid %s value (%s)", config.WordListKey, key)
}

filePath := fmt.Sprintf("%s/%s", config.WordListKey, fileName)
data, err := Files.ReadFile(filePath)
return path.Join(config.WordListKey, fileName), nil
}

// GetWordList retrieves a list of words from an embedded file identified by the
// given key. The method reads the file content, splits it by newline characters
// and returns the result as a slice of strings. If the file cannot be found or
// read, an error is returned.
func GetWordList(key string) ([]string, error) {
path, err := getWordListFilePath(key)
if err != nil {
return nil, fmt.Errorf("failed to read embedded text file '%s': %w", filePath, err)
return nil, err
}

data, err := files.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read embedded text file (%s): %w", path, err)
}

return strings.Split(string(data), "\n"), nil
}

// GetFilteredWordList reads a word list from an embedded file identified by the
// given key, and filters the words based on the specified minimum and maximum
// length. It returns a slice of strings that meet the length criteria. If the
// file cannot be opened or read, or if an error occurs during scanning, an
// error is returned.
func GetFilteredWordList(key string, minLen int, maxLen int) ([]string, error) {
path, err := getWordListFilePath(key)
if err != nil {
return nil, err
}

file, err := files.Open(path)
if err != nil {
return nil, err
}
defer file.Close()

var wl []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if len(line) >= minLen && len(line) <= maxLen {
wl = append(wl, line)
}
}

if err := scanner.Err(); err != nil {
return nil, err
}

return wl, nil
}

// GetJSONPreset reads a JSON preset file identified by the given key from
// embedded files. It returns the content of the JSON file as a string. If the
// file is not found, cannot be read, or contains invalid JSON, an error is
// returned.
func GetJSONPreset(key string) (string, error) {
fileName, ok := keyToFile(key, config.PresetKey)
if !ok {
return "", fmt.Errorf("invalid JSON preset key '%s'", key)
return "", fmt.Errorf("invalid %s value (%s)", config.PresetKey, key)
}

filePath := fmt.Sprintf("%s/%s", config.PresetKey, fileName)
return loadFileData(filePath, Files.ReadFile)
filePath := path.Join(config.PresetKey, fileName)

return loadJSONFileData(filePath, files.ReadFile)
}
135 changes: 83 additions & 52 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,99 +1,130 @@
package config

// Config key
const (
PresetKey = "preset"
WordListKey = "word_list"
PresetKey string = "preset"
WordListKey string = "word_list"
)

// Word list constant
const (
All = "ALL"
DoctorWho = "DOCTOR_WHO"
EN = "EN"
ENSmall = "EN_SMALL"
GameOfThrones = "GAME_OF_THRONES"
HarryPotter = "HARRY_POTTER"
MiddleEarth = "MIDDLE_EARTH"
Pokemon = "POKEMON"
StarTrek = "STAR_TREK"
StarWars = "STAR_WARS"
All string = "ALL"
DoctorWho string = "DOCTOR_WHO"
EN string = "EN"
ENSmall string = "EN_SMALL"
GameOfThrones string = "GAME_OF_THRONES"
HarryPotter string = "HARRY_POTTER"
MiddleEarth string = "MIDDLE_EARTH"
Pokemon string = "POKEMON"
StarTrek string = "STAR_TREK"
StarWars string = "STAR_WARS"
)

// Preset constant
const (
AppleID = "APPLEID"
Default = "DEFAULT"
NTLM = "NTLM"
SecurityQ = "SECURITYQ"
Web16 = "WEB16"
Web16XKPasswd = "WEB16_XKPASSWD"
Web32 = "WEB32"
WiFi = "WIFI"
XKCD = "XKCD"
XKCDXKPasswd = "XKCD_XKPASSWD"
AppleID string = "APPLEID"
Default string = "DEFAULT"
NTLM string = "NTLM"
SecurityQ string = "SECURITYQ"
Web16 string = "WEB16"
Web16XKPasswd string = "WEB16_XKPASSWD"
Web32 string = "WEB32"
WiFi string = "WIFI"
XKCD string = "XKCD"
XKCDXKPasswd string = "XKCD_XKPASSWD"
)

// Shared constant
const (
None = "NONE"
Random = "RANDOM"
None string = "NONE"
Random string = "RANDOM"
)

// Case transform constant
const (
Alternate = "ALTERNATE"
AlternateLettercase = "ALTERNATE_LETTERCASE"
Capitalise = "CAPITALISE"
CapitaliseInvert = "CAPITALISE_INVERT"
Invert = "INVERT" // Same as CapitaliseInvert but reserved to maintain compatibility with xkpasswd.net generated configs
Lower = "LOWER"
LowerVowelUpperConsonant = "LOWER_VOWEL_UPPER_CONSONANT"
Sentence = "SENTENCE"
Upper = "UPPER"
Alternate string = "ALTERNATE"
AlternateLettercase string = "ALTERNATE_LETTERCASE"
Capitalise string = "CAPITALISE"
CapitaliseInvert string = "CAPITALISE_INVERT"
// The same as CapitaliseInvert but reserved to maintain compatibility with xkpasswd.net generated configs
Invert string = "INVERT"
Lower string = "LOWER"
LowerVowelUpperConsonant string = "LOWER_VOWEL_UPPER_CONSONANT"
Sentence string = "SENTENCE"
Upper string = "UPPER"
)

// Padding type constant
const (
Adaptive = "ADAPTIVE"
Fixed = "FIXED"
Adaptive string = "ADAPTIVE"
Fixed string = "FIXED"
)

// A slice of available presets
var Preset = []string{
Default, AppleID, NTLM, SecurityQ, Web16, Web16XKPasswd, Web32, WiFi, XKCD,
XKCDXKPasswd,
}

// A slice of special characters which can be used for padding and separator
// characters
var DefaultSpecialCharacters = []string{
"!", "@", "$", "%", "^", "&", "*", "-", "+", "=", ":", "|", "~", "?", "/", ".", ";",
}

// A slice of available options for padding
var PaddingType = []string{Adaptive, Fixed, None}

// A slice of available options for case transformation
var TransformType = []string{
Alternate, AlternateLettercase, Capitalise, CapitaliseInvert, Invert, Lower,
LowerVowelUpperConsonant, None, Random, Sentence, Upper,
}

// A slice of available options for padding and separator characters
var PaddingCharacterAndSeparatorCharacter = append([]string{Random}, DefaultSpecialCharacters...)

// A slice of available word lists
var WordLists = []string{
All, DoctorWho, EN, ENSmall, GameOfThrones, HarryPotter, MiddleEarth,
StarTrek, StarWars,
}

type Config struct {
CaseTransform string `key:"case_transform" json:"case_transform,omitempty"`
NumPasswords int `key:"num_passwords" json:"num_passwords,omitempty"`
NumWords int `key:"num_words" json:"num_words,omitempty"`
PaddingCharactersAfter int `key:"padding_characters_after" json:"padding_characters_after,omitempty"`
PaddingCharactersBefore int `key:"padding_characters_before" json:"padding_characters_before,omitempty"`
PaddingCharacter string `key:"padding_character" json:"padding_character,omitempty"`
PaddingDigitsAfter int `key:"padding_digits_after" json:"padding_digits_after,omitempty"`
PaddingDigitsBefore int `key:"padding_digits_before" json:"padding_digits_before,omitempty"`
PaddingType string `key:"padding_type" json:"padding_type,omitempty"`
PadToLength int `key:"pad_to_length" json:"pad_to_length,omitempty"`
Preset string `key:"preset" json:"preset,omitempty"`
SeparatorAlphabet []string `key:"separator_alphabet" json:"separator_alphabet,omitempty"`
SeparatorCharacter string `key:"separator_character" json:"separator_character,omitempty"`
SymbolAlphabet []string `key:"symbol_alphabet" json:"symbol_alphabet,omitempty"`
WordLengthMax int `key:"word_length_max" json:"word_length_max,omitempty"`
WordLengthMin int `key:"word_length_min" json:"word_length_min,omitempty"`
WordList string `key:"word_list" json:"word_list,omitempty"`
// The type of case transformation to apply to the words
CaseTransform string `key:"case_transform" json:"case_transform,omitempty"`
// The number of passwords to generate
NumPasswords int `key:"num_passwords" json:"num_passwords,omitempty"`
// The number of words to use in the password
NumWords int `key:"num_words" json:"num_words,omitempty"`
// The number of padding characters to add after the password
PaddingCharactersAfter int `key:"padding_characters_after" json:"padding_characters_after,omitempty"`
// The number of padding characters to add before the password
PaddingCharactersBefore int `key:"padding_characters_before" json:"padding_characters_before,omitempty"`
// The character to use for padding
PaddingCharacter string `key:"padding_character" json:"padding_character,omitempty"`
// Te number of padding digits to add after the password
PaddingDigitsAfter int `key:"padding_digits_after" json:"padding_digits_after,omitempty"`
// The number of padding digits to add before the password
PaddingDigitsBefore int `key:"padding_digits_before" json:"padding_digits_before,omitempty"`
// The type of padding to apply to the password
PaddingType string `key:"padding_type" json:"padding_type,omitempty"`
// The length to pad the password to
PadToLength int `key:"pad_to_length" json:"pad_to_length,omitempty"`
// The preset to use for generating the password
Preset string `key:"preset" json:"preset,omitempty"`
// The alphabet to use for the separator character when using a random character
SeparatorAlphabet []string `key:"separator_alphabet" json:"separator_alphabet,omitempty"`
// The character to use to separate the words
SeparatorCharacter string `key:"separator_character" json:"separator_character,omitempty"`
// The alphabet to use for the symbol padding character when random
SymbolAlphabet []string `key:"symbol_alphabet" json:"symbol_alphabet,omitempty"`
// The maximum length of a word to use in the password
WordLengthMax int `key:"word_length_max" json:"word_length_max,omitempty"`
// The minimum length of a word to use in the password
WordLengthMin int `key:"word_length_min" json:"word_length_min,omitempty"`
// The word list to use for generating the password
WordList string `key:"word_list" json:"word_list,omitempty"`
}

var WordListDescriptionMap = map[string]string{
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/eljamo/libpass/v3
module github.com/eljamo/libpass/v4

go 1.21.4
go 1.21.5

require golang.org/x/text v0.14.0
Loading

0 comments on commit 70fd828

Please sign in to comment.