using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace Quorate.Oci; public sealed class RegistryAuth { internal RegistryAuth(string user, string password) { User = user; Password = password; } internal string User { get; } internal string Password { get; } public static RegistryAuth Load(string path, string registry) { ArgumentException.ThrowIfNullOrWhiteSpace(path); ArgumentException.ThrowIfNullOrWhiteSpace(registry); try { using FileStream stream = File.OpenRead(path); DockerConfigModel config = JsonSerializer.Deserialize(stream, OciJsonContext.Default.DockerConfigModel) ?? throw new InvalidDataException("Registry credential document is empty."); Dictionary auths = config.Auths ?? throw new InvalidDataException("Registry credential document has no auths."); if (!auths.TryGetValue(registry, out DockerAuthModel? entry) && !auths.TryGetValue($"https://{registry}", out entry)) { throw new InvalidDataException($"Registry credentials are unavailable for {registry}."); } string encoded = entry.Auth ?? throw new InvalidDataException("Registry credentials are invalid."); string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(encoded)); int separator = decoded.IndexOf(':', StringComparison.Ordinal); if (separator < 1) { throw new InvalidDataException("Registry credentials are invalid."); } return new RegistryAuth(decoded[..separator], decoded[(separator + 1)..]); } catch (Exception error) when (error is JsonException or FormatException) { throw new InvalidDataException("Registry credentials are invalid.", error); } } public override string ToString() => nameof(RegistryAuth); internal AuthenticationHeaderValue Basic() => new( "Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{User}:{Password}"))); } internal sealed record BearerChallenge(Uri Realm, string? Service, string? Scope) { public static BearerChallenge Parse(AuthenticationHeaderValue value) { if (!value.Scheme.Equals("Bearer", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(value.Parameter)) { throw new InvalidDataException("Registry bearer challenge is invalid."); } var values = new Dictionary(StringComparer.OrdinalIgnoreCase); int offset = 0; string parameter = value.Parameter; while (offset < parameter.Length) { while (offset < parameter.Length && (parameter[offset] == ',' || char.IsWhiteSpace(parameter[offset]))) { offset++; } int equals = parameter.IndexOf('=', offset); if (equals <= offset) { throw new InvalidDataException("Registry bearer challenge is invalid."); } string name = parameter[offset..equals].Trim(); offset = equals + 1; if (offset >= parameter.Length || parameter[offset] != '"') { throw new InvalidDataException("Registry bearer challenge is invalid."); } int end = parameter.IndexOf('"', ++offset); if (end < 0) { throw new InvalidDataException("Registry bearer challenge is invalid."); } values.Add(name, parameter[offset..end]); offset = end + 1; } if (!values.TryGetValue("realm", out string? realmValue) || !Uri.TryCreate(realmValue, UriKind.Absolute, out Uri? realm) || realm.Scheme != Uri.UriSchemeHttps) { throw new InvalidDataException("Registry token endpoint must use HTTPS."); } values.TryGetValue("service", out string? service); values.TryGetValue("scope", out string? scope); return new BearerChallenge(realm, service, scope); } }