Public/Join-SecretSharingSecret.ps1
|
function Join-SecretSharingSecret { <# .SYNOPSIS Reconstructs a secret from a quorum of SLIP-0039 mnemonic shares. .DESCRIPTION Accepts share mnemonics (as plain strings, or as objects with a Mnemonic property - the output of Split-SecretSharingSecret works directly), verifies each one's checksum, groups them by GroupIndex, reconstructs each resolvable group's share value (verifying that group's digest once its member threshold is met), then reconstructs the encrypted master secret from enough groups (verifying the group-level digest), and finally decrypts it. .PARAMETER Share The shares to combine: mnemonic strings, or objects with a Mnemonic property (such as Split-SecretSharingSecret's output). Accepts pipeline input. .PARAMETER Passphrase The passphrase used when the shares were created, if any. .EXAMPLE PS> $shares | Join-SecretSharingSecret Reconstructs a secret from a set of Split-SecretSharingSecret output objects. .OUTPUTS System.Security.SecureString .NOTES A wrong -Passphrase cannot be detected here: per SLIP-0039's design, every passphrase decrypts to *some* value of the right length, correct or not, with no way to tell the difference - this is deliberate, and enables plausible deniability. This cmdlet throws a clear terminating error for corrupted, insufficient, or mismatched *shares* (checksum and digest verification both fail loudly), but a wrong passphrase with otherwise-valid shares returns silently with the wrong secret, not an error. #> [CmdletBinding()] [OutputType([System.Security.SecureString])] param( [Parameter(Mandatory, ValueFromPipeline)] [PSObject[]]$Share, [System.Security.SecureString]$Passphrase ) begin { $collectedShare = New-Object System.Collections.Generic.List[PSObject] } process { foreach ($item in $Share) { $collectedShare.Add($item) } } end { if ($collectedShare.Count -eq 0) { throw 'At least one share is required.' } $digestConstant = Get-SecretSharingDigestConstant $decodedShare = @(foreach ($item in $collectedShare) { $mnemonic = $null if ($item -is [string]) { $mnemonic = $item } elseif ($item.PSObject.Properties['Mnemonic']) { $mnemonic = $item.Mnemonic } else { throw 'Each -Share item must be a mnemonic string or an object with a Mnemonic property.' } $wordIndex = @($mnemonic.Trim() -split '\s+' | ForEach-Object { ConvertFrom-SecretSharingMnemonicWord -Word $_ }) $header = ConvertFrom-SecretSharingShareWord -Index $wordIndex[0..($wordIndex.Count - 4)] if (-not (Test-SecretSharingChecksum -Data $wordIndex -Extendable:$header.Extendable)) { throw "Invalid checksum for the share with member index $($header.MemberIndex) in group $($header.GroupIndex). The mnemonic may be mistyped or corrupted." } $header }) $first = $decodedShare[0] foreach ($s in $decodedShare) { if ($s.Identifier -ne $first.Identifier -or $s.Extendable -ne $first.Extendable -or $s.IterationExponent -ne $first.IterationExponent -or $s.GroupThreshold -ne $first.GroupThreshold -or $s.GroupCount -ne $first.GroupCount) { throw 'All shares must belong to the same share set (matching identifier, extendable flag, iteration exponent, group threshold, and group count).' } } $resolvedGroup = New-Object System.Collections.Generic.List[PSObject] foreach ($groupEntry in ($decodedShare | Group-Object -Property GroupIndex)) { $memberOfGroup = @($groupEntry.Group) $memberThreshold = $memberOfGroup[0].MemberThreshold foreach ($m in $memberOfGroup) { if ($m.MemberThreshold -ne $memberThreshold) { throw "Shares in group $($groupEntry.Name) disagree on the member threshold. The shares may be from different sets." } } $distinctMemberIndexCount = @($memberOfGroup | Group-Object -Property MemberIndex).Count if ($distinctMemberIndexCount -lt $memberThreshold) { continue } if ($memberThreshold -eq 1) { $groupShareValue = $memberOfGroup[0].Value } else { $memberPoint = $memberOfGroup | ForEach-Object { [PSCustomObject]@{ X = [byte]$_.MemberIndex; Value = $_.Value } } $candidateGroupShareValue = Invoke-SecretSharingShamirInterpolation -Point $memberPoint -X $digestConstant.SecretIndex $candidateDigestShare = Invoke-SecretSharingShamirInterpolation -Point $memberPoint -X $digestConstant.DigestIndex if (-not (Test-SecretSharingDigestShare -Secret $candidateGroupShareValue -DigestShare $candidateDigestShare)) { throw "Invalid digest for group $($groupEntry.Name). The shares in this group may be corrupted or inconsistent." } $groupShareValue = $candidateGroupShareValue } $resolvedGroup.Add([PSCustomObject]@{ X = [byte]$memberOfGroup[0].GroupIndex; Value = $groupShareValue }) } if ($resolvedGroup.Count -lt $first.GroupThreshold) { throw "Not enough shares to reconstruct the secret. Resolved $($resolvedGroup.Count) of the $($first.GroupThreshold) required groups." } if ($first.GroupThreshold -eq 1) { $encryptedMasterSecret = $resolvedGroup[0].Value } else { $candidateEms = Invoke-SecretSharingShamirInterpolation -Point $resolvedGroup -X $digestConstant.SecretIndex $candidateDigestShare = Invoke-SecretSharingShamirInterpolation -Point $resolvedGroup -X $digestConstant.DigestIndex if (-not (Test-SecretSharingDigestShare -Secret $candidateEms -DigestShare $candidateDigestShare)) { throw 'Invalid digest for the reconstructed secret. The groups combined may be corrupted or inconsistent.' } $encryptedMasterSecret = $candidateEms } $passphraseByte = [byte[]]@() if ($Passphrase) { $passphraseByte = ConvertFrom-SecretSharingPassphraseSecureString -SecureString $Passphrase } $secretByte = Unprotect-SecretSharingMasterSecret -EncryptedMasterSecret $encryptedMasterSecret -Passphrase $passphraseByte ` -Identifier $first.Identifier -IterationExponent $first.IterationExponent -Extendable:$first.Extendable return ConvertTo-SecretSharingSecureString -Byte $secretByte } } |