1
0
Форкнуть 0

Merge pull request #655 from whoisj/vsts/secret-storage

[series 2/2] Improve credential key generation for Azure hosted repositories.
This commit is contained in:
J Wyman 2018-06-08 15:44:54 -04:00 коммит произвёл GitHub
Родитель 6c0efa61f4 56d1867488
Коммит f09ebdba40
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 386 добавлений и 72 удалений

Просмотреть файл

@ -20,13 +20,9 @@ using System.Runtime.InteropServices;
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("ee663736-5bad-4ca6-a4f8-99978925ad8a")]
// Version information for an assembly consists of the following four values:
//
// Major Version Minor Version Build Number Revision
//
// You can specify all the values or you can default the Build and Revision Numbers by using the '*'
// as shown below: [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("2.0.0.0")]
[assembly: AssemblyFileVersion("2.0.0.0")]
[assembly: AssemblyVersion("2.0.1.0")]
[assembly: AssemblyFileVersion("2.0.1.0")]
[assembly: InternalsVisibleTo("Bitbucket.Authentication.Test")]

Просмотреть файл

@ -153,9 +153,11 @@ namespace Microsoft.Alm.Authentication.Git
// Gloves off...
unsafe
{
var basicInfo = new Win32.ProcessBasicInformation { };
int bytesRead = 0;
long outResult = 0;
var basicInfo = new Win32.ProcessBasicInformation { };
// Ask the OS for information about the process, this will include the address of the PEB or
// Process Environment Block, which contains useful information (like the offset of the process' parameters).
var hresult = Win32.Ntdll.QueryInformationProcess(processHandle: processHandle,
@ -173,7 +175,6 @@ namespace Microsoft.Alm.Authentication.Git
return false;
}
int bytesRead = 0;
var peb = new Win32.ProcessEnvironmentBlock { };
// Now that we know the offsets of the process' parameters, read it because
@ -187,7 +188,7 @@ namespace Microsoft.Alm.Authentication.Git
{
var error = Win32.Kernel32.GetLastError();
Trace.WriteLine($"failed to read process environment block [{error}].");
Trace.WriteLine($"failed to read process environment block [{error}] ({bytesRead:n0} bytes read).");
return false;
}
@ -201,11 +202,11 @@ namespace Microsoft.Alm.Authentication.Git
buffer: &processParameters,
bufferSize: sizeof(Win32.PebProcessParameters),
bytesRead: out bytesRead)
|| bytesRead != sizeof(Win32.PebProcessParameters))
|| bytesRead < sizeof(Win32.PebProcessParameters))
{
var error = Win32.Kernel32.GetLastError();
Trace.WriteLine($"failed to read process parameters [{error}].");
Trace.WriteLine($"failed to read process parameters [{error}] ({bytesRead:n0} bytes read).");
return false;
}
@ -223,7 +224,7 @@ namespace Microsoft.Alm.Authentication.Git
{
var error = Win32.Kernel32.GetLastError();
Trace.WriteLine($"failed to read process image path [{error}].");
Trace.WriteLine($"failed to read process image path [{error}] ({bytesRead:n0} bytes read).");
}
else
{
@ -241,7 +242,7 @@ namespace Microsoft.Alm.Authentication.Git
{
var error = Win32.Kernel32.GetLastError();
Trace.WriteLine($"failed to read process command line [{error}].");
Trace.WriteLine($"failed to read process command line [{error}] ({bytesRead:n0} bytes read).");
}
else
{

Просмотреть файл

@ -13,8 +13,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("c1daabc1-b493-459d-bb4f-04fbefb1ba13")]
[assembly: AssemblyVersion("4.4.0.0")]
[assembly: AssemblyFileVersion("4.4.0.0")]
[assembly: AssemblyVersion("4.5.0.0")]
[assembly: AssemblyFileVersion("4.5.0.0")]
[assembly: NeutralResourcesLanguage("en-US")]
// Only expose internals when the binary isn't signed.

Просмотреть файл

@ -853,7 +853,7 @@ namespace Microsoft.Alm.Cli
if (operationArguments is null)
throw new ArgumentNullException(nameof(operationArguments));
if (program.Context.Utilities.TryReadGitRemoteHttpDetails(out string commandLine, out _))
if (program.Utilities.TryReadGitRemoteHttpDetails(out string commandLine, out _))
{
operationArguments.GitRemoteHttpCommandLine = commandLine;
}

Просмотреть файл

@ -416,6 +416,9 @@ namespace Microsoft.Alm.Cli
internal Git.ITrace Trace
=> _context.Trace;
internal Git.IUtilities Utilities
=> _context.Utilities;
internal Git.IWhere Where
=> _context.Where;
@ -471,22 +474,22 @@ namespace Microsoft.Alm.Cli
}
internal void Die(Exception exception,
[CallerFilePath] string path = "",
[CallerLineNumber] int line = 0,
[CallerMemberName] string name = "")
[CallerFilePath] string path = "",
[CallerLineNumber] int line = 0,
[CallerMemberName] string name = "")
=> _dieException(this, exception, path, line, name);
internal void Die(string message,
[CallerFilePath] string path = "",
[CallerLineNumber] int line = 0,
[CallerMemberName] string name = "")
[CallerFilePath] string path = "",
[CallerLineNumber] int line = 0,
[CallerMemberName] string name = "")
=> _dieMessage(this, message, path, line, name);
internal void Exit(int exitcode = 0,
string message = null,
[CallerFilePath] string path = "",
[CallerLineNumber] int line = 0,
[CallerMemberName] string name = "")
string message = null,
[CallerFilePath] string path = "",
[CallerLineNumber] int line = 0,
[CallerMemberName] string name = "")
=> _exit(this, exitcode, message, path, line, name);
internal Task LoadOperationArguments(OperationArguments operationArguments)

Просмотреть файл

@ -97,6 +97,32 @@ namespace Microsoft.Alm.Win32
[return: MarshalAs(UnmanagedType.U4)]
public static extern uint GetCurrentProcessId();
/// <summary>
/// Returns the process identifier of the specified process.
/// </summary>
/// <param name="processHandle">
/// A handle to the process.
/// <para/>
/// The handle must have the `<seealso cref="DesiredAccess.QueryInformation"/>` or `<seealso cref="DesiredAccess.QueryLimitedInformation"/>` access right.
/// </param>
[DllImport(Name, BestFitMapping = false, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, EntryPoint = "GetProcessId", ExactSpelling = true, SetLastError = true, ThrowOnUnmappableChar = true)]
[return: MarshalAs(UnmanagedType.U4)]
public static extern uint GetProcessId(
[In] SafeProcessHandle processHandle);
/// <summary>
/// Returns the process identifier of the specified process.
/// </summary>
/// <param name="processHandle">
/// A handle to the process.
/// <para/>
/// The handle must have the `<seealso cref="DesiredAccess.QueryInformation"/>` or `<seealso cref="DesiredAccess.QueryLimitedInformation"/>` access right.
/// </param>
[DllImport(Name, BestFitMapping = false, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, EntryPoint = "GetProcessId", ExactSpelling = true, SetLastError = true, ThrowOnUnmappableChar = true)]
[return: MarshalAs(UnmanagedType.U4)]
public static extern uint GetProcessId(
[In] IntPtr processHandle);
/// <summary>
/// Opens an existing local process object.
/// <para/>

Просмотреть файл

@ -299,16 +299,55 @@ namespace Microsoft.Alm.Win32
internal enum Hresult : uint
{
Ok = 0x00000000,
/// <summary>
/// Not implemented.
/// </summary>
NotImplemented = 0x80004001,
/// <summary>
/// No such interface supported.
/// </summary>
NoInterface = 0x80004002,
/// <summary>
/// Invalid pointer.
/// </summary>
InvalidPointer = 0x80004003,
/// <summary>
/// Operation aborted.
/// </summary>
Abort = 0x80004004,
/// <summary>
/// Unspecified error.
/// </summary>
Fail = 0x80004005,
/// <summary>
/// Catastrophic failure.
/// </summary>
Unexpected = 0x8000FFFF,
/// <summary>
/// General access denied error.
/// </summary>
AccessDenied = 0x80070005,
InvalidHandle = 0x80070006,
OutOfMemory = 0x8007000E,
/// <summary>
/// One or more arguments are invalid.
/// </summary>
InvalidArgument = 0x80070057,
/// <summary>
/// There is not enough space on the disk.
/// </summary>
DiskFull = 0x80070070,
}
/// <summary>
@ -369,56 +408,113 @@ namespace Microsoft.Alm.Win32
public string ExeFileName;
}
[StructLayout(LayoutKind.Explicit, Size = 0x30)]
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct ProcessEnvironmentBlock
{
[FieldOffset(0x02)]
private byte _offset_0x02;
[FieldOffset(0x20)]
private PebProcessParameters* _offset_0x20;
fixed byte _[256];
/// <summary>
/// Gets `<see langword="true"/> if the process is currently being debugged; otherwise `<see langword="false"/>`.
/// <para/>
/// It is best to use the `CheckRemoteDebuggerPresent` function instead.
/// </summary>
public bool IsBeingDebugged
{
get { return _offset_0x02 != 0; }
get { fixed (byte* p = _) { return p[2] != 0; } }
}
/// <summary>
/// Gets a pointer a `<seealso cref="PebProcessParameters"/>` structure.
/// </summary>
public PebProcessParameters* ProcessParameters
{
get { return _offset_0x20; }
get
{
fixed (byte* p = _)
{
return IntPtr.Size == 4
? *((PebProcessParameters**)(p + 0x10))
: *((PebProcessParameters**)(p + 0x20));
}
}
}
}
[StructLayout(LayoutKind.Explicit, Size = 104)]
[StructLayout(LayoutKind.Sequential, Size = 128)]
internal unsafe struct PebProcessParameters
{
[FieldOffset(0x60)]
private UnicodeString _offset_0x60;
[FieldOffset(0x70)]
private UnicodeString _offset_0x70;
fixed byte _[128];
/// <summary>
/// Gets the command-line string passed to the process.
/// </summary>
public UnicodeString CommandLine
{
get { return _offset_0x70; }
get
{
fixed (byte* p = _)
{
return IntPtr.Size == 4
? *((UnicodeString*)(p + 0x40))
: *((UnicodeString*)(p + 0x70));
}
}
}
/// <summary>
/// Gets the path of the image file for the process.
/// </summary>
public UnicodeString ImagePathName
{
get { return _offset_0x60; }
get
{
fixed (byte* p = _)
{
return IntPtr.Size == 4
? *((UnicodeString*)(p + 0x38))
: *((UnicodeString*)(p + 0x60));
}
}
}
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct ProcessBasicInformation
{
private IntPtr _exitStatus;
private IntPtr _pebBaseAddress;
private IntPtr _affinityMask;
private IntPtr _basePriority;
private UIntPtr _uniqueProcessId;
private IntPtr _inheritedFromUniqueProcessId;
fixed byte _[32];
public byte* ProcessEnvironmentBlock
/// <summary>
/// Gets the address of the `<seealso cref="ProcessEnvironmentBlock"/>`.
/// </summary>
public void* ProcessEnvironmentBlock
{
get { return (byte*)_pebBaseAddress.ToPointer(); }
get
{
fixed (byte* p = _)
{
return IntPtr.Size == 4
? *((void**)(p + 0x04))
: *((void**)(p + 0x08));
}
}
}
/// <summary>
/// Gets a pointer to the system's unique identifier for this process.
/// <para/>
/// Use the `<seealso cref="Kernel32.GetCurrentProcessId(IntPtr)"/>` function to retrieve this information.
/// </summary>
public uint* UniqueProcessId
{
get
{
fixed (byte* p = _)
{
return IntPtr.Size == 4
? *((uint**)(p + 0x10))
: *((uint**)(p + 0x20));
}
}
}
}
@ -504,22 +600,26 @@ namespace Microsoft.Alm.Win32
/// <summary>
/// Represents a Unicode encoded string.
/// </summary>
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[StructLayout(LayoutKind.Sequential)]
[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay, nq}")]
internal unsafe struct UnicodeString
{
[FieldOffset(0x00)]
private ushort _field1;
[FieldOffset(0x02)]
private ushort _field2;
[FieldOffset(0x08)]
private IntPtr _field3;
fixed byte _[16];
/// <summary>
/// Gets the pointer to the character data buffer.
/// </summary>
public char* Buffer
{
get { return (char*)_field3.ToPointer(); }
get
{
fixed (byte* p = _)
{
return (IntPtr.Size == 4)
? *((char**)(p + 0x04))
: *((char**)(p + 0x08));
}
}
}
/// <summary>
@ -527,7 +627,7 @@ namespace Microsoft.Alm.Win32
/// </summary>
public int Length
{
get { return _field1 / sizeof(char); }
get { fixed (byte* p = _) { return *((ushort*)(p + 0x00)) / sizeof(char); } }
}
/// <summary>
@ -535,7 +635,23 @@ namespace Microsoft.Alm.Win32
/// </summary>
public int MaximumSize
{
get { return _field2; }
get { fixed (byte* p = _) { return *((ushort*)(p + 0x02)); } }
}
internal string DebuggerDisplay
{
get { return $"{nameof(UnicodeString)}: \"{ToString() ?? "<NULL>"}\""; }
}
public override string ToString()
{
if (Buffer == null)
return null;
if (Length == 0)
return string.Empty;
return new string(Buffer);
}
}
}

Просмотреть файл

@ -13,8 +13,8 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("c1daabc1-b493-459d-bb4f-04fbefb1ba13")]
[assembly: AssemblyVersion("4.4.0.0")]
[assembly: AssemblyFileVersion("4.4.0.0")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: NeutralResourcesLanguage("en-US")]
// Only expose internals when the binary isn't signed.

Просмотреть файл

@ -30,6 +30,14 @@ namespace VisualStudioTeamServices.Authentication.Test
{
public abstract class AuthenticationTests
{
protected AuthenticationTests()
{
if (Trace.Listeners.Count == 0)
{
Trace.Listeners.AddRange(Debug.Listeners);
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
protected static readonly Credential DefaultCredentials = new Credential("username", "password");
@ -50,13 +58,5 @@ namespace VisualStudioTeamServices.Authentication.Test
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
protected static readonly TokenScope DefaultTokenScope = TokenScope.CodeWrite;
protected AuthenticationTests()
{
if (Trace.Listeners.Count == 0)
{
Trace.Listeners.AddRange(Debug.Listeners);
}
}
}
}

Просмотреть файл

@ -0,0 +1,171 @@
/**** Git Credential Manager for Windows ****
*
* Copyright (c) Microsoft Corporation
* All rights reserved.
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the """"Software""""), to deal
* in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
**/
using System;
using System.Collections.Generic;
using Microsoft.Alm.Authentication;
using Xunit;
namespace VisualStudioTeamServices.Authentication.Test
{
public class AuthorityTests
{
public static object[][] GetSecretKeyData
{
get
{
const string Org = "organization";
const string Path = "project/_git/repository";
const string Username = "user";
const string VstsUrl = "visualstudio.com";
const string CodexUrl = "codex.azure.com";
var boolean = new[] { false, true };
var hosts = new[] { CodexUrl, VstsUrl, };
var data = new List<object[]>();
foreach (string host in hosts)
{
foreach (bool defaultPort in boolean)
{
foreach (bool hasUsername in boolean)
{
foreach (bool hasRemoteUrl in boolean)
{
foreach (bool hasFullPath in boolean)
{
var targetBuilder = new UriBuilder()
{
// VSTS uses {organization}.{host} where as Codex does not.
Host = host.Equals(VstsUrl) ? Org + '.' + host : host,
Scheme = Uri.UriSchemeHttps,
};
var expectedBuilder = new UriBuilder()
{
// VSTS uses {organization}.{host} where as Codex does not.
Host = host.Equals(VstsUrl) ? Org + '.' + host : host,
Scheme = Uri.UriSchemeHttps,
};
var remoteUrlBuilder = new UriBuilder()
{
// VSTS uses {organization}.{host} where as Codex does not.
Host = host.Equals(VstsUrl) ? Org + '.' + host : host,
// Codex places {organization} first on path, VSTS does not.
Path = host.Equals(CodexUrl) ? Org + '/' : "/",
Scheme = Uri.UriSchemeHttps,
};
// If the URL has a non-default port, set it.
if (!defaultPort)
{
targetBuilder.Port = 8080;
expectedBuilder.Port = 8080;
remoteUrlBuilder.Port = 8080;
}
// If the URL includes a username, include it.
if (hasUsername)
{
// Append username to target
targetBuilder.UserName = Username;
// Append username to expected
expectedBuilder.UserName = Username;
// Handle Codex oddities
if (host.Equals(CodexUrl))
{
// If there's not a remote URL, then the username becomes the path of the expected and remote values.
if (!hasRemoteUrl)
{
expectedBuilder.Path = Username + '/';
remoteUrlBuilder.Path = Username + '/';
}
}
// The remote URL is the superset, so append the username
remoteUrlBuilder.UserName = Username;
}
// Handle Codex oddities...
else if (host.Equals(CodexUrl))
{
// If there's no remote URL, then Org needs to be the target's user-info;
// and the expected's URL as well.
if (!hasRemoteUrl)
{
targetBuilder.UserName = Org;
expectedBuilder.UserName = Org;
expectedBuilder.Path = Org + '/';
}
}
// Codex places the organization info in the path.
if (hasRemoteUrl && host.Equals(CodexUrl))
{
expectedBuilder.Path = Org + '/';
}
// Append the full path to both target and expected
if (hasFullPath)
{
targetBuilder.Path = Org + '/' + Path;
expectedBuilder.Path = Org + '/' + Path;
}
// The remote URL is the superset, so append the path.
remoteUrlBuilder.Path += Path;
// If the test doesn't contain a remote URL, set it to null.
remoteUrlBuilder = hasRemoteUrl
? remoteUrlBuilder
: null;
data.Add(new object[]
{
targetBuilder.ToString(),
remoteUrlBuilder?.ToString(),
"git:" + expectedBuilder.ToString().Trim('/', '\\'),
});
}
}
}
}
}
return data.ToArray();
}
}
[Theory, MemberData(nameof(GetSecretKeyData))]
public void GetSecretKey(string targetUrl, string actualUrl, string expectedKey)
{
var targetUri = new TargetUri(targetUrl, null, actualUrl);
string actualKey = Authentication.GetSecretKey(targetUri, "git");
Assert.Equal(expectedKey, actualKey, StringComparer.Ordinal);
}
}
}

Просмотреть файл

@ -52,11 +52,12 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="AuthenticationTests.cs" />
<Compile Include="AuthorityFake.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="AadTests.cs" />
<Compile Include="AuthorityFake.cs" />
<Compile Include="AuthenticationTests.cs" />
<Compile Include="AuthorityTests.cs" />
<Compile Include="MsaTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TokenScopeTests.cs" />
</ItemGroup>
<ItemGroup>