Powershell: ConvertFrom-SecureString is broken on Linux

Created on 5 Aug 2016  路  62Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

  1. Install PowerShell on Ubuntu 14.04
  2. Launch PowerShell
  3. Run the following:
   $password = Convertto-Securestring -String "PowerShellRocks!" -AsPlainText -Force
   ConvertFrom-SecureString $password  

Expected behavior

No error

Actual behavior

The following error is thrown

PS /home/chythu/temp> ConvertFrom-SecureString $password                        ConvertFrom-SecureString : Unable to load DLL 'CRYPT32.dll': The specified
module could not be found.
 (Exception from HRESULT: 0x8007007E)
At line:1 char:1
- ConvertFrom-SecureString $password
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  - CategoryInfo          : NotSpecified: (:) [ConvertFrom-SecureString], Dl
    lNotFoundException
  - FullyQualifiedErrorId : System.DllNotFoundException,Microsoft.PowerShell
    .Commands.ConvertFromSecureStringCommand

Environment data

Name                           Value
---
PSVersion                      5.1.10032.0
PSEdition                      PowerShellCore
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   3.0.0.0
GitCommitId                    v6.0.0-alpha.7
CLRVersion
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

Updates by @travisez13 on 2016-04-10

Environment data

> $PSVersionTable
Name                           Value                                           
----                           -----                                           
PSVersion                      6.0.2                                           
PSEdition                      Core                                            
GitCommitId                    v6.0.2                                          
OS                             Darwin 17.5.0 Darwin Kernel Version 17.5.0: M...
Platform                       Unix                                            
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}                         
PSRemotingProtocolVersion      2.3                                             
SerializationVersion           1.1.0.1                                         
WSManStackVersion              3.0                                             


Workaround

The following works
```Powershell

you should generate your own key

$Key = (3,4,2,3,56,34,254,222,1,1,2,23,42,54,33,233,1,34,2,7,6,5,35,43)
$s | ConvertFrom-SecureString -Key $Key
```

Area-Cmdlets Issue-Bug OS-Linux OS-macOS Resolution-Fixed

Most helpful comment

The bottom line is that the .NET framework (and PowerShell) needs a cross-platform data protection library, because dev/ops need to store secrets didn't just suddenly disappear when we added new OSes to the mix, and it's not always practical to rely on web services like Azure KeyVault, RED Identity management, or Thycotic Secret Server. 馃槙

The .NET team is apparently not inclined to be particularly helpful here.

I know that ASP.NET wrote their own DataProtection stuff, but it's fairly weird and they recommend limiting it's use to specific scenarios...

What we need to know is:

Does the PowerShell team plan to create a cross-platform implementation of SecureString serialization?

If not, please remove the cmdlets that do not work at all, and provide a better error message for CliXML than the current, "oh darn, if only there was a Crypto dll available" error.

All 62 comments

Talked @KrishnaV-MSFT This is not needed for Azure demo. Moving it out of Alpha.10

Came across the same error on MacOS 10.12 Beta (16A270f)

Was just messing around got this:

PS> $User="Jared"
PS> $PWord = ConvertTo-SecureString 鈥揝tring "TestString" 鈥揂sPlainText -Force   
PS> $Cred = New-Object -TypeName "System.Management.Automation.PSCredential" 鈥揂rgumentList $User, $PWord
PS> ConvertFrom-SecureString -SecureString ($Cred.Password)

Result:

ConvertFrom-SecureString : Unable to load DLL 'CRYPT32.dll': The specified module could not be found.
 (Exception from HRESULT: 0x8007007E)
At line:1 char:1
+ ConvertFrom-SecureString -SecureString ($Cred.Password)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [ConvertFrom-SecureString], DllNotFoundException
    + FullyQualifiedErrorId : System.DllNotFoundException,Microsoft.PowerShell.Commands.ConvertFromSecureStringComman

Hi,

same library error when trying to use mapped cert: psdrive:

Get-ChildItem Cert:/LocalMachine/

Error:

get-childitem : Unable to load DLL 'crypt32.dll': The specified module could not be found.
(Exception from HRESULT: 0x8007007E)
At line:1 char:1

  • get-childitem Cert:/LocalMachine/
  • ~~~~~~~~~

    • CategoryInfo : NotSpecified: (:) [Get-ChildItem], DllNotFoundException

    • FullyQualifiedErrorId : System.DllNotFoundException,Microsoft.PowerShell.Commands.GetChildItemCommand

@35359595 good finding! The error definitely should be friendlier. On Linux and macOS Cert:/ provider needs some re-thinking. The way these two systems approach storing certificates are completely different from each other and windows.

Fyi, 16.04.1 with PowerShell v6 alpha 14 still has this same issue

This is holding me back from bringing my modules over to Linux. I'd like to be able to store Web API keys securely on all OSes, not just Windows.

This is something we'll only be able to enable with the .NET Standard 2.0 APIs that bring back SecureString:

  • CoreFX Issue: dotnet/corefx#13062
  • CoreFX 2.0 PR: dotnet/corefx#13362

ConvertFrom-SecureString and ConvertTo-SecureString depend on System.Security.Cryptography.ProtectedData, which is still not available in netstandard2.0.
So these 2 cmdlets need to be re-worked on Unix platforms.

I would really appreciate it if this functionality comes to .Net Core
We rely on WinRM and use PSCredential to authenticate. Running our scripts on Linux or MacOS is not possible right now and it's holding us back a bit.

Error we see:
ConvertTo-SecureString : Unable to load DLL 'CRYPT32.dll': The specified module could not be found.
(Exception from HRESULT: 0x8007007E)

@reddwarf666 The package System.Security.Cryptography.ProtectedData is available on nuget.org and is netstandard2.0 complaint, However, it doesn't have implementation for Unix platforms -- it will throw 'PlatformNotSupportedException' on Unix platformas. So ConvertFrom/ConvertTo-SecureString need to be re-written for Unix. We will try to get some guidance from .NET Core team on how to do the same tasks on Unix. /cc @joeyaiello

Would [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR and [System.Runtime.InteropServices.Marshal]::PtrToStringAuto likely come along for the ride?

Still seeing this on 6.0.0-beta3 on Mac and Linux when using SecureString cmdlets.

ConvertFrom-SecureString : Unable to load DLL 'CRYPT32.dll': The specified
module or one of its dependencies could not be found.
(Exception from HRESULT: 0x8007007E)

same here with convertfrom-securestring and convertto-securestring (v6.0.0-beta.3) on linux debian (jessie 64bit):

PS /> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      6.0.0-beta
PSEdition                      Core
GitCommitId                    v6.0.0-beta.3
OS                             Linux 3.16.0-4-amd64 #1 SMP Debian 3.16.43-2+deb8u2 (2017-06-26)
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

PS />

PS /> read-host -assecurestring | convertfrom-securestring | out-file securestring.txt
********
convertfrom-securestring : Unable to load DLL 'CRYPT32.dll': The specified module or one of its dependencies could not be found.
 (Exception from HRESULT: 0x8007007E)
At line:1 char:29
+ read-host -assecurestring | convertfrom-securestring | out-file secur ...
+                             ~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [ConvertFrom-SecureString], DllNotFoundException
    + FullyQualifiedErrorId : System.DllNotFoundException,Microsoft.PowerShell.Commands.ConvertFromSecureStringCommand



md5-0a78a142751a1081c46c48e10b6cba93



PS /> $pass = cat securestring.txt | convertto-securestring
convertto-securestring : Unable to load DLL 'CRYPT32.dll': The specified module or one of its dependencies could not be found.
 (Exception from HRESULT: 0x8007007E)
At line:1 char:32
+ $pass = cat securestring.txt | convertto-securestring
+                                ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [ConvertTo-SecureString], DllNotFoundException
    + FullyQualifiedErrorId : System.DllNotFoundException,Microsoft.PowerShell.Commands.ConvertToSecureStringCommand



md5-25ce90db07e4f9f61b58abd5cf739027



PS /> [Reflection.Assembly]::LoadFrom("CRYPT32.dll")
Exception calling "LoadFrom" with "1" argument(s): "Bad IL format."
At line:1 char:1
+ [Reflection.Assembly]::LoadFrom("CRYPT32.dll")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : BadImageFormatException



md5-0a78a142751a1081c46c48e10b6cba93



PS /> Add-Type -Path 'CRYPT32.dll'
Add-Type : Bad IL format.
At line:1 char:1
+ Add-Type -Path 'CRYPT32.dll'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Add-Type], BadImageFormatException
    + FullyQualifiedErrorId : System.BadImageFormatException,Microsoft.PowerShell.Commands.AddTypeCommand

copying to /opt/microsoft/powershell/6.0.0-beta.3/ and also when specified the absolute path, gives same error as above.

is there a workaround possible/known or do we have to wait until this is fixed?

@vchrizz Windows and Unix isn't binary compatible and we cannot use Windows dll on Unix. So we should wait CoreFX.

@iSazonov i'm aware of this, was wondering because of those .dll files in /opt/microsoft/powershell/6.0.0-beta.3/ but then found out:
linux .dll files:
"PE32 executable (DLL) (console) Intel 80386 Mono/.Net assembly, for MS Windows"
"PE32+ executable (DLL) (console) Mono/.Net assembly, for MS Windows"
windows CRYPT32.dll file:
"PE32 executable (DLL) (GUI) Intel 80386, for MS Windows"

now looking for a workaround and found the same problem with Export-Clixml:

PS /> $cred=Get-Credential 鈥揷redential "myuser" | Export-Clixml SecureCredentials.xml

Windows PowerShell credential request
Enter your credentials.
Password for user myuser: **********

Export-Clixml : Unable to load DLL 'CRYPT32.dll': The specified module or one of its dependencies could not be found.
 (Exception from HRESULT: 0x8007007E)
At line:1 char:45
+ ... Credential 鈥揷redential "myuser" | Export-Clixml SecureCredentials.xml
+                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Export-Clixml], DllNotFoundException
    + FullyQualifiedErrorId : System.DllNotFoundException,Microsoft.PowerShell.Commands.ExportClixmlCommand

@vchrizz Currently .Net CLI puts all assemblies in Unix packages. #3961 track the Unix packaging.

@daxian-dbw We could use OpenSSH to protect/unprotect data.

I'd rather we don't do a custom thing for non-Windows. Looks like Nuget team is also asking for this.

@SteveL-MSFT , can confirm that the nuget team needs it ( NuGet/Home#1851 ), as I was the one who brought up with them as a missing functionality

@SteveL-MSFT @psmulovics It seems CoreFX team don't track closed issues at all - I believe we should open new Issue there if we want any progress.

It worked! 馃槃 Now we have a answer:

We have no plans to do this. It requires OS features that are only available on Windows.
..
What we have in .NET Core is clear. On Windows it does whatever DPAPI does. On non-Windows it does what Windows DPAPI does on those platforms: not exist.

So we should conclusion:

  1. Use the package System.Security.Cryptography.ProtectedData on Windows and block the feature on other planforms.
  2. Create workaround for other planforms. - If so I believe we should open a new Issue for tracking.

Thanks for looking into this.

@iSazonov would option 2 also bring in System.Runtime.InteropServices.Marshal ?

@iSazonov I suppose at least we have clarity on why it won't be done instead of just getting the issue closed. Since this isn't a small work item, I think we'll look into it for 6.1.0

@SteveL-MSFT For backward compatibility with Windows PowerShell I believe it is good to use System.Security.Cryptography.ProtectedData today.

@ngetchell The workaround as described in the CoreFX Issue, requires too much specific work so we shall wait CoreFX. Possible workaround for Unix systems may be - to use a remote connection to windows systems.

@iSazonov the repro works for me with beta.4 on Windows, I believe the issue is only on non-Windows currently

@SteveL-MSFT Sorry for the inaccuracy, under System.Security.Cryptography.ProtectedData I meant _CoreFX package_. Currently we use internal implementation. Questions is - should we remove the internal code and migrate to the package? Should we remove *-SecureString cmdlets from Unix?

@iSazonov I believe we should move to the official package. As for Unix, it seems like the right thing to do is to remove them. cc @joeyaiello

Just want to ping this story again.
Is it still the plan to _remove_ the ConvertTo/ConvertFrom SecureString cmdlets?
What about the handling of Credentials and SecureStrings in Import/Export CliXml?

All of these are still throwing the very unfriendly DllNotFoundException from HRESULT ...

I have the same issue with CRYPT32.dll error on Linux when using Export-Clixml cmdlet. PS Version 6.0.0

Same here. Since we are discussing about portability of some scripts, it would be wonderful to know if we have to work around the problem or if something is going to be done on the pwsh or CoreFX side (the latter seems not).

@cenit Perhaps we'll use Windows Compatibility Pack

@iSazonov it's my understanding that SecureString depends on specific OS support which is not available on non-Windows and not part of the Windows Compatibility Pack.

We should provide a better error message even though this won't work.

Is there some possible alternative to the *-SecureString cmdlets on Unix if they will be removed?
Sorry if i missed it, give me a pointer on why it is not possible on unix. What is the missing "os-specific" part required on unix?
How else could one handle credentials to get them in the correct format and further use them?

I think this Issue on CoreFX sums it perfectly: https://github.com/dotnet/corefx/issues/22510
It also seems that no progress is being done, unfortunately.

thanks, that explains it very well.

@SteveL-MSFT Using WCP assume using common pattern if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { ... }
If some API is absent in WCP we could feedback in WCP repo. But even without it we can use the common pattern combined with #if !UNIX.

@iSazonov for this specific issue I don't believe WCP will solve this as the SecureString type is really an empty implementation on non-Windows.

I like it anyway. 馃槃

Ok, I'm editing this to make sure I got it straight...

  1. There was no SecureString implementation except on Windows.
  2. The PowerShell team mocked it, so they could avoid changing all their APIs that require it.
  3. Then the .NET team implemented it but only the short-term in-memory protection
  4. So trying to serialize a (in)SecureString crashes except on Windows, because the whole function is now Windows only, but is exposed everywhere...

Despite the early warning of this from 18 months ago

Despite the _extremely clear_ message from the .Net Framework team 6 months ago.

馃檮

The bottom line is that the .NET framework (and PowerShell) needs a cross-platform data protection library, because dev/ops need to store secrets didn't just suddenly disappear when we added new OSes to the mix, and it's not always practical to rely on web services like Azure KeyVault, RED Identity management, or Thycotic Secret Server. 馃槙

The .NET team is apparently not inclined to be particularly helpful here.

I know that ASP.NET wrote their own DataProtection stuff, but it's fairly weird and they recommend limiting it's use to specific scenarios...

What we need to know is:

Does the PowerShell team plan to create a cross-platform implementation of SecureString serialization?

If not, please remove the cmdlets that do not work at all, and provide a better error message for CliXML than the current, "oh darn, if only there was a Crypto dll available" error.

Check out related items:
NuGet/Home#1851
dotnet/corefx#6746

We could use ASP.NET DataProtection. This is the most reliable of what is available today. Especially as we need quite a bit.

I'm trying to read a password securely, to pass into a MySQL command line. What's the recommended alternative, so that I'm not echoing passwords to the console as I type them?

Repro Steps

$str = Read-Host -AsSecureString
ConvertFrom-SecureString -SecureString $str

Result

ConvertFrom-SecureString : Unable to load DLL 'CRYPT32.dll': The specified module or one of its dependencies could not be found.
 (Exception from HRESULT: 0x8007007E)
At line:1 char:1
+ ConvertFrom-SecureString -SecureString $str
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [ConvertFrom-SecureString], DllNotFoundException
+ FullyQualifiedErrorId : System.DllNotFoundException,Microsoft.PowerShell.Commands.ConvertFromSecureStringCommand

@pcgeek86 This is one solution to get the Plain text string from a securestring on linux:

$str = Read-Host -AsSecureString
$plaintext = [System.Net.NetworkCredential]::new('',$str).Password

I added another workaround in the description

Handling secrets on Linux seems to be quite a mess. There are many implementation efforts and outdated projects.

It seems Gnome is or was using the Secret Service API. libsecret seems to be a library to access secrets using the Secret Service bus.

Mac OS X allows to interact with the Keychain via the security command line utility or programmatically via the Keychain Services.

QtKeychain is an approach to create a platform independent password and secret manager for Linux (using libsecret), Mac OS X (using the Key Chain) and Windows (using the Windows Credential Store) and is probably closest to what is required for PowerShell. Could we use this as a starting point?

Until this gets fixed, here's code to securely pass a credential to a background job:


$credentialKey = New-Object 'byte[]' (256/8)
$rng = New-Object 'Security.Cryptography.RNGCryptoServiceProvider'
$rng.GetBytes($credentialKey)

$serializableCredential = [pscustomobject]@{ 
                                                UserName = $credential.UserName;
                                                Password = ConvertFrom-SecureString -SecureString $credential.Password -Key $credentialKey
                                            }

$job = Start-Job {
    param(
        [Parameter(Mandatory)]
        [byte[]]
        $Key
    )
    $serializedCredential = $using:serializableCredential

    $password = ConvertTo-SecureString -String $serializedCredential.Password -Key $Key
    $credential = New-Object 'PSCredential' ($serializedCredential.UserName,$password)
    [Array]::Clear($Key,0,$Key.Length)
} -ArgumentList (,$credentialKey) | Wait-Job | Receive-Job

[Array]::Clear($credentialKey,0,$credentialKey.Length)

Password = ConvertFrom-SecureString -SecureString $credential.Password -Key $credentialKey

how is this supposed to work if this has an issue itself?

anyways, tried your script though but got error:

ConvertFrom-SecureString : Cannot bind argument to parameter 'SecureString' because it is null.
At /home/myusername/powershell.ps1:7 char:99
+ ... = ConvertFrom-SecureString -SecureString $credential.Password -Key $c ...
+                                              ~~~~~~~~~~~~~~~~~~~~

so i tried to define username and password in a variable but:

ConvertFrom-SecureString : Cannot bind parameter 'SecureString'. Cannot convert the "testpassword" value of type "System.String" to type "System.Security.SecureString".

Why there is no separate issue for passing secure string over psremote? All opened issues closed as duplicates of this. In my opinion problem is different.
PSRemote hangs during key exchange due to lack of CryptoAPI implementation on Linux.
Who interested it hangs here https://github.com/PowerShell/PowerShell/blob/5ece96a37fc9bb5cda962b32741b00396ae0f135/src/System.Management.Automation/utils/CryptoUtils.cs#L1117
Btw, we can add exception handler showing message that securestring not supported yet ^ it's quite hard to realise it related to securestrings if it hangs like that.
I think psremoting can be fixed without fixing ConvertFrom-SecureString commandlet, because we don't need to store keys on machine for later use. We need only generate rsa 2048 key pair, crypt/decrypt using rsa, crypt decrypt using AES CBC crossplatform.

Some good news there is crossplatform workaround.
Workaround for PSRemoting
Use python fresh implementation of PSRP pypsrp it supports securestrings!

@KKomarov Please open new issue with repo steps and your suggestion.

@KKomarov the hang has been fixed in PSCore6.2-RC as part of https://github.com/PowerShell/PowerShell/issues/8723 already. The ability to actually send secure strings over for non-Windows should be a separate issue.

DE0001: SecureString shouldn't be used
https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md#de0001-securestring-shouldnt-be-used

@iSazonov

DE0001: SecureString shouldn't be used
https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md#de0001-securestring-shouldnt-be-used

While that's nice in a perfect ivory tower world, in Powershell we are constantly gluing things together and that requires authenticating in whatever format that application requires, be it rest API, legacy application that only supports Basic authenticaiton, etc. We can't just simply "use windows credentials or certificates" for everything as this recommendation states, that's a nice recommendation for developing a new app, but not what we use powershell for.

It's not like PSCredential is going anywhere which is an implementation of SecureString, so until we have something in .net core that can use a TPM to encrypt keys or something, we need a "good enough" option.

Something like using AES256 and having the encryption key be a 600 permission-ed file on the non-windows file system is a possible start, not much worse than using the Crypto API in Windows

I added the link for information only.

Essentially:

  1. It is impossible to port SecureString because System.Security.Cryptography.ProtectedData is Windows-only. There is no plans to port the API. Core team deprecate the API.
  2. We can keep backward compatibility for SecureString on Windows (including remoting)
  3. PowerShell Core must remain flexible and allow to work with legacy applications.
  4. It is acceptable to use basic authentication in protected environment
  5. Main problem how to detect protected environment vs public environment and what to do (prevent basic authentication, only warn, ...).

Early on, the @PowerShell/powershell-committee discussed introducing a SensitiveString to replace the functional need of SecureString even though both are not secure (the SecureString type would still be needed for backwards compat). A type (whether "Sensitive" or "Secure" is needed to indicate to PowerShell to prompt without echoing the input so it's used more than just for remoting. As for the original issue of this bug, this has been fixed (you don't get an error anymore), just keep in mind the SecureString is internally in plain text.

thanks for the update, looks promising!
may we ask for an approximately timeframe when to expect to be able to utilize this (e.g. in microsoft-debian-stretch-prod debian repository)?

As for the original issue of this bug, this has been fixed (you don't get an error anymore), just keep in mind the SecureString is internally in plain text.

Does anyone have a link to the fix or know what release version it will be available in? I'm still getting crypt32.dll errors in powershell_6.1.3-1.ubuntu.16.04_amd64.deb (same deal with the 6.2.0-rc.1 preview package).

I'm also curious how this fix affects Import/Export-CliXml when the data to be serialized contains SecureString or PSCredential objects.

@rmbolger Could you please check with latest build (6.2.0-RC)?

I'm seeing the same as @rmbolger

/home/hillr
03-20 23:44:55 31ms 11> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      6.2.0-rc.1
PSEdition                      Core
GitCommitId                    6.2.0-rc.1
OS                             Linux 4.4.0-17763-Microsoft #379-Microsoft Wed Mar 06 19:16:00 PST 2019
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

/home/hillr
03-21 00:47:45 35ms 12> ConvertFrom-SecureString -SecureString $ss
ConvertFrom-SecureString : Unable to load shared library 'CRYPT32.dll' or one of its dependencies. In order to help diagnose loading problems, consider setting the LD_DEBUG environment variable: libCRYPT32.dll: cannot open shared object file: No such file or directory
At line:1 char:1
+ ConvertFrom-SecureString -SecureString $ss
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [ConvertFrom-SecureString], DllNotFoundException
+ FullyQualifiedErrorId : System.DllNotFoundException,Microsoft.PowerShell.Commands.ConvertFromSecureStringCommand
Was this page helpful?
5 / 5 - 1 ratings