New updates
This commit is contained in:
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,5 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-10-10
|
||||
|
||||
### Test-WeakADPasswords.ps1 v1.3.0
|
||||
Added:
|
||||
- `CheckOnlyEnabledUsers` flag wired from settings to filter accounts prior to `Test-PasswordQuality`.
|
||||
- Transcript logging to `Reports/logs/test-weakad-<timestamp>.log`.
|
||||
|
||||
### Extract-NTHashes.ps1 v1.2.0
|
||||
Added:
|
||||
- Transcript logging to `Reports/logs/extract-hashes-<timestamp>.log`.
|
||||
|
||||
### Elysium.ps1 v1.1.0
|
||||
Updated:
|
||||
- Added strict error handling (`$ErrorActionPreference='Stop'`) and `Set-StrictMode`.
|
||||
- Resolved script invocations via `$PSScriptRoot` to avoid CWD issues.
|
||||
|
||||
### Update-KHDB.ps1 v1.1.0
|
||||
Added/Updated:
|
||||
- Robust settings validation and SAS token normalization.
|
||||
- Safe URL construction with `UriBuilder` and custom User-Agent.
|
||||
- TLS 1.2 enforced; `HttpClient` timeout and retry with backoff for transient errors.
|
||||
- Download progress for both known and unknown content length.
|
||||
- Atomic-ish update: download to temp, extract, validate, backup existing `khdb.txt`, then replace.
|
||||
- KHDB validation: format check (32-hex), deduplication and normalization.
|
||||
- Transcript logging to `Reports/logs/update-khdb-<timestamp>.log`.
|
||||
|
||||
### Test-WeakADPasswords.ps1 v1.2.0
|
||||
Updated:
|
||||
- Enforced modules via `#Requires`; removed runtime installs.
|
||||
- Added strict mode and error preference.
|
||||
- Resolved paths relative to `$PSScriptRoot` (settings, KHDB, reports).
|
||||
- Ensured report directory creation and sane defaults (`Reports`).
|
||||
- Removed stray top-level loop; UPN enrichment occurs during report generation only.
|
||||
|
||||
### Extract-NTHashes.ps1 v1.1.0
|
||||
Updated:
|
||||
- Enforced modules via `#Requires`; added strict mode.
|
||||
- Fixed variable ordering bug and unified filename scheme with domain prefix.
|
||||
- Implemented PBKDF2 (HMAC-SHA256, 100k iterations) + random salt for AES-256-CBC encryption; header `ELY1|salt|iv`.
|
||||
- Normalized SAS token and verified container existence; checksum verified before cleanup; artifacts retained on failure.
|
||||
- Paths resolved relative to `$PSScriptRoot`; ensured report base directory exists.
|
||||
|
||||
### ElysiumSettings.txt.sample v1.1.0
|
||||
Updated:
|
||||
- `ReportPathBase` default changed to `Reports` (relative) and added guidance on required modules and replication rights.
|
||||
- Added optional `CheckOnlyEnabledUsers=true` example flag.
|
||||
|
||||
## Extract-NTHashes.ps1
|
||||
|
||||
### version 1.1.1
|
||||
|
16
Elysium.ps1
16
Elysium.ps1
@@ -7,7 +7,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Elysium.ps1 ##
|
||||
## Version: 1.0 ##
|
||||
## Version: 1.1.0 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
@@ -24,8 +24,12 @@ Elysium.ps1 offers a menu to perform various actions:
|
||||
5. Exit
|
||||
#>
|
||||
|
||||
# Safer defaults
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
# Define the path to the settings file
|
||||
$settingsFilePath = "ElysiumSettings.txt"
|
||||
$settingsFilePath = Join-Path -Path $PSScriptRoot -ChildPath "ElysiumSettings.txt"
|
||||
|
||||
# Check if the settings file exists
|
||||
if (-Not (Test-Path $settingsFilePath)) {
|
||||
@@ -69,19 +73,19 @@ do {
|
||||
switch ($userSelection) {
|
||||
'1' {
|
||||
Write-Host "Downloading KHDB..."
|
||||
.\Update-KHDB.ps1
|
||||
& (Join-Path -Path $PSScriptRoot -ChildPath 'Update-KHDB.ps1')
|
||||
}
|
||||
'2' {
|
||||
Write-Host "Testing Weak AD Passwords..."
|
||||
.\Test-WeakADPasswords.ps1
|
||||
& (Join-Path -Path $PSScriptRoot -ChildPath 'Test-WeakADPasswords.ps1')
|
||||
}
|
||||
'3' {
|
||||
Write-Host "Extracting and Sending Current Hashes..."
|
||||
.\Extract-NTHashes.ps1
|
||||
& (Join-Path -Path $PSScriptRoot -ChildPath 'Extract-NTHashes.ps1')
|
||||
}
|
||||
'4' {
|
||||
Write-Host "Uninstalling..."
|
||||
.\Uninstall.ps1
|
||||
& (Join-Path -Path $PSScriptRoot -ChildPath 'Uninstall.ps1')
|
||||
}
|
||||
'5' {
|
||||
Write-Host "Exiting..."
|
||||
|
@@ -8,7 +8,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: ElysiumSettings.txt ##
|
||||
## Version: 1.0.0 ##
|
||||
## Version: 1.1.0 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
@@ -21,9 +21,14 @@ sasToken =
|
||||
# Application Settings
|
||||
######################
|
||||
InstallationPath=
|
||||
ReportPathBase=/Reports
|
||||
ReportPathBase=Reports
|
||||
WeakPasswordsDatabase=khdb.txt
|
||||
# TODO CheckOnlyEnabledUsers=true
|
||||
# CheckOnlyEnabledUsers=true
|
||||
|
||||
# Notes:
|
||||
# - Required PowerShell modules: DSInternals, ActiveDirectory, Az.Storage (for upload).
|
||||
# - AD account permissions: Replication Directory Changes and Replication Directory Changes All
|
||||
# on the domain (DCSync-equivalent) are sufficient; full Domain Admin not required.
|
||||
|
||||
# Domain Settings
|
||||
#################
|
||||
|
@@ -7,11 +7,12 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Extract-NTLMHashes.ps1 ##
|
||||
## Version: 1.0.2 ##
|
||||
## Version: 1.2.0 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
<#
|
||||
#Requires -Modules DSInternals, Az.Storage
|
||||
.SYNOPSIS
|
||||
Script for extracting NTLM hashes from live AD for further analysis.
|
||||
|
||||
@@ -19,10 +20,31 @@ Script for extracting NTLM hashes from live AD for further analysis.
|
||||
This script will connect to selected domain (defined in ElysiumSettings.txt) using account with AD replication privileges and extract NTLM hashes from all active accounts. It will then compress and encrypt the resulting file, uploads it to designated Azure Storage account, checks for validity and then deletes everything. The hashes are extracted without usernames to minimise the sensitivity of the operation. Encryption is done with AES and passphrase that was defined in environment variable during first run.
|
||||
#>
|
||||
|
||||
# Import settings
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
$scriptRoot = $PSScriptRoot
|
||||
|
||||
function Start-ExtractTranscript {
|
||||
param([string]$BasePath)
|
||||
try {
|
||||
$logsDir = Join-Path -Path $BasePath -ChildPath 'Reports/logs'
|
||||
if (-not (Test-Path $logsDir)) { New-Item -Path $logsDir -ItemType Directory -Force | Out-Null }
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$logPath = Join-Path -Path $logsDir -ChildPath "extract-hashes-$ts.log"
|
||||
Start-Transcript -Path $logPath -Force | Out-Null
|
||||
} catch {
|
||||
Write-Warning "Could not start transcript: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-ExtractTranscript { try { Stop-Transcript | Out-Null } catch {} }
|
||||
|
||||
Start-ExtractTranscript -BasePath $scriptRoot
|
||||
try {
|
||||
# Import settings
|
||||
Write-Host "Loading settings..."
|
||||
$ElysiumSettings = @{}
|
||||
$settingsPath = "ElysiumSettings.txt"
|
||||
$settingsPath = Join-Path -Path $scriptRoot -ChildPath "ElysiumSettings.txt"
|
||||
|
||||
if (-not (Test-Path $settingsPath)) {
|
||||
Write-Error "Settings file not found at $settingsPath"
|
||||
@@ -38,17 +60,12 @@ Get-Content $settingsPath | ForEach-Object {
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure DSInternals and Az.Storage are installed
|
||||
$requiredModules = @('DSInternals', 'Az.Storage')
|
||||
foreach ($module in $requiredModules) {
|
||||
if (-not (Get-Module -ListAvailable -Name $module)) {
|
||||
Write-Host "Installing $module module..."
|
||||
Install-Module $module -Scope CurrentUser -Force
|
||||
}
|
||||
Import-Module $module
|
||||
function Normalize-ReportPath([string]$p) {
|
||||
if ([string]::IsNullOrWhiteSpace($p)) { return (Join-Path -Path $scriptRoot -ChildPath 'Reports') }
|
||||
if ([System.IO.Path]::IsPathRooted($p)) { return $p }
|
||||
return (Join-Path -Path $scriptRoot -ChildPath $p)
|
||||
}
|
||||
|
||||
# Script variables
|
||||
# External settings
|
||||
$storageAccountName = $ElysiumSettings['storageAccountName']
|
||||
$containerName = $ElysiumSettings['containerName']
|
||||
@@ -56,14 +73,10 @@ $sasToken = $ElysiumSettings['sasToken']
|
||||
|
||||
# Retrieve the passphrase from a user environment variable
|
||||
$passphrase = [System.Environment]::GetEnvironmentVariable("ELYSIUM_PASSPHRASE", [System.EnvironmentVariableTarget]::User)
|
||||
if ([string]::IsNullOrWhiteSpace($passphrase)) { Write-Error 'Passphrase not found in ELYSIUM_PASSPHRASE environment variable.'; exit }
|
||||
|
||||
# Dynamic variables
|
||||
# Timestamp
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$domainPrefix = $selectedDomain.Name -replace "\W", "_" # Replace non-alphanumeric characters to ensure a valid file name
|
||||
$exportPath = ".\${domainPrefix}_NTLM_Hashes_$timestamp.txt"
|
||||
$compressedFilePath = ".\${domainPrefix}_NTLM_Hashes_$timestamp.zip"
|
||||
$encryptedFilePath = ".\${domainPrefix}_NTLM_Hashes_$timestamp.enc"
|
||||
$blobName = "${domainPrefix}_NTLM_Hashes_$timestamp.enc"
|
||||
|
||||
function Protect-FileWithAES {
|
||||
param (
|
||||
@@ -77,41 +90,49 @@ function Protect-FileWithAES {
|
||||
[string]$Passphrase
|
||||
)
|
||||
|
||||
$aes = New-Object System.Security.Cryptography.AesManaged
|
||||
# Derive key with PBKDF2 (HMACSHA256) + random salt
|
||||
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
||||
$salt = New-Object byte[] 16
|
||||
$rng.GetBytes($salt)
|
||||
|
||||
$kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Passphrase, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA256)
|
||||
$key = $kdf.GetBytes(32)
|
||||
|
||||
$aes = [System.Security.Cryptography.Aes]::Create()
|
||||
$aes.KeySize = 256
|
||||
$aes.BlockSize = 128
|
||||
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
|
||||
|
||||
# Generate key from passphrase
|
||||
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($Passphrase)
|
||||
$aes.Key = [System.Security.Cryptography.SHA256]::Create().ComputeHash($passwordBytes)
|
||||
|
||||
# Generate a random IV
|
||||
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
|
||||
$aes.GenerateIV()
|
||||
|
||||
$encryptor = $aes.CreateEncryptor($aes.Key, $aes.IV)
|
||||
$iv = $aes.IV
|
||||
$encryptor = $aes.CreateEncryptor($key, $iv)
|
||||
|
||||
$fileStream = [System.IO.File]::Open($InputFile, [System.IO.FileMode]::Open)
|
||||
$fileStream = [System.IO.File]::Open($InputFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
|
||||
$outFileStream = [System.IO.File]::Create($OutputFile)
|
||||
# Write the IV at the beginning of the output file
|
||||
$outFileStream.Write($aes.IV, 0, $aes.IV.Length)
|
||||
|
||||
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($outFileStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
|
||||
|
||||
try {
|
||||
$buffer = New-Object Byte[] 8192
|
||||
while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
||||
$cryptoStream.Write($buffer, 0, $read)
|
||||
# File header: magic 'ELY1' (4 bytes), salt (16 bytes), IV (16 bytes)
|
||||
$magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1')
|
||||
$outFileStream.Write($magic, 0, $magic.Length)
|
||||
$outFileStream.Write($salt, 0, $salt.Length)
|
||||
$outFileStream.Write($iv, 0, $iv.Length)
|
||||
|
||||
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($outFileStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
|
||||
try {
|
||||
$buffer = New-Object Byte[] 8192
|
||||
while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
||||
$cryptoStream.Write($buffer, 0, $read)
|
||||
}
|
||||
} finally {
|
||||
$cryptoStream.FlushFinalBlock()
|
||||
$cryptoStream.Close()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$cryptoStream.Close()
|
||||
$outFileStream.Close()
|
||||
$fileStream.Close()
|
||||
$aes.Clear()
|
||||
} finally {
|
||||
$outFileStream.Close(); $fileStream.Close(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose()
|
||||
}
|
||||
|
||||
Write-Host "File has been encrypted: $OutputFile"
|
||||
Write-Host "File has been encrypted (PBKDF2+AES-256-CBC): $OutputFile"
|
||||
}
|
||||
function Get-FileChecksum {
|
||||
param (
|
||||
@@ -131,8 +152,8 @@ function Get-FileChecksum {
|
||||
}
|
||||
|
||||
# Extract NTLM hashes
|
||||
$exportPath = ".\NTLM_Hashes_$timestamp.txt"
|
||||
$compressedFilePath = ".\NTLM_Hashes_$timestamp.zip"
|
||||
$reportBase = Normalize-ReportPath -p $ElysiumSettings['ReportPathBase']
|
||||
if (-not (Test-Path $reportBase)) { New-Item -Path $reportBase -ItemType Directory -Force | Out-Null }
|
||||
|
||||
# Build domain details from settings
|
||||
$DomainDetails = @{}
|
||||
@@ -159,6 +180,13 @@ if (-not $selectedDomain) {
|
||||
$domainController = $selectedDomain.DC
|
||||
$credential = Get-Credential -Message "Enter AD credentials with replication rights for $($selectedDomain.Name)"
|
||||
|
||||
$domainPrefix = ($selectedDomain.Name -replace "\W", "_")
|
||||
$baseName = "${domainPrefix}_NTLM_Hashes_$timestamp"
|
||||
$exportPath = Join-Path -Path $scriptRoot -ChildPath "$baseName.txt"
|
||||
$compressedFilePath = Join-Path -Path $scriptRoot -ChildPath "$baseName.zip"
|
||||
$encryptedFilePath = Join-Path -Path $scriptRoot -ChildPath "$baseName.enc"
|
||||
$blobName = "$baseName.enc"
|
||||
|
||||
$ntlmHashes = Get-ADReplAccount -All -Server $domainController -Credential $credential |
|
||||
Where-Object { $_.NTHash } |
|
||||
ForEach-Object { [BitConverter]::ToString($_.NTHash).Replace("-", "") } |
|
||||
@@ -179,15 +207,22 @@ Write-Host "File has been encrypted: $encryptedFilePath"
|
||||
$localFileChecksum = Get-FileChecksum -Path $encryptedFilePath
|
||||
|
||||
# Create the context for Azure Blob Storage with SAS token
|
||||
$storageContext = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken "$sasToken"
|
||||
$sas = $sasToken
|
||||
if ([string]::IsNullOrWhiteSpace($sas)) { Write-Error 'sasToken is missing in settings.'; exit }
|
||||
$sas = $sas.Trim(); if (-not $sas.StartsWith('?')) { $sas = '?' + $sas }
|
||||
$storageContext = New-AzStorageContext -StorageAccountName $storageAccountName -SasToken $sas
|
||||
|
||||
# Ensure container exists
|
||||
$container = Get-AzStorageContainer -Name $containerName -Context $storageContext -ErrorAction SilentlyContinue
|
||||
if (-not $container) { Write-Error "Azure container '$containerName' not found or access denied."; exit }
|
||||
|
||||
# Upload the encrypted file to Azure Blob Storage
|
||||
Set-AzStorageBlobContent -File $encryptedFilePath -Container $containerName -Blob $blobName -Context $storageContext
|
||||
Set-AzStorageBlobContent -File $encryptedFilePath -Container $containerName -Blob $blobName -Context $storageContext | Out-Null
|
||||
Write-Host "Encrypted file uploaded to Azure Blob Storage: $blobName"
|
||||
|
||||
# Download the blob to a temporary location to verify
|
||||
$tempDownloadPath = [System.IO.Path]::GetTempFileName()
|
||||
Get-AzStorageBlobContent -Blob $blobName -Container $containerName -Context $storageContext -Destination $tempDownloadPath -Force
|
||||
Get-AzStorageBlobContent -Blob $blobName -Container $containerName -Context $storageContext -Destination $tempDownloadPath -Force | Out-Null
|
||||
|
||||
# Calculate the downloaded file checksum
|
||||
$downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
|
||||
@@ -195,13 +230,16 @@ $downloadedFileChecksum = Get-FileChecksum -Path $tempDownloadPath
|
||||
# Compare the checksums
|
||||
if ($localFileChecksum -eq $downloadedFileChecksum) {
|
||||
Write-Host "The file was correctly uploaded. Checksum verified."
|
||||
# Clean up local and temporary files only on success
|
||||
Remove-Item -Path $exportPath, $compressedFilePath, $encryptedFilePath, $tempDownloadPath -Force
|
||||
Write-Host "Local and temporary files cleaned up after uploading to Azure Blob Storage."
|
||||
}
|
||||
else {
|
||||
Write-Host "Checksum verification failed. The uploaded file may be corrupted."
|
||||
Write-Warning "Checksum verification failed. Keeping local artifacts for investigation: $exportPath, $compressedFilePath, $encryptedFilePath"
|
||||
if (Test-Path $tempDownloadPath) { Remove-Item -Path $tempDownloadPath -Force }
|
||||
}
|
||||
|
||||
# Clean up local and temporary files
|
||||
Remove-Item -Path $exportPath, $compressedFilePath, $encryptedFilePath, $tempDownloadPath -Force
|
||||
Write-Host "Local and temporary files cleaned up after uploading to Azure Blob Storage."
|
||||
|
||||
Write-Host "Script execution completed."
|
||||
Write-Host "Script execution completed."
|
||||
} finally {
|
||||
Stop-ExtractTranscript
|
||||
}
|
||||
|
@@ -8,11 +8,12 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Test-WeakADPasswords.ps1 ##
|
||||
## Version: 1.1.1 ##
|
||||
## Version: 1.3.0 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
<#
|
||||
#Requires -Modules DSInternals, ActiveDirectory
|
||||
.SYNOPSIS
|
||||
Weak AD password finder component of Elysium tool.
|
||||
|
||||
@@ -21,8 +22,27 @@ This script will test the passwords of selected domain (defined in ElysiumSettin
|
||||
#>
|
||||
|
||||
# Enable verbose output
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
$VerbosePreference = "Continue"
|
||||
|
||||
$scriptRoot = $PSScriptRoot
|
||||
|
||||
function Start-TestTranscript {
|
||||
param([string]$BasePath)
|
||||
try {
|
||||
$logsDir = Join-Path -Path $BasePath -ChildPath 'Reports/logs'
|
||||
if (-not (Test-Path $logsDir)) { New-Item -Path $logsDir -ItemType Directory -Force | Out-Null }
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$logPath = Join-Path -Path $logsDir -ChildPath "test-weakad-$ts.log"
|
||||
Start-Transcript -Path $logPath -Force | Out-Null
|
||||
} catch {
|
||||
Write-Warning "Could not start transcript: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-TestTranscript { try { Stop-Transcript | Out-Null } catch {} }
|
||||
|
||||
# Current timestamp for both report generation and header
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
|
||||
@@ -35,32 +55,34 @@ Report Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
||||
"@
|
||||
$footer = "`r`n==== End of Report ===="
|
||||
|
||||
# Import settings
|
||||
Write-Verbose "Loading settings..."
|
||||
$ElysiumSettings = @{}
|
||||
$settingsPath = "ElysiumSettings.txt"
|
||||
|
||||
# Ensure the settings file exists
|
||||
if (-not (Test-Path $settingsPath)) {
|
||||
Write-Error "Settings file not found at $settingsPath"
|
||||
exit
|
||||
}
|
||||
|
||||
# Load settings from file
|
||||
Start-TestTranscript -BasePath $scriptRoot
|
||||
try {
|
||||
Get-Content $settingsPath | ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace($_) -and -not $_.StartsWith("#")) {
|
||||
$keyValue = $_ -split '=', 2
|
||||
if ($keyValue.Count -eq 2) {
|
||||
$ElysiumSettings[$keyValue[0].Trim()] = $keyValue[1].Trim()
|
||||
# Import settings
|
||||
Write-Verbose "Loading settings..."
|
||||
$ElysiumSettings = @{}
|
||||
$settingsPath = Join-Path -Path $scriptRoot -ChildPath "ElysiumSettings.txt"
|
||||
|
||||
# Ensure the settings file exists
|
||||
if (-not (Test-Path $settingsPath)) {
|
||||
Write-Error "Settings file not found at $settingsPath"
|
||||
exit
|
||||
}
|
||||
|
||||
# Load settings from file
|
||||
try {
|
||||
Get-Content $settingsPath | ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace($_) -and -not $_.StartsWith("#")) {
|
||||
$keyValue = $_ -split '=', 2
|
||||
if ($keyValue.Count -eq 2) {
|
||||
$ElysiumSettings[$keyValue[0].Trim()] = $keyValue[1].Trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Verbose "Settings loaded successfully."
|
||||
} catch {
|
||||
Write-Error ("An error occurred while loading settings: {0}" -f $_.Exception.Message)
|
||||
exit
|
||||
}
|
||||
Write-Verbose "Settings loaded successfully."
|
||||
} catch {
|
||||
Write-Error ("An error occurred while loading settings: {0}" -f $_.Exception.Message)
|
||||
exit
|
||||
}
|
||||
|
||||
# Define the function to extract domain details from settings
|
||||
function Get-DomainDetailsFromSettings {
|
||||
@@ -91,44 +113,28 @@ function Get-DomainDetailsFromSettings {
|
||||
$domainDetails = Get-DomainDetailsFromSettings -Settings $ElysiumSettings
|
||||
Write-Verbose ("Domain details extracted: {0}" -f ($domainDetails | ConvertTo-Json))
|
||||
|
||||
# Required modules
|
||||
$requiredModules = @("DSInternals", "ActiveDirectory")
|
||||
# Modules are required via #Requires; PowerShell will stop early if missing.
|
||||
|
||||
# Check each required module and import
|
||||
foreach ($module in $requiredModules) {
|
||||
if (-not (Get-Module -ListAvailable -Name $module)) {
|
||||
Write-Verbose "Required module '$module' is not installed."
|
||||
$response = Read-Host "Would you like to install it? (Y/N)"
|
||||
if ($response -eq 'Y') {
|
||||
try {
|
||||
Install-Module -Name $module -Force -ErrorAction Stop
|
||||
Write-Verbose "Module '$module' installed successfully."
|
||||
} catch {
|
||||
Write-Error ("Failed to install module '{0}': {1}" -f $module, $_.Exception.Message)
|
||||
exit
|
||||
}
|
||||
} else {
|
||||
Write-Error "Required module '$module' is not installed. Please install it to proceed."
|
||||
exit
|
||||
}
|
||||
}
|
||||
Import-Module $module
|
||||
Write-Verbose "Module '$module' imported."
|
||||
}
|
||||
# Resolve KHDB path with fallbacks
|
||||
$installationPath = $ElysiumSettings["InstallationPath"]
|
||||
if ([string]::IsNullOrWhiteSpace($installationPath)) { $installationPath = $scriptRoot }
|
||||
elseif (-not [System.IO.Path]::IsPathRooted($installationPath)) { $installationPath = Join-Path -Path $scriptRoot -ChildPath $installationPath }
|
||||
|
||||
# Verify the existence of the Weak Password Hashes file
|
||||
$WeakHashesSortedFilePath = Join-Path -Path $ElysiumSettings["InstallationPath"] -ChildPath $ElysiumSettings["WeakPasswordsDatabase"]
|
||||
$khdbName = if ([string]::IsNullOrWhiteSpace($ElysiumSettings["WeakPasswordsDatabase"])) { 'khdb.txt' } else { $ElysiumSettings["WeakPasswordsDatabase"] }
|
||||
$WeakHashesSortedFilePath = Join-Path -Path $installationPath -ChildPath $khdbName
|
||||
if (-not (Test-Path $WeakHashesSortedFilePath)) {
|
||||
Write-Error "Weak password hashes file not found at '$WeakHashesSortedFilePath'."
|
||||
exit
|
||||
}
|
||||
Write-Verbose "Weak password hashes file found at '$WeakHashesSortedFilePath'."
|
||||
|
||||
# Ensure the report directory exists
|
||||
# Ensure the report directory exists (relative paths resolved against script root)
|
||||
$reportPathBase = $ElysiumSettings["ReportPathBase"]
|
||||
if ([string]::IsNullOrWhiteSpace($reportPathBase)) { $reportPathBase = 'Reports' }
|
||||
if (-not [System.IO.Path]::IsPathRooted($reportPathBase)) { $reportPathBase = Join-Path -Path $scriptRoot -ChildPath $reportPathBase }
|
||||
if (-not (Test-Path -Path $reportPathBase)) {
|
||||
try {
|
||||
New-Item -Path $reportPathBase -ItemType Directory -ErrorAction Stop
|
||||
New-Item -Path $reportPathBase -ItemType Directory -ErrorAction Stop | Out-Null
|
||||
Write-Verbose "Report directory created at '$reportPathBase'."
|
||||
} catch {
|
||||
Write-Error ("Failed to create report directory: {0}" -f $_.Exception.Message)
|
||||
@@ -136,6 +142,12 @@ if (-not (Test-Path -Path $reportPathBase)) {
|
||||
}
|
||||
}
|
||||
|
||||
# Read filtering flag (defaults to false)
|
||||
$checkOnlyEnabledUsers = $false
|
||||
if ($ElysiumSettings.ContainsKey('CheckOnlyEnabledUsers')) {
|
||||
try { $checkOnlyEnabledUsers = [System.Convert]::ToBoolean($ElysiumSettings['CheckOnlyEnabledUsers']) } catch { $checkOnlyEnabledUsers = $false }
|
||||
}
|
||||
|
||||
# Function to get UPN for a given SAM account name
|
||||
function Get-UserUPN {
|
||||
param (
|
||||
@@ -158,25 +170,14 @@ function Get-UserUPN {
|
||||
}
|
||||
}
|
||||
|
||||
# Inside the foreach loop where accounts are processed:
|
||||
|
||||
foreach ($line in $lines) {
|
||||
$newReportContent += $line
|
||||
|
||||
# Regex to match the SAMAccountName from the report line
|
||||
if ($line -match "^\s*(\S+)\s*$") {
|
||||
$samAccountName = $matches[1]
|
||||
Write-Verbose "Looking up UPN for $samAccountName"
|
||||
$upn = Get-UserUPN -SamAccountName $samAccountName -Domain $selectedDomain.DC -Credential $credential
|
||||
$newReportContent += " UPN: $upn"
|
||||
}
|
||||
}
|
||||
# (removed stray top-level loop; UPN enrichment happens during report generation below)
|
||||
|
||||
# Function to test for weak AD passwords
|
||||
function Test-WeakADPasswords {
|
||||
param (
|
||||
[hashtable]$DomainDetails,
|
||||
[string]$FilePath
|
||||
[string]$FilePath,
|
||||
[bool]$CheckOnlyEnabledUsers = $false
|
||||
)
|
||||
|
||||
# User selects a domain
|
||||
@@ -198,8 +199,15 @@ function Test-WeakADPasswords {
|
||||
# Performing the test
|
||||
Write-Verbose "Testing password quality for $($selectedDomain.Name)..."
|
||||
try {
|
||||
$testResults = Get-ADReplAccount -All -Server $selectedDomain["DC"] -Credential $credential |
|
||||
Test-PasswordQuality -WeakPasswordHashesFile $FilePath
|
||||
$accounts = Get-ADReplAccount -All -Server $selectedDomain["DC"] -Credential $credential
|
||||
if ($CheckOnlyEnabledUsers) {
|
||||
Write-Verbose "Filtering to only enabled users per settings."
|
||||
# Prefer property 'Enabled' if present, fall back gracefully if not
|
||||
$accounts = $accounts | Where-Object {
|
||||
if ($_.PSObject.Properties.Name -contains 'Enabled') { $_.Enabled } else { $true }
|
||||
}
|
||||
}
|
||||
$testResults = $accounts | Test-PasswordQuality -WeakPasswordHashesFile $FilePath
|
||||
Write-Verbose "Password quality test completed."
|
||||
} catch {
|
||||
Write-Error ("An error occurred while testing passwords: {0}" -f $_.Exception.Message)
|
||||
@@ -262,11 +270,12 @@ function Test-WeakADPasswords {
|
||||
}
|
||||
|
||||
# Main script logic
|
||||
try {
|
||||
Write-Verbose "Starting main script execution..."
|
||||
Test-WeakADPasswords -DomainDetails $domainDetails -FilePath $WeakHashesSortedFilePath
|
||||
Test-WeakADPasswords -DomainDetails $domainDetails -FilePath $WeakHashesSortedFilePath -CheckOnlyEnabledUsers:$checkOnlyEnabledUsers
|
||||
} catch {
|
||||
Write-Error ("An error occurred during script execution: {0}" -f $_.Exception.Message)
|
||||
} finally {
|
||||
Stop-TestTranscript
|
||||
}
|
||||
|
||||
Write-Host "Script execution completed."
|
||||
|
230
Update-KHDB.ps1
230
Update-KHDB.ps1
@@ -7,7 +7,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: Update-KHDB.ps1 ##
|
||||
## Version: 1.0.1 ##
|
||||
## Version: 1.1.0 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
@@ -16,83 +16,183 @@
|
||||
Known hashes database update script for the Elysium AD password testing tool.
|
||||
|
||||
.DESCRIPTION
|
||||
This script downloads khdb.txt.zip from the designated Azure Storage account, decompresses it, and overwrites the current version.
|
||||
This script downloads khdb.txt.zip from the designated Azure Storage account, validates and decompresses it, and atomically updates the current version with backup and logging.
|
||||
#>
|
||||
|
||||
# Initialize an empty hashtable to store settings
|
||||
$ElysiumSettings = @{}
|
||||
# safer defaults
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
# Read the settings file
|
||||
$settingsPath = "ElysiumSettings.txt"
|
||||
Get-Content $settingsPath | ForEach-Object {
|
||||
if ($_ -notmatch '^#' -and $_.Trim()) {
|
||||
$keyValue = $_.Split('=', 2)
|
||||
$key = $keyValue[0].Trim()
|
||||
$value = $keyValue[1].Trim().Trim("'")
|
||||
$ElysiumSettings[$key] = $value
|
||||
# ensure TLS 1.2
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# Resolve paths
|
||||
$scriptRoot = $PSScriptRoot
|
||||
|
||||
function Start-UpdateTranscript {
|
||||
param(
|
||||
[string]$BasePath
|
||||
)
|
||||
try {
|
||||
$logsDir = Join-Path -Path $BasePath -ChildPath 'Reports/logs'
|
||||
if (-not (Test-Path $logsDir)) { New-Item -Path $logsDir -ItemType Directory -Force | Out-Null }
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$logPath = Join-Path -Path $logsDir -ChildPath "update-khdb-$ts.log"
|
||||
Start-Transcript -Path $logPath -Force | Out-Null
|
||||
} catch {
|
||||
Write-Warning "Could not start transcript: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify that all required settings have been loaded
|
||||
if (-not $ElysiumSettings.ContainsKey("storageAccountName") -or
|
||||
-not $ElysiumSettings.ContainsKey("containerName") -or
|
||||
-not $ElysiumSettings.ContainsKey("sasToken")) {
|
||||
Write-Error "Missing required settings. Please check your settings file."
|
||||
return
|
||||
function Stop-UpdateTranscript {
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
}
|
||||
|
||||
# Construct the full URL for accessing the Azure Blob Storage
|
||||
$storageAccountName = $ElysiumSettings["storageAccountName"]
|
||||
$containerName = $ElysiumSettings["containerName"]
|
||||
$sasToken = $ElysiumSettings["sasToken"]
|
||||
$AzureBlobStorageUrl = "https://$storageAccountName.blob.core.windows.net/$containerName/khdb.txt.zip$sasToken"
|
||||
|
||||
# Load necessary .NET assembly for HTTP operations
|
||||
Add-Type -AssemblyName System.Net.Http
|
||||
function Update-KHDB {
|
||||
Write-Host "Downloading KHDB..."
|
||||
|
||||
# Initialize the client for downloading the file
|
||||
$httpClient = New-Object System.Net.Http.HttpClient
|
||||
|
||||
try {
|
||||
# Start the asynchronous request to download the file
|
||||
$response = $httpClient.GetAsync($AzureBlobStorageUrl, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result
|
||||
|
||||
if ($response.IsSuccessStatusCode) {
|
||||
$totalBytes = $response.Content.Headers.ContentLength
|
||||
$totalRead = 0
|
||||
$read = 0
|
||||
$buffer = New-Object byte[] 8192
|
||||
$stream = $response.Content.ReadAsStreamAsync().Result
|
||||
$fileStream = [System.IO.File]::Create("khdb.txt.zip")
|
||||
|
||||
# Read the stream in chunks and update the progress bar
|
||||
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
||||
$fileStream.Write($buffer, 0, $read)
|
||||
$totalRead += $read
|
||||
$percentage = ($totalRead * 100) / $totalBytes
|
||||
Write-Progress -Activity "Downloading khdb.txt.zip" -Status "$([Math]::Round($percentage, 2))% Complete:" -PercentComplete $percentage
|
||||
}
|
||||
|
||||
$fileStream.Close()
|
||||
Write-Host "KHDB.zip downloaded successfully."
|
||||
} else {
|
||||
Write-Error "Failed to download khdb.txt.zip: $($response.StatusCode)"
|
||||
function Read-ElysiumSettings {
|
||||
$settings = @{}
|
||||
$settingsPath = Join-Path -Path $scriptRoot -ChildPath 'ElysiumSettings.txt'
|
||||
if (-not (Test-Path $settingsPath)) { throw "Settings file not found at $settingsPath" }
|
||||
Get-Content $settingsPath | ForEach-Object {
|
||||
if ($_ -and -not $_.Trim().StartsWith('#')) {
|
||||
$kv = $_ -split '=', 2
|
||||
if ($kv.Count -eq 2) { $settings[$kv[0].Trim()] = $kv[1].Trim().Trim("'") }
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Error during download: $_"
|
||||
return
|
||||
}
|
||||
return $settings
|
||||
}
|
||||
|
||||
# Decompressing KHDB.zip
|
||||
function Get-InstallationPath([hashtable]$settings) {
|
||||
$p = $settings['InstallationPath']
|
||||
if ([string]::IsNullOrWhiteSpace($p)) { return $scriptRoot }
|
||||
if ([System.IO.Path]::IsPathRooted($p)) { return $p }
|
||||
return (Join-Path -Path $scriptRoot -ChildPath $p)
|
||||
}
|
||||
|
||||
function New-HttpClient {
|
||||
Add-Type -AssemblyName System.Net.Http
|
||||
$client = [System.Net.Http.HttpClient]::new()
|
||||
$client.Timeout = [TimeSpan]::FromSeconds(600)
|
||||
$client.DefaultRequestHeaders.UserAgent.ParseAdd('Elysium/1.1 (+Update-KHDB)')
|
||||
return $client
|
||||
}
|
||||
|
||||
function Build-BlobUri([string]$account, [string]$container, [string]$sas) {
|
||||
if ([string]::IsNullOrWhiteSpace($account)) { throw 'storageAccountName is missing or empty.' }
|
||||
if ([string]::IsNullOrWhiteSpace($container)) { throw 'containerName is missing or empty.' }
|
||||
if ([string]::IsNullOrWhiteSpace($sas)) { throw 'sasToken is missing or empty.' }
|
||||
$sas = $sas.Trim()
|
||||
if (-not $sas.StartsWith('?')) { $sas = '?' + $sas }
|
||||
$ub = [System.UriBuilder]::new("https://$account.blob.core.windows.net/$container/khdb.txt.zip")
|
||||
$ub.Query = $sas.TrimStart('?')
|
||||
return $ub.Uri.AbsoluteUri
|
||||
}
|
||||
|
||||
function Invoke-DownloadWithRetry([System.Net.Http.HttpClient]$client, [string]$uri, [string]$targetPath) {
|
||||
$retries = 5
|
||||
$delay = 2
|
||||
for ($i = 0; $i -lt $retries; $i++) {
|
||||
try {
|
||||
$resp = $client.GetAsync($uri, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result
|
||||
if (-not $resp.IsSuccessStatusCode) {
|
||||
$code = [int]$resp.StatusCode
|
||||
if (($code -ge 500 -and $code -lt 600) -or $code -eq 429 -or $code -eq 408) { throw "Transient HTTP error $code" }
|
||||
throw "HTTP error $code"
|
||||
}
|
||||
$totalBytes = $resp.Content.Headers.ContentLength
|
||||
$stream = $resp.Content.ReadAsStreamAsync().Result
|
||||
$fs = [System.IO.File]::Create($targetPath)
|
||||
try {
|
||||
$buffer = New-Object byte[] 8192
|
||||
$totalRead = 0
|
||||
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
|
||||
$fs.Write($buffer, 0, $read)
|
||||
$totalRead += $read
|
||||
if ($totalBytes) {
|
||||
$pct = ($totalRead * 100.0) / $totalBytes
|
||||
Write-Progress -Activity "Downloading khdb.txt.zip" -Status ("{0:N2}% Complete" -f $pct) -PercentComplete $pct
|
||||
} else {
|
||||
Write-Progress -Activity "Downloading khdb.txt.zip" -Status ("Downloaded {0:N0} bytes" -f $totalRead) -PercentComplete 0
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$fs.Close(); $stream.Close()
|
||||
}
|
||||
return
|
||||
} catch {
|
||||
if ($i -lt ($retries - 1)) {
|
||||
Write-Warning "Download failed (attempt $($i+1)/$retries): $($_.Exception.Message). Retrying in ${delay}s..."
|
||||
Start-Sleep -Seconds $delay
|
||||
$delay = [Math]::Min($delay * 2, 30)
|
||||
} else {
|
||||
throw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Validate-KHDBFile([string]$path) {
|
||||
if (-not (Test-Path $path)) { throw "Validation failed: $path not found" }
|
||||
$lines = Get-Content -Path $path -Encoding UTF8
|
||||
if (-not $lines -or $lines.Count -eq 0) { throw 'Validation failed: file is empty.' }
|
||||
$regex = '^[0-9A-Fa-f]{32}$'
|
||||
$invalid = $lines | Where-Object { $_ -notmatch $regex }
|
||||
if ($invalid.Count -gt 0) {
|
||||
throw ("Validation failed: {0} invalid lines detected." -f $invalid.Count)
|
||||
}
|
||||
# Deduplicate and normalize line endings
|
||||
$unique = $lines | ForEach-Object { $_.Trim() } | Where-Object { $_ } | Sort-Object -Unique
|
||||
Set-Content -Path $path -Value $unique -Encoding ASCII
|
||||
}
|
||||
|
||||
function Update-KHDB {
|
||||
Start-UpdateTranscript -BasePath $scriptRoot
|
||||
try {
|
||||
Expand-Archive -Path "khdb.txt.zip" -DestinationPath . -Force
|
||||
Remove-Item -Path "khdb.txt.zip" -Force # Delete the zip file after extraction
|
||||
Write-Host "KHDB decompressed and cleaned up successfully."
|
||||
$settings = Read-ElysiumSettings
|
||||
$installPath = Get-InstallationPath $settings
|
||||
if (-not (Test-Path $installPath)) { New-Item -Path $installPath -ItemType Directory -Force | Out-Null }
|
||||
|
||||
$storageAccountName = $settings['storageAccountName']
|
||||
$containerName = $settings['containerName']
|
||||
$sasToken = $settings['sasToken']
|
||||
$uri = Build-BlobUri -account $storageAccountName -container $containerName -sas $sasToken
|
||||
|
||||
$client = New-HttpClient
|
||||
$tmpDir = New-Item -ItemType Directory -Path ([System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "elysium-khdb-" + [System.Guid]::NewGuid())) -Force
|
||||
$zipPath = Join-Path -Path $tmpDir.FullName -ChildPath 'khdb.txt.zip'
|
||||
$extractDir = Join-Path -Path $tmpDir.FullName -ChildPath 'extract'
|
||||
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
|
||||
|
||||
Write-Host "Downloading KHDB from Azure Blob Storage..."
|
||||
Invoke-DownloadWithRetry -client $client -uri $uri -targetPath $zipPath
|
||||
Write-Host "Download completed. Extracting archive..."
|
||||
|
||||
Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
|
||||
|
||||
$extractedKHDB = Get-ChildItem -Path $extractDir -Recurse -Filter 'khdb.txt' | Select-Object -First 1
|
||||
if (-not $extractedKHDB) { throw 'Extracted archive does not contain khdb.txt.' }
|
||||
|
||||
# Validate content
|
||||
Validate-KHDBFile -path $extractedKHDB.FullName
|
||||
|
||||
# Compute target path and backup
|
||||
$targetKHDB = Join-Path -Path $installPath -ChildPath 'khdb.txt'
|
||||
if (Test-Path $targetKHDB) {
|
||||
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$backupPath = Join-Path -Path $installPath -ChildPath ("khdb.txt.bak-$ts")
|
||||
Copy-Item -Path $targetKHDB -Destination $backupPath -Force
|
||||
Write-Host "Existing KHDB backed up to $backupPath"
|
||||
}
|
||||
|
||||
# Atomic-ish replace: move validated file into place
|
||||
Move-Item -Path $extractedKHDB.FullName -Destination $targetKHDB -Force
|
||||
Write-Host "KHDB updated at $targetKHDB"
|
||||
Write-Host "KHDB update completed successfully."
|
||||
} catch {
|
||||
Write-Error "Error decompressing KHDB: $_"
|
||||
return
|
||||
Write-Error ("KHDB update failed: {0}" -f $_.Exception.Message)
|
||||
throw
|
||||
} finally {
|
||||
try { if ($tmpDir -and (Test-Path $tmpDir.FullName)) { Remove-Item -Path $tmpDir.FullName -Recurse -Force } } catch {}
|
||||
Stop-UpdateTranscript
|
||||
}
|
||||
}
|
||||
|
||||
|
52
decrypt.py
52
decrypt.py
@@ -7,7 +7,7 @@
|
||||
##################################################
|
||||
## Project: Elysium ##
|
||||
## File: decrypt.py ##
|
||||
## Version: 1.0.0 ##
|
||||
## Version: 1.1.0 ##
|
||||
## Support: support@cqre.net ##
|
||||
##################################################
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
# Install PyCryptodome with "pip install pycryptodome". Must be run with python3.
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
# Ask for the encrypted file's name
|
||||
encrypted_file_name = input("Enter the name of the encrypted file (with .enc extension): ")
|
||||
encrypted_file_path = f'path/to/your/encrypted/{encrypted_file_name}'
|
||||
encrypted_file_path = encrypted_file_name if os.path.isabs(encrypted_file_name) else os.path.join(os.getcwd(), encrypted_file_name)
|
||||
decrypted_file_path = encrypted_file_path.replace('.enc', '.zip')
|
||||
|
||||
# Try to retrieve the passphrase from the environment variable
|
||||
@@ -30,22 +31,47 @@ if passphrase is None:
|
||||
passphrase = input("Passphrase not found in environment. Please enter the passphrase: ")
|
||||
# Here, you might save the passphrase to a temporary session or file, but be cautious with security.
|
||||
|
||||
# Derive the AES key from the passphrase
|
||||
key = hashlib.sha256(passphrase.encode()).digest()
|
||||
def decrypt_legacy(data: bytes, key_bytes: bytes) -> bytes:
|
||||
if len(data) < 16:
|
||||
raise ValueError("Encrypted data too short for legacy format")
|
||||
iv = data[:16]
|
||||
encrypted = data[16:]
|
||||
cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
|
||||
plaintext = cipher.decrypt(encrypted)
|
||||
pad_len = plaintext[-1]
|
||||
if not (1 <= pad_len <= 16):
|
||||
raise ValueError("Invalid padding length in legacy data")
|
||||
return plaintext[:-pad_len]
|
||||
|
||||
def decrypt_pbkdf2(data: bytes, passphrase: str) -> bytes:
|
||||
if len(data) < 4 + 16 + 16:
|
||||
raise ValueError("Encrypted data too short for PBKDF2 format")
|
||||
magic = data[:4]
|
||||
if magic != b"ELY1":
|
||||
raise ValueError("Invalid magic header for PBKDF2 format")
|
||||
salt = data[4:20]
|
||||
iv = data[20:36]
|
||||
encrypted = data[36:]
|
||||
key = PBKDF2(passphrase, salt, dkLen=32, count=100000)
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
plaintext = cipher.decrypt(encrypted)
|
||||
pad_len = plaintext[-1]
|
||||
if not (1 <= pad_len <= 16):
|
||||
raise ValueError("Invalid padding length in PBKDF2 data")
|
||||
return plaintext[:-pad_len]
|
||||
|
||||
try:
|
||||
# Read the encrypted file
|
||||
with open(encrypted_file_path, 'rb') as encrypted_file:
|
||||
iv = encrypted_file.read(16) # The first 16 bytes are the IV
|
||||
encrypted_data = encrypted_file.read()
|
||||
blob = encrypted_file.read()
|
||||
|
||||
# Decrypt the data
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
decrypted_data = cipher.decrypt(encrypted_data)
|
||||
|
||||
# Remove potential PKCS#7 padding
|
||||
pad_len = decrypted_data[-1]
|
||||
decrypted_data = decrypted_data[:-pad_len]
|
||||
# Try PBKDF2 format first (ELY1 header), then legacy fallback
|
||||
if blob.startswith(b"ELY1"):
|
||||
decrypted_data = decrypt_pbkdf2(blob, passphrase)
|
||||
else:
|
||||
# Legacy key derivation: SHA-256(passphrase), IV is first 16 bytes
|
||||
legacy_key = hashlib.sha256(passphrase.encode()).digest()
|
||||
decrypted_data = decrypt_legacy(blob, legacy_key)
|
||||
|
||||
# Write the decrypted data to a file
|
||||
with open(decrypted_file_path, 'wb') as decrypted_file:
|
||||
|
Reference in New Issue
Block a user