Merge pull request #90 from wneessen/feature/70_group-password-characters-in-a-smart-phone-friendly-order

Add mobile-friendly character groupoing
This commit is contained in:
Winni Neessen 2024-03-25 11:53:42 +01:00 committed by GitHub
commit b7b4073483
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 142 additions and 19 deletions

View file

@ -28,7 +28,7 @@ This section provides some examples on how to use apg-go for common password gen
### Login password for a website
```shell
$ apg -C -f 20 -n 1
$ apg-go -C -f 20 -n 1
Zq#lIY?=?J@4_\X@\xtf
```
**Note:** Nowadays 20 random characters are still considered secure for passwords. You might want to adjust
@ -36,7 +36,7 @@ the `-f` parameter if you require a longer password.
### PIN generation
```shell
$ apg -M lusN -f 6 -n 1
$ apg-go -M lusN -f 6 -n 1
952170
```
**Note:** A code example on how to programatically build a PIN generator with apg-go, can be found
@ -44,7 +44,7 @@ here: [pin-generator](example-code/pin-generator).
### Phone verification phrase (pronounceable)
```shell
$ apg -a 0 -m 15 -x 15 -t -n 1
$ apg-go -a 0 -m 15 -x 15 -t -n 1
vEbErlaFryaNgyex (vE-bEr-la-Fry-aN-gy-ex)
```
We generated a 15-character long pronounceable phrase with syllables output, for easy
@ -52,7 +52,7 @@ use in e. g. a phone verification process.
### Cryptographic key for encryption
```shell
$ apg -a 3 -f 32 -bh
$ apg-go -a 3 -f 32 -bh
```
We generated a 32 bytes/256 bits long fully binary secret that can be used i. e. as
encryption key for a AES-256 symmetric encryption. The output is represented in
@ -200,7 +200,7 @@ By default apg-go will generate 6 passwords, with a minimum length of 12 charact
maxiumum length of 20 characters. The generated password will use a character set constructed
from lower case, upper case and numeric characters.
```shell
$ ./apg-go
$ apg-go
R8rCC8bw5NvJmTUK2g
cHB9qogTbfdzFgnH
hoHfpWAHHSNa4Q
@ -216,7 +216,7 @@ by setting the `-L` parameter. In addition you would set the `-S` parameter to e
characters. Finally the parameter `-n 1` is needed to keep apg-go from generating more than one
password:
```shell
$ ./apg-go -n 1 -L -S
$ apg-go -n 1 -L -S
XY7>}H@5U40&_A1*9I$
```
@ -226,7 +226,7 @@ parameters instead. The new style is all combined in the `-M` parameter. Using t
version of a parameter argument enables a feature, while the lower case version disabled it. The
previous example could be represented like this in new style:
```shell
$ ./apg-go -n 1 -M lUSN
$ apg-go -n 1 -M lUSN
$</K?*|M)%8\U$5JA5~
```
@ -239,7 +239,7 @@ readability, you can set the `-H` parameter to toggle on the "human readable" fe
option is set, apg-go will avoid using any of the typical ambiguous characters in the generated
passwords.
```shell
$ ./apg-go -n 1 -M LUSN -H
$ apg-go -n 1 -M LUSN -H
YpranThY3b6b5%\6ARx
```
@ -248,7 +248,7 @@ Let's assume, that for whatever reason, your generated password can never includ
this specific case, you can use the `-E` parameter to specify a list of characters that are to be excluded
from the password generation character set:
```shell
$ ./apg-go -n 1 -M lUSN -H -E :
$ apg-go -n 1 -M lUSN -H -E :
~B2\%E_|\VV|/5C7EF=
```
@ -258,7 +258,7 @@ parameter, apg-go will automatically default to the most secure settings. The co
basically implies that the password will use all available characters (lower case, upper case,
numeric and special) and will make sure that human readability is disabled.
```shell
$ ./apg-go -n 1 -C
$ apg-go -n 1 -C
{q6cvz9le5_fo"X7
```
@ -268,13 +268,13 @@ want to be more specific, you can use the `-m` and `-x` parameters to override t
assume you want a single complex password with a length of exactly 32 characters you can do so by
running:
```shell
$ ./apg-go -n 1 -C -m 32 -x 32
$ apg-go -n 1 -C -m 32 -x 32
5lc&HBvx=!EUY*;'/t&>B|~sudhtyDBu
```
Alternatively, since v1.0.0 apg-go has the new `-f` flag, which allows to request a fixed length
password. Instead of using `-m` and `-x` you can just use `-f 32` to get a 32 character long password:
```shell
$ ./apg -n 1 -C -f 32
$ apg-go -n 1 -C -f 32
O"Q\d0zT'@(1f~%_56O*!q[!9:z[~\A*
```
@ -283,7 +283,7 @@ If you need to read out a password, it can be helpful to know the corresponding
the phonetic alphabet. By setting the `-l` parameter, agp-go will provide you with the phonetic spelling
(english language) of your newly created password:
```shell
$ ./apg-go -n 1 -M LUSN -H -E : -l
$ apg-go -n 1 -M LUSN -H -E : -l
fUTDKeFsU+zn3r= (foxtrot/Uniform/Tango/Delta/Kilo/echo/Foxtrot/sierra/Uniform/PLUS_SIGN/zulu/november/THREE/romeo/EQUAL_SIGN)
```
@ -312,10 +312,10 @@ randomly generated passwords. It might also be helpful to run the pronoucable pa
"[HIBP](#have-i-been-pwned)" flag, so that each generated password is automatically checked against "Have I Been Pwned"
database.
```shell
$ ./apg-go -a 0 -n 1
$ apg-go -a 0 -n 1
KebrutinernMy
$ ./apg-go -a 0 -n 1 -m 15 -x 15 -t
$ apg-go -a 0 -n 1 -m 15 -x 15 -t
pEnbocydrageT*En (pEn-bo-cy-dra-geT-ASTERISK-En)
```
@ -323,7 +323,7 @@ pEnbocydrageT*En (pEn-bo-cy-dra-geT-ASTERISK-En)
Sometimes you just want to quickly perform a simple, but random coinflip. Since v1.0.0 apg-go has a
coinflip mode, which will return either "Heads" or "Tails". To use coinflip mode, use the `-a 2` argument:
```shell
$ ./apg -n 10 -a 2
$ apg-go -n 10 -a 2
Tails
Tails
Heads
@ -348,12 +348,29 @@ This mode can be useful for example if you need to generate a AES-256 encryption
the default length for the secret generation in this mode, you can simply generate a secret key with
the following command:
```shell
$ apg -a 3 -bh
$ apg-go -a 3 -bh
a1cdab8db365af3d70828b1fe43b7896190c157ad3f1ae2a0a1d52ec1628c6b5
```
*For ease for readability we used the `-bh` flag, to instruct apg-go to output the secret in its
hexadecimal representation*
### Mobile-friendly character grouping
Since v1.2.0 apg-go supports grouping of characters in a mobile-friendly manner. Entering a random string
of characters with a smartphone touch screen is tedious and error prone due to the need to toggle keypads
to gain access to different character tables. For this reason, this feature groups the characters of the
generated password in "keypad-order". It does so by groupoing the characters into character groups. The
following precedense is used: Upper-case characters, lower-case characters, numeric values, any other
character.
Example:
```shell
$ apg-go -C -f 20 -n 1 -g
CETMPGGxuamj346!)>})
```
**Please note that this feature makes the generated passwords much more predictable and lowers the
entropy of the generated password. Please use this feature with caution**
### Minimum required characters
Even though in apg-go you can select what kind of characters are used for the password generation, it is
not guaranteed, that if you request a password with a numeric value, that the generated password will
@ -369,7 +386,7 @@ never being able to finish the job.
Example:
```shell
$ ./apg -n 10 -a 1 -M NLUs -f 20 -mN 3
$ apg-go -n 10 -a 1 -M NLUs -f 20 -mN 3
kqFG935E280LvTFUbJ4M
RVBJAI5tJ6hy6oWrNfXG
uy1IWBEoOQFyG66VrLqu
@ -409,6 +426,7 @@ _apg-go_ replicates most of the parameters of the original c-apg. Some parameter
- `-m <length>`: The minimum length of the password to be generated (Default: 12)
- `-x <length>`: The maximum length of the password to be generated (Default: 20)
- `-f <length>`: Fixed length of the password to be generated (Ignores -m and -x)
- `-g`: When set, mobile-friendly character grouping will be enabled in Algo: 1 (Default: off)
- `-n <number of passwords>`: The amount of passwords to be generated (Default: 6)
- `-E <list of characters>`: Do not use the specified characters in generated passwords
- `-M <[LUNSHClunshc]>`: New style password parameters (upper-case enables, lower-case disables)

2
apg.go
View file

@ -5,7 +5,7 @@
package apg
// VERSION represents the version string
const VERSION = "1.1.0"
const VERSION = "1.2.0"
// Generator is the password generator type of the APG package
type Generator struct {

View file

@ -36,6 +36,7 @@ func main() {
flag.BoolVar(&complexPass, "C", false, "")
flag.StringVar(&config.ExcludeChars, "E", "", "")
flag.Int64Var(&config.FixedLength, "f", 0, "")
flag.BoolVar(&config.MobileGrouping, "g", false, "")
flag.BoolVar(&humanReadable, "H", false, "")
flag.BoolVar(&config.SpellPassword, "l", false, "")
flag.BoolVar(&lowerCase, "L", false, "")
@ -225,6 +226,9 @@ Flags:
-f LENGTH Fixed length of the password to be generated (Ignores -m and -x)
- Note: Due to the way the pronounceable password algorithm works,
this setting might not always apply
-g When set, mobile-friendly character grouping will be enabled in Algo: 1
- Note: Grouping characters in random passwords makes them much
more predictable and lowers the entropy of the generated password.
-n NUMBER Amount of password to be generated (Default: 6)
- Note: Does not apply to binary mode (Algo: 3)
-E CHARS List of characters to be excluded in the generated password

View file

@ -53,6 +53,9 @@ type Config struct {
// MinUpperCase represents the minimum amount of upper-case characters that have
// to be part of the generated password
MinUpperCase int64
// MobileGrouping indicates if the generated password should be grouped in a
// mobile-friendly manner
MobileGrouping bool
// Mode holds the different character modes for the Random algorithm
Mode ModeMask
// NumberPass sets the number of passwords that are generated
@ -175,6 +178,13 @@ func WithMaxLength(length int64) Option {
}
}
// WithMobileGrouping enables the mobile-friendly character grouping for AlgoRandom
func WithMobileGrouping() Option {
return func(config *Config) {
config.MobileGrouping = true
}
}
// WithModeMask overrides the default mode mask for the random algorithm
func WithModeMask(mask ModeMask) Option {
return func(config *Config) {

View file

@ -184,6 +184,18 @@ func TestWithMinUppercase(t *testing.T) {
}
}
func TestWithMobileGrouping(t *testing.T) {
c := NewConfig(WithMobileGrouping())
if c == nil {
t.Errorf("NewConfig(WithMobileGrouping()) failed, expected config pointer but got nil")
return
}
if c.MobileGrouping != true {
t.Errorf("NewConfig(WithMobileGrouping()) failed, expected: %t, got: %t",
true, c.MobileGrouping)
}
}
func TestWithModeMask(t *testing.T) {
e := DefaultMode
c := NewConfig(WithModeMask(e))

27
grouping.go Normal file
View file

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package apg
import "unicode"
// GroupCharsForMobile takes a given string of characters and groups them in a mobile-friendly
// manner. The grouping is based on the following precedense: uppercase, lowercase, numbers
// and special characters. The grouped string is then returned.
func GroupCharsForMobile(chars string) string {
var uppers, lowers, numbers, others []rune
for _, char := range chars {
switch {
case unicode.IsUpper(char):
uppers = append(uppers, char)
case unicode.IsLower(char):
lowers = append(lowers, char)
case unicode.IsNumber(char):
numbers = append(numbers, char)
default:
others = append(others, char)
}
}
return string(uppers) + string(lowers) + string(numbers) + string(others)
}

28
grouping_test.go Normal file
View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package apg
import "testing"
func TestGroupCharacters(t *testing.T) {
tests := []struct {
name string
password string
want string
}{
{`PW: A1c9.Ba`, `A1c9.Ba`, `ABca19.`},
{`PW: PX4xDoiKrs,[egEAief{`, `PX4xDoiKrs,[egEAief{`, `PXDKEAxoirsegief4,[{`},
{`PW: *Z%C9d+PZYkD7D+{~r'w`, `*Z%C9d+PZYkD7D+{~r'w`, `ZCPZYDDdkrw97*%++{~'`},
{`PW: 4?r2YV:Abo&/z<3tJ*Z{`, `4?r2YV:Abo&/z<3tJ*Z{`, `YVAJZrbozt423?:&/<*{`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
grouped := GroupCharsForMobile(tt.password)
if grouped != tt.want {
t.Errorf("GroupCharsForMobile() failed, expected: %s, got: %s", tt.want, grouped)
}
})
}
}

View file

@ -354,6 +354,9 @@ func (g *Generator) generateRandom() (string, error) {
ok = g.checkMinimumRequirements(password)
}
if g.config.MobileGrouping {
return GroupCharsForMobile(password), nil
}
return password, nil
}

View file

@ -453,6 +453,27 @@ func TestGenerateRandom(t *testing.T) {
}
}
func TestGenerateRandom_withGrouping(t *testing.T) {
config := NewConfig(WithAlgorithm(AlgoRandom), WithMinLength(1),
WithMaxLength(1), WithMobileGrouping())
config.MinNumeric = 1
generator := New(config)
pw, err := generator.generateRandom()
if err != nil {
t.Errorf("generateRandom() failed: %s", err)
}
if len(pw) > 1 {
t.Errorf("expected password with length 1 but got: %d", len(pw))
}
n, err := strconv.Atoi(pw)
if err != nil {
t.Errorf("expected password to be a number but got an error: %s", err)
}
if n < 0 || n > 9 {
t.Errorf("expected password to be a number between 0 and 9, got: %d", n)
}
}
func TestGenerate(t *testing.T) {
tests := []struct {
name string