functions/sync.ps1


function Get-PropertyNames($obj) {
  # Measure-function "$($MyInvocation.MyCommand.Name)" {
        if ($obj -is [System.Collections.IDictionary]) {
            return $obj.keys
        }
        return $obj.psobject.Properties | select -ExpandProperty name
 # }
}

function ConvertTo-Hashtable([Parameter(ValueFromPipeline=$true)]$obj, [switch][bool]$recurse) {
 # Measure-function "$($MyInvocation.MyCommand.Name)" {

        $object =$obj
        if (!$recurse -and ($object -is [System.Collections.IDictionary] -or $object -is [array])) {
            return $object
        }
 
        if($object -is [array]) {
            if ($recurse) {
                for($i = 0; $i -lt $object.Length; $i++) {
                    $object[$i] = ConvertTo-Hashtable $object[$i] -recurse:$recurse
                }
            }
            return $object
        } 
        elseif ($object -is [System.Collections.IDictionary] -or  $object -is [System.Management.Automation.PSCustomObject] -or $true) {
            $h = @{}
            $props = get-propertynames $object
            foreach ($p in $props) {
                if ($recurse) {
                    $h[$p] = ConvertTo-Hashtable $object.$p -recurse:$recurse
                } else {
                    $h[$p] = $object.$p
                }
            }
            return $h
        } else {
            throw "could not convert object to hashtable"
            #return $object
        }
 # }
    
}


function Get-SyncDir {
    param($type = $null)

    if ($type -eq "onedrive") {
        if (test-path "HKCU:\Software\Microsoft\OneDrive") 
        {
            $prop = get-itemproperty "HKCU:\Software\Microsoft\OneDrive\" "UserFolder"
            if ($prop -ne $null) {
                $dir = $prop.userfolder
            }        
        }
    }
    elseif ($type -eq "local") {
        $dir = $env:USERPROFILE
    }
    elseif ($type -eq $null) {
        # try default locations. onedrive then local
        $syncdir = get-syncdir -type onedrive
        if ($syncdir -eq $null) {
            write-warning "couldn't find OneDrive synced folder. Using local storage - settings will not be synced across devices."
            $syncdir = get-syncdir -type local
        }
        return $syncdir
    }
    else {
        throw "unrecognized sync dir type: '$type'"
    }

    if ($dir -ne $null) {
        $syncdir = join-path $dir ".powershell-data"
        if (!(test-path $syncdir)) {
            $null = mkdir $syncdir
        }
        return $syncdir
    }
}


function Set-GlobalPassword {
    [CmdletBinding()]
    param(
        $container = "user-settings",
        [SecureString] $password = $null
    )

    if ($container -eq "user-settings") { $container = "global-key" }
    else { $container = "global-key-" + $container }
    if ($password -eq $null) {
        # TODO: hash password before storing it - make sure noone can retrieve plaintext password
        $c = Get-CredentialsCached -message "Global settings password" -reset -container $container
    } else {
        $c = new-credentials -username "any" -password $password
        Export-Credentials -container $container -cred $c
    }
}
function Remove-GlobalPassword {
    param($container = "user-settings")   
 
    if ($container -eq "user-settings") { $container = "global-key" }
    else { $container = "global-key-" + $container }

    Remove-CredentialsCached -container $container
}

function Update-GlobalPassword {
    [CmdletBinding()]
    param(
        $container = "user-settings",
        [SecureString] $password
        )

    $newpass = $password
    if ($newpass -eq $null) {
        import-module Microsoft.PowerShell.Security -verbose:$false
        $c = Microsoft.PowerShell.Security\Get-Credential -message "Global settings password" 
        $newpass = $c.password
    }
    # make sure current password is correct - stop on any warning
    $settings = Import-Settings -container $container -ErrorAction stop
    
    # reencrypt everything
    foreach($kvp in $settings.GetEnumerator()) {
        $key = $kvp.key
        if ($kvp.value -is [securestring]) {
            Export-Setting -container $container -key $key -securevalue $kvp.value -password $newpass -force        
        }         
    }

    Set-GlobalPassword -container $container -password $newpass
    $null = Import-Settings -container $container
}

function _getenckey { 
    [CmdletBinding()]
    param(
        [SecureString]$password,
        $container = "user-settings"
    ) 
    $pass = $password
    if ($container -eq "user-settings") { $container = "global-key" }
    else { $container = "global-key-" + $container }
    if ($pass -eq $null) {
        $pass = get-passwordcached -message "Global settings password" -container $container -allowuserui
    } 
    if ($pass -is [SecureString]) {$pass = ConvertTo-PlainText $pass }
    $rfc = new-object System.Security.Cryptography.Rfc2898DeriveBytes $pass,@(1,2,3,4,5,6,7,8),1000            
    $enckey = $rfc.GetBytes(256/8);
    #write-verbose "key=$($enckey | convertto-base64) length=$($enckey.length)"
    return $enckey
} 

function New-Credentials(
    [Parameter(Mandatory=$true)]$username, 
    [Parameter(Mandatory=$true)][SecureString]$password) {
        return New-Object 'system.management.automation.pscredential' $username,$password
    }

function ConvertTo-PlainText {
    param (
        [Parameter(Mandatory=$true,ValueFromPipeline=$true,Position=1)][securestring]$password
    )
    return (new-credentials $="dummy" $password).GetNetworkCredential().password
}

function Import-Settings {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)] $enckey = $null,
        [Parameter(Mandatory=$false)][SecureString] $password = $null,
        $container = "user-settings"
    )
    $syncdir = get-syncdir
    if ($syncdir -eq $null) { throw "couldn't determine settings home directory" } 
    
    $settings = import-cache -container $container -dir $syncdir | convertto-hashtable 
    
    
    if ($settings -eq $null) {
        $settings = @{}
    }

    $decrypted = @{}
    
    foreach($kvp in $settings.GetEnumerator()) {
        if ($kvp.value -eq $null) {
            # just skip nulls
            continue
        }
        if ($kvp.value.startswith("enc:")) {
            if ($enckey -eq $null) { $enckey = _getenckey -password $password -container $container }
            try {                
             $encvalue = $kvp.value.substring("enc:".length)
             $secvalue = convertto-securestring $encvalue -Key $enckey -ErrorAction stop
             $decrypted[$kvp.key] = $secvalue
             #$creds = new-object system.management.automation.pscredential ("dummy",$secvalue)
             #$pass = $creds.getnetworkcredential().password
            } catch {
                write-Error "failed to decode key $($kvp.key): $_"
                $decrypted[$kvp.key] = $kvp.value
            }
        }
        else {
            $decrypted[$kvp.key] = $kvp.value
        }
    }

    $settings = $decrypted
    write-verbose "imported $($settings.Count) settings from '$syncdir'"

    if ($container -eq "user-settings") {
        $global:settings = $settings
    }

    return $settings
}

function Export-Settings {
    [CmdletBinding(DefaultParameterSetName="plaintext")]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)] $settings, 
        $container = "user-settings"
    )
    $syncdir = get-syncdir
    export-cache -data $settings -container $container -dir $syncdir
}

function Export-Setting {
    [CmdletBinding(DefaultParameterSetName="plaintext")]
    param(
        [Parameter(Mandatory=$true)] $key, 
        [Parameter(Mandatory=$true,ParameterSetName="plaintext")] $value, 
        [Alias("secure")]
        [Parameter(Mandatory=$true,ParameterSetName="encrypted")][securestring] $securevalue, 
        [Parameter(Mandatory=$false,ParameterSetName="encrypted")] $enckey = $null,
        [Parameter(Mandatory=$false,ParameterSetName="encrypted")][SecureString] $password = $null,
        $container = "user-settings",
        [Switch][bool]$force
        ) 
    $syncdir = get-syncdir
    if ($syncdir -eq $null) { throw "couldn't determine settings home directory" } 
        
    $settings = import-cache -container $container -dir $syncdir | convertto-hashtable
    if ($settings -eq $null) { $settings = @{} }
    if ($settings[$key] -ne $null) {
        if (!$force) {
            write-warning "a setting with key $key already exists. Use -Force to override"
            return
        }
    }
    $encrypt = $securevalue -ne $null
    write-verbose "storing setting $key=$value at '$syncdir'"
    if ($encrypt) {
        if ($enckey -eq $null) { $enckey = _getenckey -password $password -container $container }
        $secvalue = $securevalue
        $encvalue = convertfrom-securestring $secvalue -key $enckey
        $settings[$key] = "enc:$encvalue"
    } else {
        $settings[$key] = "$value"
    }
    export-cache -data $settings -container $container -dir $syncdir
 
    if ($container -eq "user-settings") {
        # make sure settings are imported into global variable
        $null = import-settings -container $container -password $password 
    }
}