拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 ShufflePasswordString限制同一类的连续字符

ShufflePasswordString限制同一类的连续字符

白鹭 - 2022-01-24 2152 0 0

因此,在 IT 领域作业,我们几乎一直都需要生成安全密码,某些组织除了所需的字符类数量和长度要求之外,还增加了更严格的要求。我作业的一个这样的组织也限制了可以连续出现的单个字符类(小写、大写、特殊字符、数字)的字符数。我已经构建了一个确实有助于实作这一点的函式,但是,它本质上只是暴力破解密码,这非常糟糕。你会如何从计算机科学的角度来解决这个特定的问题,知道速度是关键,同时保持随机性。

我觉得应该有某种我可以实作的 shuffle 技术,但是我想出的每个解决方案要么太慢,要么降低了字符串的随机性。

Function New-Password {
        PARAM(
            [Int]$PasswordLength            = 64,
            [Int]$MinUpperCase              = 5,
            [Int]$MinLowerCase              = 5,
            [Int]$MinSpecialCharacters      = 5,
            [Int]$MinNumbers                = 5,
            [Int]$ConsecutiveCharClass      = 0,
            [Int]$ConsecutiveCharCheckCount = 1000,
            [String]$LowerCase              = 'abcdefghiklmnoprstuvwxyz',
            [String]$UpperCase              = 'ABCDEFGHKLMNOPRSTUVWXYZ',
            [String]$Numbers                = '1234567890',
            [String]$SpecialCharacters      = '!"$%&/()=?}][{@#* ',
            [String]$PasswordProfile        = '',
        
            #Advanced Options
            [Bool]$EnhancedEntrophy = $True
        )
        
        If ([String]::IsNullOrEmpty($PasswordProfile) -eq $False) {
            #You can define custom password profiles here for easy reference later on.
            New-Variable -Force -Name:'PasswordProfiles' -Value:@{
                'iDrac' = [PSCustomObject]@{PasswordLength=20;SpecialCharacters=" &?>-}|.!(',_[`"@#)*;$]/§%=<:{@";}
            }
        
            If ($PasswordProfile -in $PasswordProfiles.Keys) {
                $PasswordProfiles[$PasswordProfile] |Get-Member -MemberType NoteProperty |ForEach-Object {
                    Set-Variable -Name $_.name -Value $PasswordProfiles[$PasswordProfile].($_.name)
                }
            }
        }
        
        New-Variable -Force -Name:'PassBldr' -Value @{}
        New-Variable -Force -Name:'CharacterClass' -Value:([String]::Empty)
        ForEach ($CharacterClass in @("UpperCase","LowerCase","SpecialCharacters","Numbers")) {
            $Characters = (Get-Variable -Name:$CharacterClass -ValueOnly)
            If ($Characters.Length -gt 0) {
                $PassBldr[$CharacterClass] = [PSCustomObject]@{
                    Min        = (Get-Variable -Name:"min$CharacterClass" -ValueOnly);
                    Characters = $Characters
                    Length     = $Characters.length
                }
            }
        }
        
        #Sanity Check(s)
        $MinimumChars = $MinUpperCase   $MinLowerCase   $MinSpecialCharacters   $MinNumbers
        If ($MinimumChars -gt $PasswordLength) {
            Write-Error -Message:"Specified number of minimum characters ($MinimumChars) is greater than password length ($PasswordLength)."
            Return
        }
        
        #New-Variable -Force -Name:'Random' -Value:(New-Object -TypeName:'System.Random')
        New-Variable -Force -Name:'Randomizer' -Value:$Null
        New-Variable -Force -Name:'Random' -Value:([ScriptBlock]::Create({
            Param([Int]$Max=[Int32]::MaxValue,[Int32]$Min=1)
            if ($Min -gt $Max) {
                Write-Warning  "[$($myinvocation.ScriptLineNumber)] Min ($Min) must be less than Max ($Max)."
                return -1
            }
        
            if ($EnhancedEntrophy) {
                if ($Randomizer -eq $Null) {
                    Set-Variable -Name:'Randomizer' -Value:(New-Object -TypeName:'System.Security.Cryptography.RNGCryptoServiceProvider') -Scope:1
                }
                #initialize everything
                $Difference=$Max-$Min
                [Byte[]] $bytes = 1..4  #4 byte array for int32/uint32
        
                #generate the number
                $Randomizer.getbytes($bytes)
                $Number = [System.BitConverter]::ToUInt32(($bytes),0)
                return ([Int32]($Number % $Difference   $Min))
        
            } Else {
                if ($Randomizer -eq $Null) {
                    Set-Variable -Name:'Randomizer' -Value:(New-Object -TypeName:'System.Random') -Scope:1
                }
                return ([Int]$Randomizer.Next($Min,$Max))
            }
        }))
        
        $GetString = [ScriptBlock]::Create({
            Param([Int]$Length,[String]$Characters)
            Return ([String]$Characters[(1..$Length |ForEach-Object {& $Random $Characters.length})] -replace " ","")
        })
        
        $CreatePassword = [scriptblock]::Create({
            New-Variable -Name Password -Value ([System.Text.StringBuilder]::new()) -Force
        
            #Meet the minimum requirements for each character class
            ForEach ($CharacterClass in $PassBldr.Values) {
                If ($CharacterClass.Min -gt 0) {
                    $Null = $Password.Append([string](Invoke-Command $GetString -ArgumentList $CharacterClass.Min,$CharacterClass.Characters))
                }
            }
        
            #Now meet the minimum length requirements.
            If ([Int]($PasswordLength-$Password.length) -gt 0) {
                $Null = $Password.Append((Invoke-Command $GetString -ArgumentList ($PasswordLength-$Password.length),($PassBldr.Values.Characters -join "")))
            }
        
            return (([Char[]]$Password.ToString() | Get-Random -Count $Password.Length) -join "")
        })
        
        Switch ([Int]$ConsecutiveCharClass) {
            '0' { New-Variable -Name NewPassword -Value (& $CreatePassword) -Force }
            {$_ -gt 0} {
                New-Variable -Name CheckPass    -Value $False -Force
                New-Variable -Name CheckCount   -Value ([Int]0) -Force
                For ($I=0; $I -le $ConsecutiveCharCheckCount -and $CheckPass -eq $False; $I  ) {
                    New-Variable -Name NewPassword -Value (& $CreatePassword) -Force
                    $TestPassed = 0
                    ForEach ($CharClass in $PassBldr.Values) {                   
                        IF ([Regex]::IsMatch([Regex]::Escape($NewPassword),"[$([Regex]::Escape($CharClass.Characters))]{$ConsecutiveCharClass}") -eq $False) {
                            $TestPassed  
                        }
                    }
                    if ($TestPassed -eq $CheckClasses.Count) {
                        $CheckPass = $True
                    }
                }
            }
            Default {Write-Warning -Message "This shouldn't be possible, how did you get here?!"}
        }
        
        Return $NewPassword
    }

uj5u.com热心网友回复:

在进一步讨论之前,我应该注意这些属性(符合所描述的策略与保持随机性/熵)是相互排斥的 - 您不能通过仔细“纠正”来自 PRNG 的输出分布来“保持随机性”。

我会将问题分成两个独立的功能:

  • Test-PasswordCharSequence - 快速验证给定的密码字符串是否符合策略
  • Shuffle-PasswordCharSequence - 随机打乱任何给定密码中的字符一次

原子化这些核心操作应该更容易调整/重构。

对于验证函式,使用正则表达式可能很诱人——但我建议简单地遍历字符串并跟踪同一类的连续字符。

function Test-PasswordCharSequence {
    param(
        [string]
        $String,

        [System.Collections.IDictionary]
        $CharacterMap,

        [int]$Limit = 5
    )

    # Keep tracking the last seen character class and length of consecutive sequence
    $currentClass = ""
    $counter = 0

    foreach($char in $String.ToCharArray())
    {
        if($CharacterMap.ContainsKey($char) -and $CharacterMap[$char] -eq $currentClass)
        {
            $counter  
        }
        else
        {
            $counter = 1
            $currentClass = $CharacterMap[$char]
        }

        # if we've seen the same class for too many consecutive characters, fail
        if($counter -gt $Limit){
            return $false
        }
    }

    # No sequence over limit observed
    return $true
}

然后我们需要一个函式来洗牌密码。我所知道的最有效的真正随机(同样取决于所使用的 RNG)混洗算法是就地 Fisher-Yates 混洗算法,可以按如下方式实作:

function Shuffle-PasswordCharSequence
{
    param(
        [Parameter(Mandatory)]
        [string]$String
    )

    $chars = $String.ToCharArray()

    $max = $chars.Length

    #Fisher-Yates Left to Right
    for($i = 0; $i -lt $max - 1; $i  )
    {
        $j = Get-Random -Minimum 0 -Maximum ($max - $i)
        $chars[$j],$chars[$i $j] = $chars[$i $j],$chars[$j]
    }

    return [string]::new($chars)
}

要将这些与您现有的New-Password功能结合使用

# define character classes to use
$CharacterClasses = @{
    LowerCase = 'abcdefghiklmnoprstuvwxyz'
    UpperCase = 'ABCDEFGHKLMNOPRSTUVWXYZ'
    Numbers = '1234567890'
    SpecialCharacters = '!"$%&/()=?}][{@#* '
}

# generate inverse character map for the validation function
# we use [Dictionary[char,string]] rather than [hashtable] to ensure case-sensitive handling of keys ('b' vs 'B')
$classMap = [System.Collections.Generic.Dictionary[char,string]]::new()

foreach($entry in $CharacterClasses.GetEnumerator())
{
    foreach($char in $entry.Value.ToCharArray())
    {
        $classMap[$char] = $entry.Name
    }
}

# generate initial password
$passwordCandidate = New-Password -PasswordLength 127 @CharacterClasses

# validate generated password, shuffle until successful
$shuffleCount = 0
while(!(Test-PasswordCharSequence $passwordCandidate -CharacterMap $classMap)){
  $passwordCandidate = Shuffle-PasswordCharSequence $passwordCandidate
  $shuffleCount  
}

Write-Host "Generated valid password after ${shuffleCount} shuffles"

uj5u.com热心网友回复:

我认为@vonPryz在他的评论中提到的“为什么不逐个字符地构建密码? ”实际上是可能的,并且可能是最快的方法。
关键是您必须分两个阶段创建密码,首先使用字符(而不是最终字符构建复杂性串列,然后在下一阶段为该位置的字符集选择相关字符。如果$MaxConsecutiveChar达到,则从该位置的字符集中选择一个新字符:

Function New-Password {
    Param(
        [Int]$PasswordLength            = 64,
        [Int]$MinUpperCase              = 5,
        [Int]$MinLowerCase              = 5,
        [Int]$MinSpecialCharacters      = 5,
        [Int]$MinNumbers                = 5,
        [Int]$MaxConsecutiveChar        = 3,
        [String]$LowerCase              = 'abcdefghiklmnoprstuvwxyz',
        [String]$UpperCase              = 'ABCDEFGHKLMNOPRSTUVWXYZ',
        [String]$Numbers                = '1234567890',
        [String]$SpecialCharacters      = '!"$%&/()=?}][{@#* '
    )

    enum CharSet {
        LowerCase
        UpperCase
        SpecialCharacters
        Numbers
    }

    $CharSets    = [system.collections.generic.dictionary[CharSet, Char[]]]::new()
    $MinSetChars = [system.collections.generic.dictionary[CharSet, Int]]::new()
    
    $MinimumChars = 0
    [CharSet].GetEnumNames().ForEach{
        $CharSets[$_] = (Get-Variable -ValueOnly -Name $_).ToCharArray()
        $MinChar = [Int](Get-Variable -ValueOnly -Name "Min$_")
        $MinSetChars[$_] = $MinChar
        $MinimumChars  = $MinChar
    }

    If ($MinimumChars -gt $PasswordLength) {
        Throw "Specified number of minimum characters ($MinimumChars) is greater than password length ($PasswordLength)."
    }

    # Build a list of characters sets
    $SetList = for ($i = 0; $i -lt $PasswordLength; $i  ) { $CharSets.Keys |Get-Random }
 
    # Insert the Min* required characters for the specific sets
    # Making sure that the position is not already taken by another Min* characterset
    $Used = [System.Collections.Generic.HashSet[int]]::New()
    $CharSets.Keys.ForEach{
        for ($i = 0; $i -lt $MinSetChars[$_]) {
            $At = Get-Random $PasswordLength
            if (!$Used.Contains[$At]) {
                $SetList[$At] = $_
                $Null = $Used.Add($At)
                $i  
            }
        }
    }
    
    # Elect a character for each set
    $LastChar = $Null
    $ConsecutiveChars = 1
    -Join $SetList.ForEach{
        $Char = $CharSets[$_] |Get-Random
        # Check the consecutive characters (choose another when required)
        While ($ConsecutiveChars -ge $MaxConsecutiveChar -and $Char -eq $LastChar) { $Char = $CharSets[$_] |Get-Random }
        $ConsecutiveChars = if ($Char -eq $LastChar) { $ConsecutiveChars   1 } else { 1 }
        $LastChar = $Char
        $Char
    }
}

New-Password
X23@[X0C5%FL3Demyf5?5})f]5Kt#usC#m1 3?T(NOb4DmYsX8FA3pF46OUZeW3V

为了证明-MaxConsecutiveChar有效并且安静快速:

$Params = @{
    MaxConsecutiveChar     = 1
    LowerCase              = 'ab'
    UpperCase              = 'ab'
    Numbers                = 'ab'
    SpecialCharacters      = 'ab'
}
New-Password @Params
babababababababababababababababababababababababababababababababa
标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *