About File Associations

The following article Microsoft Broke Windows 10’s File Associations With a Botched Update appeared recently and states that:

File associations no longer work properly on Windows 10 after a buggy update. Windows won’t let you select certain applications as your defaults.
[…]
For example, here’s what happens when we try setting Notepad++ as our default application for .txt files in Windows 10’s Settings app. Windows just ignores our choice and chooses Notepad as the default.

behavior

It appears that I’ve experienced the same thing on a Windows 10 Enterprise (1803). Just keep in mind that we see in the above gif that only “registered” applications can be set. Applications like notepad++ isn’t registered and cannot be set and used for the .txt file association.

I’ve set successfully a group policy that defines the .pdf file to be opened by Adobe Reader. It’s still there and works fine. I’ve created it using the official guidance from Microsoft and the ProgID found in the Adobe documentation on this page.

Using my Google-fu, I also found the following articles that shed some lights on how the file associations work:

I was not able to define and load a custom xml file where I could set notepad++ as the default .txt handler.

After I removed the anti-tampering protection where Microsoft sets a DENY permission on the
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.xxx\UserChoice registry key, I could delete values under that key.

I didn’t need to reverse engineer secret hashes under that key like some known tools (setuserfta) do.

All I did actually is remove anything under the UserChoice, create my own ProgId under the OpenWithProgids key and create its related value under the OpenWithList list.

Function Add-FileAssociation {
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[ValidatePattern('^\.[a-zA-Z0-9]{1,3}')]
$Extension,
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[ValidateScript({
Test-Path -Path $_ -PathType Leaf
})]
[string]$TargetExecutable,
[Parameter()]
[string]$ftypeName
)
Begin {
$ext = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($Extension)
$exec = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($TargetExecutable)
# 2. Create a ftype
if (-not($PSBoundParameters['ftypeName'])) {
$ftypeName = '{0}{1}File'-f $($ext -replace '\.',''),
$((Get-Item -Path "$($exec)").BaseName)
$ftypeName = [Management.Automation.Language.CodeGeneration]::EscapeFormatStringContent($ftypeName)
} else {
$ftypeName = [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($ftypeName)
}
Write-Verbose -Message "Ftype name set to $($ftypeName)"
}
Process {
# 1. remove anti-tampering protection if required
if (Test-Path -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)") {
$ParentACL = Get-Acl -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)"
if (Test-Path -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\UserChoice") {
$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\UserChoice",'ReadWriteSubTree','TakeOwnership')
$acl = $k.GetAccessControl()
$null = $acl.SetAccessRuleProtection($false,$true)
$rule = New-Object System.Security.AccessControl.RegistryAccessRule ($ParentACL.Owner,'FullControl','Allow')
$null = $acl.SetAccessRule($rule)
$rule = New-Object System.Security.AccessControl.RegistryAccessRule ($ParentACL.Owner,'SetValue','Deny')
$null = $acl.RemoveAccessRule($rule)
$null = $k.SetAccessControl($acl)
Write-Verbose -Message 'Removed anti-tampering protection'
}
}
# 2. add a ftype
$null = & (Get-Command "$($env:systemroot)\system32\reg.exe") @(
'add',
"HKCU\Software\Classes\$($ftypeName)\shell\open\command"
'/ve','/d',"$('\"{0}\" \"%1\"'-f $($exec))",
'/f','/reg:64'
)
Write-Verbose -Message "Adding command under HKCU\Software\Classes\$($ftypeName)\shell\open\command"
# 3. Update user file association
@"
Windows Registry Editor Version 5.00
[-HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\OpenWithList]
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\OpenWithList]
"MRUList"="a"
"a"="$((Get-Item -Path "$($exec)").Name)"
[-HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\OpenWithProgids]
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\OpenWithProgids]
"$($ftypeName)"=hex(0):
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\$($ext)\UserChoice]
"Hash"=-
"Progid"=-
"@ |
Out-File -FilePath "$($env:TEMP)\$($ftypeName).dat" -Encoding ascii -Force
& (Get-Command "$($env:systemroot)\regedit.exe") @('/s',"$($env:TEMP)\$($ftypeName).dat")
Write-Verbose -Message 'Updated user file extension under HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts'
}
End {}
<#
.SYNOPSIS
Set user file associations
.DESCRIPTION
Define a program to open a file extension
.PARAMETER Extension
The file extension to modify
.PARAMETER TargetExecutable
The program to use to open the file extension
.PARAMETER ftypeName
Non mandatory parameter used to override the created file type handler value
.EXAMPLE
$HT = @{
Extension = '.txt'
TargetExecutable = "C:\Program Files\Notepad++\notepad++.exe"
}
Add-FileAssociation @HT
.EXAMPLE
$HT = @{
Extension = '.xml'
TargetExecutable = "C:\Program Files\Microsoft VS Code\Code.exe"
FtypeName = 'vscode'
}
Add-FileAssociation @HT
#>
}

Bonus: If you use the Reset button under Default Apps, Microsoft will gracefully restore the previous behavior.
With the above function or script, your user (admin or not) is autonomous and can restore his favorite file associations until Microsoft changes the rules…