OneDrive_utils.ps1

# Utility functions for OneDrive native client

# OneDrive settings class
class OneDriveSettings {
    [string]$Url
    [string]$AuthenticationCookie
    [string]$DefaultDocumentLibraryId
    [string]$DownloadUrlTemplate
    [int]$ItemCount
}

# Gets the authentication cookie for OneDrive native client
# Nov 26th 2019
function Get-ODAuthenticationCookie
{
<#
    .SYNOPSIS
    Gets authentication cookie for OneDrive

    .DESCRIPTION
    Gets authentication cookie for OneDrive native client

    .Parameter AccessToken
    AccessToken for OneDrive
    
    .Example
    Get-AADIntODAuthenticationCookie
#>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [String]$AccessToken
    )
    Process
    {
        # Get the tenant url
        $tenant = ((Read-Accesstoken $AccessToken).aud.Split("/"))[2]

        $url = "https://$tenant/_api/SP.OAuth.NativeClient/Authenticate?client-request-id=$((New-Guid).toString())"
        
        
        $headers=@{
                "Authorization" = "Bearer $AccessToken"
                "Accept"= "application/json;odata=verbose"
                "User-Agent"="Microsoft SkyDriveSync 19.192.0926.0012 ship; Windows NT 10.0 (17763)"
                "X-GeoMoveOptions" = "HttpRedirection"
                "X-IDCRL_ACCEPTED" ="t"
                "X-UserScenario"= "AUO,SignIn"
                

        }

        # Call the authentication API
        $response = Invoke-WebRequest -UseBasicParsing -uri $url -MaximumRedirection 0 -ErrorAction SilentlyContinue -Method Post -ContentType "application/x-www-form-urlencoded" -Headers $headers
        
        # Return the SPOIDCRL cookie
        ($response.headers["Set-Cookie"].split(";"))[0]

    }
}

# Invokes the OD API commands
# Nov 26th
function Invoke-ODCommand
{
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        $OneDriveSettings,
        [Parameter(Mandatory=$True)]
        [String]$Command,
        [Parameter(Mandatory=$False)]
        [String]$Accept="application/json;odata=verbose",
        [Parameter(Mandatory=$False)]
        [String]$Scenario="AUO,SignIn",
        [Parameter(Mandatory=$False)]
        [byte[]]$Body,
        [Parameter(Mandatory=$False)]
        $headers=@{},
        [Parameter(Mandatory=$False)]
        [Switch]$UseStreamReader,
        [Parameter(Mandatory=$False)]
        [PSObject][ref]$ResponseHeaders,
        [Parameter(Mandatory=$False)]
        [boolean]$Mac=$False

    )
    Process
    {
        # Set the headers
        $headers["Accept"] = $Accept

        if($MAC)
        {
            $headers["User-Agent"] = "Microsoft SkyDriveSync 20.169.0823.0006 ship; Mac OS X 10.15.7"
        }
        else
        {
            $headers["User-Agent"] = "Microsoft SkyDriveSync 19.192.0926.0012 ship; Windows NT 10.0 (17763)"
        }

        if(![string]::IsNullOrEmpty($Scenario))
        {
            $headers["X-UserScenario"] = $Scenario 
        }

        # Create a web session for the authentication cookie
        $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
        $webCookie = New-Object System.Net.Cookie
        $webCookie.Name = ($OneDriveSettings.AuthenticationCookie.Split("="))[0]
        $webCookie.Value = $OneDriveSettings.AuthenticationCookie.Substring($webCookie.Name.Length + 1)
        $webCookie.Domain = ($OneDriveSettings.Url.Split("/"))[2]
        $session.Cookies.Add($webCookie)

        # Create the url
        $url = $OneDriveSettings.Url
        $url += $Command

        # Call the API
        try
        {
            if($UseStreamReader)
            {
                if($Body -ne $null)
                {
                    $fullResponse = Invoke-WebRequest -UseBasicParsing -uri $url -Method Post -Headers $headers -WebSession $session -Body $Body
                }
                else
                {
                    $fullResponse = Invoke-WebRequest -UseBasicParsing -uri $url -Method Get -Headers $headers -WebSession $session
                }

                $response = [System.IO.StreamReader]::new($fullResponse.RawContentStream, [System.Text.Encoding]::UTF8).ReadToEnd()
                if($ResponseHeaders -ne $null)
                {
                    $ResponseHeaders.Value = $fullResponse.headers
                }
            }
            else
            {
                $response = Invoke-RestMethod -UseBasicParsing -uri $url -Method Get -Headers $headers -WebSession $session
            }
        }
        catch
        {
            if($_.Exception -like "*(501)*")
            {
                Write-Error "Got 501 - try using a -Mac switch or proper domain guid"
            }
            elseif($Body -ne $null -and $_.Exception -like "*(403)*" -and $ResponseHeaders -ne $null)
            {
                # This is part of the normal file upload flow
                $ResponseHeaders.Value = $_.Exception.Response.Headers
            }
            else
            {
                Write-Error $_.Exception
            }

            return
        }

        # Return
        $response 
    }
}


# Creates an OneDrive settings object to be used in OneDrive functions
function New-OneDriveSettings
{
<#
    .SYNOPSIS
    Creates a new OneDriveSettings object

    .DESCRIPTION
    Creates a new OneDriveSettings object used with OneDrive functions

    .Example
    $os = New-AADIntOneDriveSettings
    PS C:\> Get-AADIntOneDriveFiles -OneDriveSettings $os | Format-Table

    Path Size Created Modified ResourceID
    ---- ---- ------- -------- ----------
    \RootFolder\Document1.docx 11032 2.12.2019 20.47.23 2.12.2019 20.48.46 5e7acf393a2e45f18c1ce6caa7...
    \RootFolder\Book.xlsx 8388 2.12.2019 20.49.14 2.12.2019 20.50.14 b26c0a38d4d14b23b785576e29...
    \RootFolder\Docs\Document1.docx 84567 9.12.2019 11.24.40 9.12.2019 12.17.50 d9d51e47b66c4805aff3a08763...
    \RootFolder\Docs\Document2.docx 31145 7.12.2019 17.28.37 7.12.2019 17.28.37 972f9c317e1e468fb2b6080ac2...
#>

    [cmdletbinding()]
    Param(
        [Parameter(ParameterSetName='Credentials',Mandatory=$False)]
        [System.Management.Automation.PSCredential]$Credentials,
        [Parameter(ParameterSetName='SAML',Mandatory=$True)]
        [String]$SAMLToken,
        [Parameter(ParameterSetName='Kerberos',Mandatory=$True)]
        [String]$KerberosTicket,
        [Parameter(ParameterSetName='Kerberos',Mandatory=$True)]
        [String]$Domain
    )
    Process
    {
        # Create a new settings object
        $ODSettings=[OneDriveSettings]::new()

        # Get AccessToken for OfficeApps
        $OAtoken=Get-AccessToken -Resource "https://officeapps.live.com" -ClientId "ab9b8c07-8f02-4f72-87fa-80105867a763" -KerberosTicket $KerberosTicket -Domain $Domain -SAMLToken $SAMLToken -Credentials $Credentials -IncludeRefreshToken $true

        # Get the connection details
        $connections = Get-UserConnections -AccessToken $OAtoken[0]

        # Get the url
        foreach($connection in $connections)
        {
            if($connection.EnabledCapabilities -eq 2051) # Should be OneDrive
            {
                $url = $connection.ConnectionUrl
                # Strip the "/Documents" from the end of the url
                $ODSettings.Url = $url.Substring(0,$url.LastIndexOf("/"))
                break
            }
        }

        if([string]::IsNullOrEmpty($ODSettings.Url))
        {
            # The user doesn't have onedrive :(
            $upn = (Read-Accesstoken $OAtoken[0]).upn
            Write-Error "The user $upn doesn't have OneDrive :("
            return
        }

        # Get AccessToken for OneDrive
        $ODtoken=Get-AccessTokenWithRefreshToken -Resource "https://$(($ODSettings.Url.Split("/"))[2])" -ClientId "ab9b8c07-8f02-4f72-87fa-80105867a763" -RefreshToken $OAtoken[1] -TenantId ((Read-Accesstoken -AccessToken $OAtoken[0]).tid)

        # Get the authentication cookie
        $ODSettings.AuthenticationCookie = Get-ODAuthenticationCookie -AccessToken $ODtoken

        # Get the document library id
        $ODSettings.DefaultDocumentLibraryId = Get-ODDefaultDocLibId -OneDriveSettings $ODSettings

        # Get the sync policy
        $syncPolicy = Get-ODSyncPolicy -OneDriveSettings $ODSettings

        # Set the download url template
        $dlUrl = $syncPolicy.DownloadUrlTemplate
        $ODSettings.DownloadUrlTemplate = $dlUrl.Substring(0,$dlUrl.IndexOf("{"))

        # Set the ItemCount
        $ODSettings.ItemCount = [int]$syncPolicy.ItemCount

        # return
        $ODSettings
    }
}

# QuickXorHash by Microsoft https://docs.microsoft.com/en-us/onedrive/developer/code-snippets/quickxorhash
# Dec 9th 2019
$xorhash_code = @"
using System;

public class QuickXorHash : System.Security.Cryptography.HashAlgorithm
{
    private const int BitsInLastCell = 32;
    private const byte Shift = 11;
    private const int Threshold = 600;
    private const byte WidthInBits = 160;

    private UInt64[] _data;
    private Int64 _lengthSoFar;
    private int _shiftSoFar;

    public QuickXorHash()
    {
        this.Initialize();
    }

    protected override void HashCore(byte[] array, int ibStart, int cbSize)
    {
        unchecked
        {
            int currentShift = this._shiftSoFar;

            // The bitvector where we'll start xoring
            int vectorArrayIndex = currentShift / 64;

            // The position within the bit vector at which we begin xoring
            int vectorOffset = currentShift % 64;
            int iterations = Math.Min(cbSize, QuickXorHash.WidthInBits);

            for (int i = 0; i < iterations; i++)
            {
                bool isLastCell = vectorArrayIndex == this._data.Length - 1;
                int bitsInVectorCell = isLastCell ? QuickXorHash.BitsInLastCell : 64;

                // There's at least 2 bitvectors before we reach the end of the array
                if (vectorOffset <= bitsInVectorCell - 8)
                {
                    for (int j = ibStart + i; j < cbSize + ibStart; j += QuickXorHash.WidthInBits)
                    {
                        this._data[vectorArrayIndex] ^= (ulong)array[j] << vectorOffset;
                    }
                }
                else
                {
                    int index1 = vectorArrayIndex;
                    int index2 = isLastCell ? 0 : (vectorArrayIndex + 1);
                    byte low = (byte)(bitsInVectorCell - vectorOffset);

                    byte xoredByte = 0;
                    for (int j = ibStart + i; j < cbSize + ibStart; j += QuickXorHash.WidthInBits)
                    {
                        xoredByte ^= array[j];
                    }
                    this._data[index1] ^= (ulong)xoredByte << vectorOffset;
                    this._data[index2] ^= (ulong)xoredByte >> low;
                }
                vectorOffset += QuickXorHash.Shift;
                while (vectorOffset >= bitsInVectorCell)
                {
                    vectorArrayIndex = isLastCell ? 0 : vectorArrayIndex + 1;
                    vectorOffset -= bitsInVectorCell;
                }
            }

            // Update the starting position in a circular shift pattern
            this._shiftSoFar = (this._shiftSoFar + QuickXorHash.Shift * (cbSize % QuickXorHash.WidthInBits)) % QuickXorHash.WidthInBits;
        }

        this._lengthSoFar += cbSize;
    }

    protected override byte[] HashFinal()
    {
        // Create a byte array big enough to hold all our data
        byte[] rgb = new byte[(QuickXorHash.WidthInBits - 1) / 8 + 1];

        // Block copy all our bitvectors to this byte array
        for (Int32 i = 0; i < this._data.Length - 1; i++)
        {
            Buffer.BlockCopy(
                BitConverter.GetBytes(this._data[i]), 0,
                rgb, i * 8,
                8);
        }

        Buffer.BlockCopy(
            BitConverter.GetBytes(this._data[this._data.Length - 1]), 0,
            rgb, (this._data.Length - 1) * 8,
            rgb.Length - (this._data.Length - 1) * 8);

        // XOR the file length with the least significant bits
        // Note that GetBytes is architecture-dependent, so care should
        // be taken with porting. The expected value is 8-bytes in length in little-endian format
        var lengthBytes = BitConverter.GetBytes(this._lengthSoFar);
        System.Diagnostics.Debug.Assert(lengthBytes.Length == 8);
        for (int i = 0; i < lengthBytes.Length; i++)
        {
            rgb[(QuickXorHash.WidthInBits / 8) - lengthBytes.Length + i] ^= lengthBytes[i];
        }

        return rgb;
    }

    public override sealed void Initialize()
    {
        this._data = new ulong[(QuickXorHash.WidthInBits - 1) / 64 + 1];
        this._shiftSoFar = 0;
        this._lengthSoFar = 0;
    }

    public override int HashSize
    {
        get
        {
            return QuickXorHash.WidthInBits;
        }
    }
}
"@

Add-Type -TypeDefinition $xorhash_code -Language CSharp    
Remove-Variable $xorhash_code


# Calculates XorHash for OneDrive files
# Dec 9th 2019
function Get-XorHash
{
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [string]$FileName
    )
    Process
    {
        # Get the full path..
        $fullpath = (Get-Item $FileName).FullName

        # Create a stream to read bytes
        $stream = [System.IO.FileStream]::new($fullpath,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read)

        # Create the hash object and do the magic
        $xorhash = [quickxorhash]::new()
        $hash = $xorhash.ComputeHash($stream)
        $b64Hash = [convert]::ToBase64String($hash)   

        # Return
        $b64Hash
    }
}