Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
1573a13
Merge pull request #26 from Apteco/dev: Adding publish workflows for …
gitfvb Sep 23, 2025
0054157
Merge pull request #27 from Apteco/dev
gitfvb Sep 23, 2025
f64f24b
Merge pull request #28 from Apteco/dev
gitfvb Sep 23, 2025
6fa7ead
Merge pull request #29 from Apteco/dev
gitfvb Sep 23, 2025
763cc11
Merge pull request #30 from Apteco/dev
gitfvb Sep 23, 2025
371cde7
Merge pull request #31 from Apteco/dev
gitfvb Sep 24, 2025
bc73cf8
Merge pull request #32 from Apteco/dev
gitfvb Sep 24, 2025
2ff7cac
Merge pull request #33 from Apteco/dev
gitfvb Sep 25, 2025
4821bcb
Merge pull request #34 from Apteco/dev
gitfvb Sep 25, 2025
925b947
Merge pull request #35 from Apteco/dev
gitfvb Sep 26, 2025
49bb4f7
Merge pull request #36 from Apteco/dev
gitfvb Sep 26, 2025
94ef10a
Merge pull request #37 from Apteco/dev
gitfvb Sep 26, 2025
9750e1d
Merge pull request #38 from Apteco/dev
gitfvb Sep 29, 2025
9ecc59f
Merge pull request #39 from Apteco/dev
gitfvb Sep 29, 2025
cb107a9
Merge pull request #40 from Apteco/dev
gitfvb Sep 29, 2025
4b77c77
Merge pull request #41 from Apteco/dev
gitfvb Sep 29, 2025
29889b2
Merge pull request #42 from Apteco/dev
gitfvb Sep 30, 2025
432191f
Merge pull request #43 from Apteco/dev
gitfvb Sep 30, 2025
f503901
Merge pull request #44 from Apteco/dev
gitfvb Sep 30, 2025
194fd18
Merge pull request #45 from Apteco/dev
gitfvb Sep 30, 2025
5a3c805
Merge pull request #46 from Apteco/dev
gitfvb Oct 1, 2025
11ab939
Merge pull request #47 from Apteco/dev
gitfvb Oct 1, 2025
b69e028
Merge pull request #48 from Apteco/dev
gitfvb Oct 1, 2025
704dcef
Merge pull request #49 from Apteco/dev
gitfvb Oct 21, 2025
4ba40b2
Merge pull request #50 from Apteco/dev
gitfvb Oct 22, 2025
94c09e3
Merge pull request #51 from Apteco/dev
gitfvb Oct 22, 2025
cd8767c
Merge pull request #52 from Apteco/dev
gitfvb Oct 30, 2025
1df5d69
Merge pull request #53 from Apteco/dev
gitfvb Nov 6, 2025
ced0296
Merge pull request #54 from Apteco/dev
gitfvb Nov 7, 2025
87f647a
Merge pull request #55 from Apteco/dev
gitfvb Nov 7, 2025
371f112
Merge pull request #56 from Apteco/dev
gitfvb Nov 18, 2025
fb4e50b
Update psnotify.publish.yml
gitfvb Nov 19, 2025
da5aaaa
Merge pull request #57 from Apteco/dev
gitfvb Nov 19, 2025
34164d7
Merge pull request #58 from Apteco/dev
gitfvb Nov 20, 2025
84ed13a
Merge pull request #59 from Apteco/dev
gitfvb Dec 8, 2025
1267d9e
Merge pull request #60 from Apteco/dev
gitfvb Dec 8, 2025
db780f4
Merge pull request #61 from Apteco/dev
gitfvb Dec 8, 2025
00c5f2d
Merge pull request #62 from Apteco/dev
gitfvb Dec 10, 2025
b04e220
Merge pull request #63 from Apteco/dev
gitfvb Dec 23, 2025
cb4a3cb
Update copyright year in LICENSE file
gitfvb Jan 16, 2026
44ce5a5
Merge pull request #64 from Apteco/dev
gitfvb Jan 21, 2026
c878fae
Merge pull request #65 from Apteco/dev
gitfvb Jan 22, 2026
9de8209
Merge pull request #66 from Apteco/dev
gitfvb Jan 22, 2026
8c6c7f4
Merge pull request #67 from Apteco/dev
gitfvb Jan 22, 2026
cf4ea24
Merge pull request #68 from Apteco/dev
gitfvb Jan 23, 2026
2307561
Merge pull request #69 from Apteco/dev
gitfvb Jan 23, 2026
f3f8d7b
Merge pull request #70 from Apteco/dev
gitfvb Mar 3, 2026
b0f8d20
Merge pull request #71 from Apteco/dev
gitfvb Mar 3, 2026
88c1a88
Change module installation scope to CurrentUser
gitfvb Mar 27, 2026
9ff3019
Update module installation scope in SQL pipeline
gitfvb Mar 27, 2026
1a95468
Add ConnectionName parameter to Open-SQLiteConnection
gitfvb Mar 27, 2026
4457847
Merge pull request #72 from Apteco/dev
gitfvb Mar 27, 2026
4884e52
Merge pull request #73 from Apteco/dev
gitfvb Mar 27, 2026
5181911
docs(EncryptCredential): add keyfile ACL guidance and scheduled task/…
claude Mar 28, 2026
8f3d1e5
feat(EncryptCredential): bind encrypted strings to machine and OS use…
claude Mar 28, 2026
df28e07
docs(EncryptCredential): add v0.4.0 migration instructions to README
claude Mar 28, 2026
582be20
feat(EncryptCredential): replace HMAC binding with DPAPI on Windows
claude Mar 28, 2026
22af9ed
docs(EncryptCredential): add C# hosted PowerShell section with DPAPI …
claude Mar 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/sqlpipeline.tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ jobs:
shell: powershell
run: |
Set-PSRepository psgallery -InstallationPolicy trusted
Install-Module WriteLog, ImportDependency -Repository PSGallery
Install-Module SimplySQL -AllowClobber -Repository PSGallery
Install-Module WriteLog, ImportDependency -Repository PSGallery -Scope CurrentUser
Install-Module SimplySQL -AllowClobber -Repository PSGallery -Scope CurrentUser

- name: Debug
run: |
Expand All @@ -53,8 +53,8 @@ jobs:
shell: pwsh
run: |
Set-PSRepository psgallery -InstallationPolicy trusted
Install-Module WriteLog, ImportDependency -Repository PSGallery
Install-Module SimplySQL -AllowClobber -Repository PSGallery
Install-Module WriteLog, ImportDependency -Repository PSGallery -Scope CurrentUser
Install-Module SimplySQL -AllowClobber -Repository PSGallery -Scope CurrentUser

- name: Run Pester Tests (pwsh on ${{ matrix.os }})
if: matrix.shell != 'powershell'
Expand Down
15 changes: 14 additions & 1 deletion EncryptCredential/EncryptCredential/EncryptCredential.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
RootModule = 'EncryptCredential.psm1'

# Version number of this module.
ModuleVersion = '0.3.0'
ModuleVersion = '0.4.0'

# Supported PSEditions
# CompatiblePSEditions = @()
Expand Down Expand Up @@ -135,6 +135,19 @@ PrivateData = @{

# ReleaseNotes of this module
ReleaseNotes = "
0.4.0 Added machine-and-user binding to make encrypted strings non-portable.
Windows: keyfile is now written as a DPAPI-protected blob (CurrentUser scope)
by New-KeyfileRaw. Get-BoundKey calls ProtectedData.Unprotect() to recover
the AES key; this only succeeds for the same user on the same machine and is
backed by Windows credential infrastructure (LSASS, optionally TPM).
Knowing the user SID or machine GUID alone is not sufficient to bypass it.
Linux/macOS: key is derived via HMAC-SHA256(key=keyfileBytes,
data=machine-id|username|uid), all read from the OS at runtime.
Read-Keyfile updated to return DPAPI blobs as-is for Get-BoundKey to handle.
Scheduled tasks on Windows must use LogonType=Password to ensure the user
profile is loaded (required by DPAPI).
BREAKING CHANGE: all previously encrypted strings must be re-encrypted.
See 'Migrating to v0.4.0' in the README.
0.3.0 Reworked the module with Claude AI to be more secure and robust, now using another way to create
a keyfile for salting. The old encryption method is still supported, so all
previously encrypted strings will stay valid UNTIL you call New-Keyfile. After that,
Expand Down
86 changes: 86 additions & 0 deletions EncryptCredential/EncryptCredential/Private/Get-BoundKey.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
Function Get-BoundKey {
<#
Derives or recovers a machine-and-user-bound 32-byte AES key from raw keyfile bytes.

SECURITY MODEL
--------------
Windows
New-KeyfileRaw writes the keyfile as a DPAPI-protected blob (CurrentUser scope).
Get-BoundKey calls ProtectedData.Unprotect() to recover the original random bytes.
Unprotect only succeeds for the same user on the same machine; it is backed by
Windows credential infrastructure (LSASS, optionally TPM-backed master key).
Unlike the previous HMAC approach, knowing the user SID or machine GUID alone
is NOT sufficient — the user's actual login credentials are part of the key
material managed by Windows.

Linux / macOS
The keyfile holds raw random bytes (no DPAPI available).
An HMAC-SHA256 is computed over those bytes using machine identity
(/etc/machine-id) and the current numeric UID as the HMAC key.
This raises the bar against cross-machine / cross-user keyfile theft but is
a software-only binding — the binding values are not themselves secret.

In both cases the binding is determined by the operating system at runtime.
There is no parameter a caller can supply to override or forge the identity.
#>

[OutputType([byte[]])]
param(
[Parameter(Mandatory=$true)][byte[]]$RawKey
)

if ($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) {

# Windows: keyfile is a DPAPI blob written by New-KeyfileRaw.
# Unprotect recovers the original AES key bytes.
# This only succeeds for the same user on the same machine.
Add-Type -AssemblyName System.Security
try {
return [byte[]][System.Security.Cryptography.ProtectedData]::Unprotect(
$RawKey,
$null,
[System.Security.Cryptography.DataProtectionScope]::CurrentUser
)
} catch [System.Security.Cryptography.CryptographicException] {
throw (
"Keyfile cannot be decrypted by DPAPI. Possible causes: " +
"(1) the keyfile was created on a different machine or by a different user account; " +
"(2) it is in the legacy raw-bytes format from v0.3.0 or earlier — " +
"see 'Migrating to v0.4.0' in the README."
)
}

} else {

# Linux / macOS: HMAC-SHA256 binding.
# machine-id + username + numeric UID are read from the OS at runtime;
# they cannot be overridden by the calling script.
$machineId = $null
foreach ($candidate in @('/etc/machine-id', '/var/lib/dbus/machine-id')) {
if (Test-Path -Path $candidate) {
$machineId = ([System.IO.File]::ReadAllText($candidate)).Trim()
break
}
}
if ([string]::IsNullOrEmpty($machineId)) {
throw (
"Cannot determine machine identity: neither /etc/machine-id nor " +
"/var/lib/dbus/machine-id was found. Ensure one of these files exists."
)
}

$userName = [System.Environment]::UserName
$uid = (& id -u 2>$null)
$binding = "${machineId}|${userName}|${uid}"

$bindingBytes = [System.Text.Encoding]::UTF8.GetBytes($binding)
$hmac = [System.Security.Cryptography.HMACSHA256]::new($RawKey)
try {
return [byte[]]$hmac.ComputeHash($bindingBytes) # always 32 bytes
} finally {
$hmac.Dispose()
}

}

}
23 changes: 19 additions & 4 deletions EncryptCredential/EncryptCredential/Private/New-KeyfileRaw.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,22 @@ Function New-KeyfileRaw {
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$rng.GetBytes($Key)
$rng.Dispose()
[System.IO.File]::WriteAllBytes($Path, $Key)

# Restrict file access to the current user only
If ($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) {
# Windows: remove inherited ACEs, grant current user full control

# Windows: DPAPI-protect the key bytes before writing to disk.
# The resulting blob can only be decrypted by the same user on the
# same machine — it is backed by Windows credential infrastructure
# (LSASS, optionally TPM). The raw AES key never touches disk.
Add-Type -AssemblyName System.Security
$protected = [System.Security.Cryptography.ProtectedData]::Protect(
$Key,
$null,
[System.Security.Cryptography.DataProtectionScope]::CurrentUser
)
[System.IO.File]::WriteAllBytes($Path, $protected)

# ACL: also restrict file access to the current user (defence in depth)
$acl = Get-Acl -Path $Path
$acl.SetAccessRuleProtection($true, $false)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
Expand All @@ -56,9 +67,13 @@ Function New-KeyfileRaw {
)
$acl.SetAccessRule($rule)
Set-Acl -Path $Path -AclObject $acl

} else {
# Linux/macOS: owner read/write only (600)

# Linux/macOS: save raw bytes, restrict to owner read/write only (600)
[System.IO.File]::WriteAllBytes($Path, $Key)
& chmod 600 $Path

}

} else {
Expand Down
16 changes: 10 additions & 6 deletions EncryptCredential/EncryptCredential/Private/Read-Keyfile.ps1
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
Function Read-Keyfile {
<#
Returns the AES key bytes from the keyfile.
Supports both formats:
- New: raw binary (16/24/32 bytes written with WriteAllBytes)
- Legacy: UTF8 text with one decimal number per line (written with Set-Content -Encoding UTF8)
Returns raw file bytes for Get-BoundKey to interpret.

Supports three formats:
- Windows v0.4.0+: DPAPI-protected blob (variable length, always > 32 bytes)
- Binary (Linux / Windows v0.3.0): raw AES key, exactly 16, 24, or 32 bytes
- Legacy text (all platforms, very old): UTF8, one decimal byte value per line
#>
param(
[Parameter(Mandatory=$true)][String]$Path
)

$rawBytes = [System.IO.File]::ReadAllBytes($Path)

If ($rawBytes.Length -in @(16, 24, 32)) {
# Binary keyfile (new format)
# Binary file: either a raw AES key (16/24/32 bytes) or a Windows DPAPI blob (> 32 bytes).
# Return as-is in both cases; Get-BoundKey handles the interpretation.
If ($rawBytes.Length -in @(16, 24, 32) -or
(($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) -and $rawBytes.Length -gt 32)) {
return $rawBytes
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ Function Convert-PlaintextToSecure {

$return = ""

# read key bytes (handles both binary and legacy text format)
$salt = Read-Keyfile -Path $Script:keyfile
# read key bytes (handles both binary and legacy text format),
# then derive a machine-and-user-bound key so the result is not portable
$salt = Get-BoundKey -RawKey (Read-Keyfile -Path $Script:keyfile)

# convert
$stringSecure = ConvertTo-secureString -String $String -asplaintext -force
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ Function Convert-SecureToPlaintext {

$return = ""

# read key bytes (handles both binary and legacy text format)
$salt = Read-Keyfile -Path $Script:keyfile
# read key bytes (handles both binary and legacy text format),
# then derive a machine-and-user-bound key so decryption only succeeds
# on the same machine and under the same OS user account
$salt = Get-BoundKey -RawKey (Read-Keyfile -Path $Script:keyfile)

#convert
Try {
Expand Down
Loading