diff --git a/dracut/30ignition/ignition-fetch.service b/dracut/30ignition/ignition-fetch.service
index fb36f1af7..489177320 100644
--- a/dracut/30ignition/ignition-fetch.service
+++ b/dracut/30ignition/ignition-fetch.service
@@ -25,4 +25,4 @@ After=network.target
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=/run/ignition.env
-ExecStart=/usr/bin/ignition --root=/sysroot --platform=${PLATFORM_ID} --stage=fetch ${IGNITION_ARGS}
+ExecStart=/usr/bin/ignition --root=/sysroot --platform=${PLATFORM_ID} --stage=fetch --generate-cloud-config=true ${IGNITION_ARGS}
diff --git a/go.mod b/go.mod
index 712e4a75b..f352433c4 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
cloud.google.com/go/storage v1.58.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3
+ github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/aws/aws-sdk-go-v2 v1.41.0
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
diff --git a/go.sum b/go.sum
index 8640313b2..050d61f9f 100644
--- a/go.sum
+++ b/go.sum
@@ -36,6 +36,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
+github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
+github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
diff --git a/internal/exec/engine.go b/internal/exec/engine.go
index c2e61f7eb..f172f12ad 100644
--- a/internal/exec/engine.go
+++ b/internal/exec/engine.go
@@ -55,14 +55,15 @@ var (
// Engine represents the entity that fetches and executes a configuration.
type Engine struct {
- ConfigCache string
- FetchTimeout time.Duration
- Logger *log.Logger
- NeedNet string
- Root string
- PlatformConfig platform.Config
- Fetcher *resource.Fetcher
- State *state.State
+ ConfigCache string
+ FetchTimeout time.Duration
+ GenerateCloudConfig bool
+ Logger *log.Logger
+ NeedNet string
+ Root string
+ PlatformConfig platform.Config
+ Fetcher *resource.Fetcher
+ State *state.State
}
// Run executes the stage of the given name. It returns true if the stage
@@ -286,6 +287,10 @@ func (e *Engine) acquireProviderConfig() (cfg types.Config, err error) {
// is unavailable. This will also render the config (see renderConfig) before
// returning.
func (e *Engine) fetchProviderConfig() (types.Config, error) {
+ if e.GenerateCloudConfig {
+ return e.fetchGeneratedConfig()
+ }
+
platformConfigs := []platform.Config{
cmdline.Config,
system.Config,
@@ -331,6 +336,28 @@ func (e *Engine) fetchProviderConfig() (types.Config, error) {
return configFetcher.RenderConfig(cfg)
}
+func (e *Engine) fetchGeneratedConfig() (types.Config, error) {
+ e.Logger.Info("using generated cloud config for platform %q", e.PlatformConfig.Name())
+ cfg, err := e.PlatformConfig.GenerateConfig(e.Fetcher)
+ if err != nil {
+ return types.Config{}, err
+ }
+
+ e.State.FetchedConfigs = append(e.State.FetchedConfigs, state.FetchedConfig{
+ Kind: "user",
+ Source: fmt.Sprintf("%s-generator", e.PlatformConfig.Name()),
+ Referenced: false,
+ })
+
+ configFetcher := ConfigFetcher{
+ Logger: e.Logger,
+ Fetcher: e.Fetcher,
+ State: e.State,
+ }
+
+ return configFetcher.RenderConfig(cfg)
+}
+
func (e *Engine) signalNeedNet() error {
if err := executil.MkdirForFile(e.NeedNet); err != nil {
return err
diff --git a/internal/main.go b/internal/main.go
index 4d636a399..41139c197 100644
--- a/internal/main.go
+++ b/internal/main.go
@@ -48,20 +48,22 @@ func main() {
func ignitionMain() {
flags := struct {
- configCache string
- fetchTimeout time.Duration
- needNet string
- platform platform.Name
- root string
- stage stages.Name
- stateFile string
- version bool
- logToStdout bool
+ configCache string
+ fetchTimeout time.Duration
+ generateCloudConfig bool
+ needNet string
+ platform platform.Name
+ root string
+ stage stages.Name
+ stateFile string
+ version bool
+ logToStdout bool
}{}
flag.StringVar(&flags.configCache, "config-cache", "/run/ignition.json", "where to cache the config")
flag.DurationVar(&flags.fetchTimeout, "fetch-timeout", exec.DefaultFetchTimeout, "initial duration for which to wait for config")
flag.StringVar(&flags.needNet, "neednet", "/run/ignition/neednet", "flag file to write from fetch-offline if networking is needed")
+ flag.BoolVar(&flags.generateCloudConfig, "generate-cloud-config", false, "generate config from cloud provider metadata instead of fetching")
flag.Var(&flags.platform, "platform", fmt.Sprintf("current platform. %v", platform.Names()))
flag.StringVar(&flags.root, "root", "/", "root of the filesystem")
flag.Var(&flags.stage, "stage", fmt.Sprintf("execution stage. %v", stages.Names()))
@@ -71,6 +73,11 @@ func ignitionMain() {
flag.Parse()
+ // Never allow cloud config generation during fetch-offline stage (no networking)
+ if flags.stage == "fetch" && flags.platform == "azure" {
+ flags.generateCloudConfig = true
+ }
+
if flags.version {
fmt.Printf("%s\n", version.String)
return
@@ -91,6 +98,8 @@ func ignitionMain() {
logger.Info("%s", version.String)
logger.Info("Stage: %v", flags.stage)
+ logger.Info("Platform: %v", flags.platform)
+ logger.Info("GenerateCloudConfig: %v", flags.generateCloudConfig)
platformConfig := platform.MustGet(flags.platform.String())
fetcher, err := platformConfig.NewFetcher(&logger)
@@ -104,14 +113,15 @@ func ignitionMain() {
os.Exit(3)
}
engine := exec.Engine{
- Root: flags.root,
- FetchTimeout: flags.fetchTimeout,
- Logger: &logger,
- NeedNet: flags.needNet,
- ConfigCache: flags.configCache,
- PlatformConfig: platformConfig,
- Fetcher: &fetcher,
- State: &state,
+ Root: flags.root,
+ FetchTimeout: flags.fetchTimeout,
+ GenerateCloudConfig: flags.generateCloudConfig,
+ Logger: &logger,
+ NeedNet: flags.needNet,
+ ConfigCache: flags.configCache,
+ PlatformConfig: platformConfig,
+ Fetcher: &fetcher,
+ State: &state,
}
err = engine.Run(flags.stage.String())
diff --git a/internal/platform/platform.go b/internal/platform/platform.go
index 9b01df0cc..74c472720 100644
--- a/internal/platform/platform.go
+++ b/internal/platform/platform.go
@@ -49,6 +49,9 @@ type Provider struct {
Status func(stageName string, f resource.Fetcher, e error) error
DelConfig func(f *resource.Fetcher) error
+ // Generates a platform-specific Ignition config from cloud provider metadata.
+ GenerateCloudConfig func(f *resource.Fetcher) (types.Config, error)
+
// Fetch, and also save output files to be written during files stage.
// Avoid, unless you're certain you need it.
FetchWithFiles func(f *resource.Fetcher) ([]types.File, types.Config, report.Report, error)
@@ -104,6 +107,13 @@ func (c Config) DelConfig(f *resource.Fetcher) error {
}
}
+func (c Config) GenerateConfig(f *resource.Fetcher) (types.Config, error) {
+ if c.p.GenerateCloudConfig != nil {
+ return c.p.GenerateCloudConfig(f)
+ }
+ return types.Config{}, ErrNoProvider
+}
+
var configs = registry.Create("platform configs")
func Register(provider Provider) {
diff --git a/internal/providers/azure/azure.go b/internal/providers/azure/azure.go
index 09de9591f..7286e09cd 100644
--- a/internal/providers/azure/azure.go
+++ b/internal/providers/azure/azure.go
@@ -18,21 +18,28 @@ package azure
import (
"encoding/base64"
+ "encoding/json"
+ "encoding/xml"
+ "errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
+ "strconv"
+ "strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
- "github.com/coreos/ignition/v2/config/shared/errors"
+ configErrors "github.com/coreos/ignition/v2/config/shared/errors"
+ cfgutil "github.com/coreos/ignition/v2/config/util"
"github.com/coreos/ignition/v2/config/v3_6_experimental/types"
execUtil "github.com/coreos/ignition/v2/internal/exec/util"
"github.com/coreos/ignition/v2/internal/log"
"github.com/coreos/ignition/v2/internal/platform"
"github.com/coreos/ignition/v2/internal/providers/util"
"github.com/coreos/ignition/v2/internal/resource"
+ "github.com/vincent-petithory/dataurl"
"github.com/coreos/vcontext/report"
"golang.org/x/sys/unix"
@@ -68,13 +75,32 @@ var (
Path: "metadata/instance/compute/userData",
RawQuery: "api-version=2021-01-01&format=text",
}
+
+ imdsInstanceURL = url.URL{
+ Scheme: "http",
+ Host: "169.254.169.254",
+ Path: "metadata/instance",
+ RawQuery: "api-version=2021-03-01&format=json&extended=true",
+ }
+)
+
+var imdsRetryCodes = []int{
+ 404,
+ 410,
+ 429,
+}
+
+var (
+ fetchInstanceMetadataFunc = fetchInstanceMetadata
+ readOvfEnvironmentFunc = readOvfEnvironment
)
func init() {
platform.Register(platform.Provider{
- Name: "azure",
- NewFetcher: newFetcher,
- Fetch: fetchConfig,
+ Name: "azure",
+ NewFetcher: newFetcher,
+ Fetch: fetchConfig,
+ GenerateCloudConfig: generateCloudConfig,
})
}
@@ -110,23 +136,38 @@ func fetchFromAzureMetadata(f *resource.Fetcher) (types.Config, report.Report, e
logger := f.Logger
- // we first try to fetch config from IMDS, in case of failure we fallback on the custom data.
- userData, err := fetchFromIMDS(f)
- if err == nil {
- logger.Info("config has been read from IMDS userdata")
- return util.ParseConfig(logger, userData)
- }
-
- if err != errors.ErrEmpty {
- return types.Config{}, report.Report{}, err
+ // We first try to fetch config from IMDS. If it fails, retry a few times
+ // before falling back to custom data from the OVF media.
+ logger.Info("azure: attempting to read userdata from IMDS")
+ const maxUserdataRetries = 10
+ const userdataRetryDelay = 2 * time.Second
+
+ var userData []byte
+ var err error
+ for attempt := 0; attempt < maxUserdataRetries; attempt++ {
+ userData, err = fetchFromIMDS(f)
+ if err == nil {
+ logger.Info("config has been read from IMDS userdata")
+ return util.ParseConfig(logger, userData)
+ }
+ if errors.Is(err, configErrors.ErrEmpty) {
+ logger.Info("azure: IMDS userdata was empty, falling back to custom data")
+ break
+ }
+ if attempt < maxUserdataRetries-1 {
+ logger.Info("azure: IMDS userdata request failed, retrying in %v (attempt %d/%d): %v", userdataRetryDelay, attempt+1, maxUserdataRetries, err)
+ time.Sleep(userdataRetryDelay)
+ continue
+ }
+ logger.Warning("azure: IMDS userdata request failed after %d attempts, falling back to custom data: %v", maxUserdataRetries, err)
}
- logger.Debug("failed to retrieve userdata from IMDS, falling back to custom data: %v", err)
return FetchFromOvfDevice(f, []string{CDS_FSTYPE_UDF})
}
// fetchFromIMDS requests the Azure IMDS to fetch userdata and decode it.
func fetchFromIMDS(f *resource.Fetcher) ([]byte, error) {
+ logger := f.Logger
headers := make(http.Header)
headers.Set("Metadata", "true")
@@ -134,12 +175,9 @@ func fetchFromIMDS(f *resource.Fetcher) ([]byte, error) {
// Here, we match the cloud-init set.
// https://github.com/canonical/cloud-init/commit/c1a2047cf291
// https://github.com/coreos/ignition/issues/1806
- retryCodes := []int{
- 404, // not found
- 410, // gone
- 429, // rate-limited
- }
- data, err := f.FetchToBuffer(imdsUserdataURL, resource.FetchOptions{Headers: headers, RetryCodes: retryCodes})
+ logger.Debug("azure: requesting IMDS userdata from %s", imdsUserdataURL.String())
+ data, err := f.FetchToBuffer(imdsUserdataURL, resource.FetchOptions{Headers: headers, RetryCodes: imdsRetryCodes})
+
if err != nil {
return nil, fmt.Errorf("fetching to buffer: %w", err)
}
@@ -147,7 +185,7 @@ func fetchFromIMDS(f *resource.Fetcher) ([]byte, error) {
n := len(data)
if n == 0 {
- return nil, errors.ErrEmpty
+ return nil, configErrors.ErrEmpty
}
// data is base64 encoded by the IMDS
@@ -200,42 +238,8 @@ func FetchFromOvfDevice(f *resource.Fetcher, ovfFsTypes []string) (types.Config,
// getRawConfig returns the config by mounting the given block device
func getRawConfig(f *resource.Fetcher, devicePath string, fstype string) ([]byte, error) {
logger := f.Logger
- mnt, err := os.MkdirTemp("", "ignition-azure")
- if err != nil {
- return nil, fmt.Errorf("failed to create temp directory: %v", err)
- }
- defer func() {
- if removeErr := os.Remove(mnt); removeErr != nil {
- logger.Warning("failed to remove temp directory %q: %v", mnt, removeErr)
- }
- }()
-
- logger.Debug("mounting config device")
- if err := logger.LogOp(
- func() error { return unix.Mount(devicePath, mnt, fstype, unix.MS_RDONLY, "") },
- "mounting %q at %q", devicePath, mnt,
- ); err != nil {
- return nil, fmt.Errorf("failed to mount device %q at %q: %v", devicePath, mnt, err)
- }
- defer func() {
- _ = logger.LogOp(
- func() error { return unix.Unmount(mnt, 0) },
- "unmounting %q at %q", devicePath, mnt,
- )
- }()
-
- // detect the config drive by looking for a file which is always present
- logger.Debug("checking for config drive")
- if _, err := os.Stat(filepath.Join(mnt, "ovf-env.xml")); err != nil {
- return nil, fmt.Errorf("device %q does not appear to be a config drive: %v", devicePath, err)
- }
-
logger.Debug("reading config")
- rawConfig, err := os.ReadFile(filepath.Join(mnt, configPath))
- if err != nil && !os.IsNotExist(err) {
- return nil, fmt.Errorf("failed to read config from device %q: %v", devicePath, err)
- }
- return rawConfig, nil
+ return readFileFromDevice(f, devicePath, fstype, configPath)
}
// isCdromPresent verifies if the given config drive is CD-ROM
@@ -275,3 +279,403 @@ func isCdromPresent(logger *log.Logger, devicePath string) bool {
return (status == CDS_DISC_OK)
}
+
+func readFileFromDevice(f *resource.Fetcher, devicePath string, fstype string, relativePath string) ([]byte, error) {
+ logger := f.Logger
+ mnt, err := os.MkdirTemp("", "ignition-azure")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create temp directory: %v", err)
+ }
+ defer func() {
+ if removeErr := os.Remove(mnt); removeErr != nil {
+ logger.Warning("failed to remove temp directory %q: %v", mnt, removeErr)
+ }
+ }()
+
+ logger.Debug("mounting config device")
+ if err := logger.LogOp(
+ func() error { return unix.Mount(devicePath, mnt, fstype, unix.MS_RDONLY, "") },
+ "mounting %q at %q", devicePath, mnt,
+ ); err != nil {
+ return nil, fmt.Errorf("failed to mount device %q at %q: %v", devicePath, mnt, err)
+ }
+ defer func() {
+ _ = logger.LogOp(
+ func() error { return unix.Unmount(mnt, 0) },
+ "unmounting %q at %q", devicePath, mnt,
+ )
+ }()
+
+ logger.Debug("checking for config drive")
+ if _, err := os.Stat(filepath.Join(mnt, "ovf-env.xml")); err != nil {
+ return nil, fmt.Errorf("device %q does not appear to be a config drive: %v", devicePath, err)
+ }
+
+ target := filepath.Join(mnt, strings.TrimPrefix(relativePath, "/"))
+ data, err := os.ReadFile(target)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+ return nil, fmt.Errorf("failed to read %q from device %q: %v", relativePath, devicePath, err)
+ }
+ return data, nil
+}
+
+type instanceMetadata struct {
+ Compute instanceComputeMetadata `json:"compute"`
+}
+
+type instanceComputeMetadata struct {
+ Hostname string `json:"hostname"`
+ OSProfile instanceOSProfile `json:"osProfile"`
+ PublicKeys []instancePublicKey `json:"publicKeys"`
+}
+
+type instanceOSProfile struct {
+ AdminUsername string `json:"adminUsername"`
+}
+
+type instancePublicKey struct {
+ KeyData string `json:"keyData"`
+}
+
+type provisioningEnvelope struct {
+ LinuxProvisioningConfigurationSet linuxProvisioningConfigurationSet `xml:"LinuxProvisioningConfigurationSet"`
+}
+
+type linuxProvisioningConfigurationSet struct {
+ HostName string `xml:"HostName"`
+ UserName string `xml:"UserName"`
+ UserPassword string `xml:"UserPassword"`
+ DisableSshPasswordAuthentication string `xml:"DisableSshPasswordAuthentication"`
+ SSH sshSection `xml:"SSH"`
+ CustomData string `xml:"CustomData"`
+ UserData string `xml:"UserData"`
+}
+
+type sshSection struct {
+ PublicKeys []sshPublicKey `xml:"PublicKeys>PublicKey"`
+}
+
+type sshPublicKey struct {
+ Value string `xml:"Value"`
+}
+
+func (l linuxProvisioningConfigurationSet) passwordAuthDisabled() bool {
+ val := strings.ToLower(strings.TrimSpace(l.DisableSshPasswordAuthentication))
+ switch val {
+ case "true", "1", "yes":
+ return true
+ case "false", "0", "no", "":
+ return false
+ default:
+ // Try parsing as bool for any other values
+ disabled, err := strconv.ParseBool(val)
+ if err != nil {
+ return false
+ }
+ return disabled
+ }
+}
+
+func generateCloudConfig(f *resource.Fetcher) (types.Config, error) {
+ logger := f.Logger
+ logger.Info("azure: [1/4] generating cloud config via IMDS + OVF metadata")
+ logger.Info("azure: [2/4] requesting instance metadata from IMDS")
+ meta, metaErr := fetchInstanceMetadataFunc(f)
+ if metaErr != nil {
+ metaErr = fmt.Errorf("fetching instance metadata: %w", metaErr)
+ logger.Warning("azure: failed to fetch instance metadata from IMDS: %v", metaErr)
+ meta = nil
+ } else {
+ logger.Info("azure: fetched instance metadata from IMDS: %+v", meta)
+ }
+
+ logger.Info("azure: [3/4] reading OVF provisioning metadata from attached media")
+ ovfRaw, err := readOvfEnvironmentFunc(f, []string{CDS_FSTYPE_UDF})
+ var provisioningErr error
+ if err != nil {
+ provisioningErr = fmt.Errorf("reading OVF provisioning metadata: %w", err)
+ logger.Warning("azure: failed to read OVF provisioning metadata: %v", provisioningErr)
+ ovfRaw = nil
+ } else if len(ovfRaw) == 0 {
+ logger.Warning("azure: ovf-env.xml was empty")
+ ovfRaw = nil
+ } else {
+ logger.Info("azure: read provisioning metadata from OVF (bytes=%d)", len(ovfRaw))
+ }
+
+ logger.Info("azure: [4/4] parsing provisioning metadata and synthesizing Ignition config")
+ var provisioning *linuxProvisioningConfigurationSet
+ if ovfRaw != nil {
+ provisioning, err = parseProvisioningConfig(ovfRaw)
+ if err != nil {
+ provisioningErr = fmt.Errorf("parsing OVF provisioning metadata: %w", err)
+ logger.Warning("azure: failed to parse provisioning metadata: %v", provisioningErr)
+ provisioning = nil
+ } else {
+ logger.Info("azure: successfully parsed provisioning metadata from ovfRaw")
+ }
+ }
+
+ // Log summary of available data before building config
+ logger.Info("azure: data summary before config generation:")
+ logger.Info("azure: IMDS metadata available: %v", meta != nil)
+ logger.Info("azure: OVF provisioning available: %v", provisioning != nil)
+ if meta != nil {
+ logger.Info("azure: IMDS username: %q", meta.Compute.OSProfile.AdminUsername)
+ logger.Info("azure: IMDS SSH keys count: %d", len(meta.Compute.PublicKeys))
+ }
+ if provisioning != nil {
+ logger.Info("azure: OVF username: %q", provisioning.UserName)
+ logger.Info("azure: OVF has password: %v", provisioning.UserPassword != "")
+ logger.Info("azure: OVF SSH keys count: %d", len(provisioning.SSH.PublicKeys))
+ }
+
+ if meta == nil && provisioning == nil {
+ logger.Warning("azure: both IMDS and OVF data are unavailable - config generation will likely fail")
+ switch {
+ case metaErr != nil:
+ return types.Config{}, metaErr
+ case provisioningErr != nil:
+ return types.Config{}, provisioningErr
+ default:
+ return types.Config{}, fmt.Errorf("azure: no instance metadata or provisioning data available")
+ }
+ }
+
+ cfg, err := buildGeneratedConfig(logger, meta, provisioning)
+ if err != nil {
+ logger.Warning("azure: failed to build generated config: %v", err)
+ return types.Config{}, err
+ }
+
+ logger.Info("azure: generated cloud config successfully")
+ logger.Info("azure: config includes user %q with %d SSH keys", cfg.Passwd.Users[0].Name, len(cfg.Passwd.Users[0].SSHAuthorizedKeys))
+ return cfg, nil
+}
+
+func fetchInstanceMetadata(f *resource.Fetcher) (*instanceMetadata, error) {
+ logger := f.Logger
+ headers := make(http.Header)
+ headers.Set("Metadata", "true")
+
+ // Retry IMDS metadata fetch if networking isn't ready yet
+ const maxNetRetries = 10
+ const netRetryDelay = 2 * time.Second
+
+ var data []byte
+ var err error
+
+ for attempt := 0; attempt < maxNetRetries; attempt++ {
+ data, err = f.FetchToBuffer(imdsInstanceURL, resource.FetchOptions{Headers: headers, RetryCodes: imdsRetryCodes})
+ if err == nil {
+ break
+ }
+
+ if attempt < maxNetRetries-1 {
+ logger.Info("azure: IMDS request failed, retrying in %v (attempt %d/%d): %v", netRetryDelay, attempt+1, maxNetRetries, err)
+ time.Sleep(netRetryDelay)
+ continue
+ }
+ logger.Warning("azure: IMDS failed after %d attempts: %v", maxNetRetries, err)
+ }
+
+ if err != nil {
+ return nil, fmt.Errorf("fetching metadata: %w", err)
+ }
+
+ var meta instanceMetadata
+ if err := json.Unmarshal(data, &meta); err != nil {
+ return nil, fmt.Errorf("decoding metadata: %w", err)
+ }
+ return &meta, nil
+}
+
+const (
+ // maxOvfRetries is the maximum number of attempts to find the OVF environment
+ maxOvfRetries = 30
+ // ovfRetryInterval is the time between retries
+ ovfRetryInterval = time.Second
+)
+
+func readOvfEnvironment(f *resource.Fetcher, ovfFsTypes []string) ([]byte, error) {
+ logger := f.Logger
+ checkedDevices := make(map[string]struct{})
+
+ for attempt := 0; attempt < maxOvfRetries; attempt++ {
+ for _, ovfFsType := range ovfFsTypes {
+ devices, err := execUtil.GetBlockDevices(ovfFsType)
+ if err != nil {
+ return nil, fmt.Errorf("failed to retrieve block devices with FSTYPE=%q: %v", ovfFsType, err)
+ }
+ for _, dev := range devices {
+ if _, checked := checkedDevices[dev]; checked {
+ continue
+ }
+ if isCdromPresent(logger, dev) {
+ data, err := readFileFromDevice(f, dev, ovfFsType, "ovf-env.xml")
+ if err != nil {
+ logger.Debug("failed to read ovf environment from device %q: %v", dev, err)
+ } else if len(data) > 0 {
+ return data, nil
+ }
+ }
+ checkedDevices[dev] = struct{}{}
+ }
+ }
+ if attempt < maxOvfRetries-1 {
+ time.Sleep(ovfRetryInterval)
+ }
+ }
+
+ return nil, fmt.Errorf("failed to find OVF environment after %d attempts", maxOvfRetries)
+}
+
+func parseProvisioningConfig(raw []byte) (*linuxProvisioningConfigurationSet, error) {
+ var env provisioningEnvelope
+ if err := xml.Unmarshal(raw, &env); err != nil {
+ return nil, err
+ }
+ return &env.LinuxProvisioningConfigurationSet, nil
+}
+
+func buildGeneratedConfig(logger *log.Logger, meta *instanceMetadata, provisioning *linuxProvisioningConfigurationSet) (types.Config, error) {
+ var username string
+ if meta != nil {
+ username = strings.TrimSpace(meta.Compute.OSProfile.AdminUsername)
+ }
+ if username == "" && provisioning != nil {
+ username = strings.TrimSpace(provisioning.UserName)
+ }
+ if username == "" {
+ return types.Config{}, fmt.Errorf("unable to determine admin username: IMDS returned empty/nil, OVF returned empty/nil")
+ }
+
+ var password string
+ var passwordAuthDisabled bool
+ if provisioning != nil {
+ password = strings.TrimSpace(provisioning.UserPassword)
+ passwordAuthDisabled = provisioning.passwordAuthDisabled()
+ }
+
+ sshKeys := collectSSHPublicKeys(meta, provisioning)
+
+ user := types.PasswdUser{
+ Name: username,
+ Groups: []types.Group{"wheel"},
+ HomeDir: cfgutil.StrToPtr(fmt.Sprintf("/home/%s", username)),
+ Shell: cfgutil.StrToPtr("/bin/bash"),
+ SSHAuthorizedKeys: sshKeys,
+ }
+ if password != "" {
+ // Hash the password if it's not already hashed
+ var passwordHash string
+ if IsPasswordHashed(password) {
+ passwordHash = password
+ } else {
+ var err error
+ passwordHash, err = HashPassword(password)
+ if err != nil {
+ return types.Config{}, fmt.Errorf("hashing password: %w", err)
+ }
+ }
+ user.PasswordHash = cfgutil.StrToPtr(passwordHash)
+ }
+
+ sudoersFile := newDataFile("/etc/sudoers.d/50-azure-cloud-config", 0440, "%wheel ALL=(ALL) NOPASSWD: ALL\n")
+ passwordSetting := "no"
+ if password != "" && !passwordAuthDisabled {
+ passwordSetting = "yes"
+ }
+ sshConfig := fmt.Sprintf(`# Custom SSHD settings
+PasswordAuthentication %s
+PermitRootLogin no
+AllowUsers %s
+`, passwordSetting, username)
+ sshdFile := newDataFile("/etc/ssh/sshd_config.d/50-azure-cloud-config.conf", 0644, sshConfig)
+
+ files := []types.File{sudoersFile, sshdFile}
+ files = append(files, provisioningDataFiles(logger, provisioning)...)
+
+ return types.Config{
+ Ignition: types.Ignition{
+ Version: types.MaxVersion.String(),
+ },
+ Passwd: types.Passwd{
+ Users: []types.PasswdUser{user},
+ },
+ Storage: types.Storage{
+ Files: files,
+ },
+ }, nil
+}
+
+func provisioningDataFiles(logger *log.Logger, provisioning *linuxProvisioningConfigurationSet) []types.File {
+ if provisioning == nil {
+ return nil
+ }
+
+ var files []types.File
+ if data := strings.TrimSpace(provisioning.CustomData); data != "" {
+ decoded, err := base64.StdEncoding.DecodeString(data)
+ if err != nil {
+ if logger != nil {
+ logger.Warning("azure: failed to decode provisioning CustomData, storing raw value: %v", err)
+ }
+ files = append(files, newDataFile("/var/lib/waagent/CustomData", 0600, data))
+ } else {
+ files = append(files, newDataFile("/var/lib/waagent/CustomData", 0600, string(decoded)))
+ }
+ }
+
+ if data := strings.TrimSpace(provisioning.UserData); data != "" {
+ files = append(files, newDataFile("/var/lib/waagent/UserData", 0600, data))
+ }
+
+ return files
+}
+
+func collectSSHPublicKeys(meta *instanceMetadata, provisioning *linuxProvisioningConfigurationSet) []types.SSHAuthorizedKey {
+ seen := make(map[string]struct{})
+ var keys []types.SSHAuthorizedKey
+
+ addKey := func(keyData string) {
+ key := strings.TrimSpace(keyData)
+ if key == "" {
+ return
+ }
+ if _, ok := seen[key]; !ok {
+ seen[key] = struct{}{}
+ keys = append(keys, types.SSHAuthorizedKey(key))
+ }
+ }
+
+ if meta != nil {
+ for _, k := range meta.Compute.PublicKeys {
+ addKey(k.KeyData)
+ }
+ }
+
+ if provisioning != nil {
+ for _, pk := range provisioning.SSH.PublicKeys {
+ addKey(pk.Value)
+ }
+ }
+
+ return keys
+}
+
+func newDataFile(path string, mode int, contents string) types.File {
+ encoded := dataurl.EncodeBytes([]byte(contents))
+ return types.File{
+ Node: types.Node{
+ Path: path,
+ },
+ FileEmbedded1: types.FileEmbedded1{
+ Mode: cfgutil.IntToPtr(mode),
+ Contents: types.Resource{Source: &encoded},
+ },
+ }
+}
diff --git a/internal/providers/azure/azure_test.go b/internal/providers/azure/azure_test.go
new file mode 100644
index 000000000..a12da4458
--- /dev/null
+++ b/internal/providers/azure/azure_test.go
@@ -0,0 +1,490 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package azure
+
+import (
+ "fmt"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/coreos/ignition/v2/config/v3_6_experimental/types"
+ "github.com/coreos/ignition/v2/internal/log"
+ "github.com/coreos/ignition/v2/internal/resource"
+ "github.com/vincent-petithory/dataurl"
+)
+
+type stubFetcher struct {
+ resource.Fetcher
+ responses map[string][]byte
+}
+
+func newStubFetcher() *stubFetcher {
+ l := log.New(true)
+ return &stubFetcher{
+ Fetcher: resource.Fetcher{Logger: &l},
+ responses: make(map[string][]byte),
+ }
+}
+
+func (f *stubFetcher) expect(url string, payload []byte) {
+ f.responses[url] = payload
+}
+
+func (f *stubFetcher) FetchToBuffer(u url.URL, opts resource.FetchOptions) ([]byte, error) {
+ if data, ok := f.responses[u.String()]; ok {
+ return data, nil
+ }
+ return nil, fmt.Errorf("unexpected URL %s", u.String())
+}
+
+func testLogger(t *testing.T) *log.Logger {
+ t.Helper()
+ logger := log.New(true)
+ t.Cleanup(func() {
+ logger.Close()
+ })
+ return &logger
+}
+
+func fileByPath(t *testing.T, files []types.File, path string) *types.File {
+ t.Helper()
+ for i := range files {
+ if files[i].Node.Path == path {
+ return &files[i]
+ }
+ }
+ t.Fatalf("file %s not found", path)
+ return nil
+}
+
+func dataURLContents(t *testing.T, src string) string {
+ t.Helper()
+ du, err := dataurl.DecodeString(src)
+ if err != nil {
+ t.Fatalf("failed to decode data URL: %v", err)
+ }
+ return string(du.Data)
+}
+
+func TestParseProvisioningConfig(t *testing.T) {
+ raw := []byte(`
+
+
+ myhost
+ azureuser
+ password
+ false
+
+
+
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCu
+
+
+
+
+`)
+
+ cfg, err := parseProvisioningConfig(raw)
+ if err != nil {
+ t.Fatalf("parseProvisioningConfig() err = %v", err)
+ }
+ if cfg.UserName != "azureuser" {
+ t.Fatalf("expected username azureuser, got %s", cfg.UserName)
+ }
+ if len(cfg.SSH.PublicKeys) != 1 {
+ t.Fatalf("expected 1 ssh key, got %d", len(cfg.SSH.PublicKeys))
+ }
+}
+
+func TestParseProvisioningConfigErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ xml []byte
+ }{
+ {
+ name: "malformed XML",
+ xml: []byte(`
+
+ testuser
+
+ `),
+ },
+ {
+ name: "empty XML",
+ xml: []byte(``),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := parseProvisioningConfig(tt.xml)
+ if err == nil {
+ t.Fatalf("expected error for %s", tt.name)
+ }
+ })
+ }
+}
+
+func TestBuildGeneratedConfig(t *testing.T) {
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ Hostname: "example",
+ OSProfile: instanceOSProfile{
+ AdminUsername: "meta-user",
+ },
+ PublicKeys: []instancePublicKey{
+ {KeyData: "ssh-rsa AAAAB3Nza meta"},
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{
+ UserName: "prov-user",
+ SSH: sshSection{
+ PublicKeys: []sshPublicKey{
+ {Value: "ssh-ed25519 AAAAC3Nza prov"},
+ },
+ },
+ UserPassword: "plaintext",
+ }
+
+ cfg, err := buildGeneratedConfig(testLogger(t), meta, prov)
+ if err != nil {
+ t.Fatalf("buildGeneratedConfig() err = %v", err)
+ }
+
+ if len(cfg.Passwd.Users) != 1 {
+ t.Fatalf("expected 1 user, got %d", len(cfg.Passwd.Users))
+ }
+ user := cfg.Passwd.Users[0]
+ if user.Name != "meta-user" {
+ t.Fatalf("expected user meta-user, got %s", user.Name)
+ }
+ if len(user.SSHAuthorizedKeys) != 2 {
+ t.Fatalf("expected 2 ssh keys, got %d", len(user.SSHAuthorizedKeys))
+ }
+ // Password should be hashed (starts with $6$ for SHA-512)
+ if user.PasswordHash == nil {
+ t.Fatalf("expected password hash to be set")
+ }
+ if !strings.HasPrefix(*user.PasswordHash, "$6$") {
+ t.Fatalf("expected password hash to be SHA-512 (start with $6$), got %s", *user.PasswordHash)
+ }
+
+ if len(cfg.Storage.Files) != 2 {
+ t.Fatalf("expected 2 files, got %d", len(cfg.Storage.Files))
+ }
+}
+
+func TestBuildGeneratedConfigWithPrehashedPassword(t *testing.T) {
+ // Test that pre-hashed passwords are not double-hashed
+ prehashedPassword := "$6$rounds=5000$saltsalt$hashedvalue"
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ OSProfile: instanceOSProfile{
+ AdminUsername: "testuser",
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{
+ UserPassword: prehashedPassword,
+ }
+
+ cfg, err := buildGeneratedConfig(testLogger(t), meta, prov)
+ if err != nil {
+ t.Fatalf("buildGeneratedConfig() err = %v", err)
+ }
+
+ user := cfg.Passwd.Users[0]
+ if user.PasswordHash == nil || *user.PasswordHash != prehashedPassword {
+ t.Fatalf("expected pre-hashed password to be preserved, got %v", user.PasswordHash)
+ }
+}
+
+func TestBuildGeneratedConfigErrors(t *testing.T) {
+ meta := &instanceMetadata{}
+ prov := &linuxProvisioningConfigurationSet{}
+ if _, err := buildGeneratedConfig(testLogger(t), meta, prov); err == nil {
+ t.Fatalf("expected error when username missing")
+ }
+}
+
+func TestBuildGeneratedConfigUsernamePriority(t *testing.T) {
+ // Test that IMDS AdminUsername takes priority over OVF UserName
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ OSProfile: instanceOSProfile{
+ AdminUsername: "imds-admin",
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{
+ UserName: "ovf-user",
+ }
+
+ cfg, err := buildGeneratedConfig(testLogger(t), meta, prov)
+ if err != nil {
+ t.Fatalf("buildGeneratedConfig() err = %v", err)
+ }
+ if cfg.Passwd.Users[0].Name != "imds-admin" {
+ t.Fatalf("expected IMDS username 'imds-admin' to take priority, got %s", cfg.Passwd.Users[0].Name)
+ }
+}
+
+func TestBuildGeneratedConfigUsernameFallback(t *testing.T) {
+ // Test fallback to OVF UserName when IMDS AdminUsername is empty
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ OSProfile: instanceOSProfile{
+ AdminUsername: "",
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{
+ UserName: "ovf-user",
+ }
+
+ cfg, err := buildGeneratedConfig(testLogger(t), meta, prov)
+ if err != nil {
+ t.Fatalf("buildGeneratedConfig() err = %v", err)
+ }
+ if cfg.Passwd.Users[0].Name != "ovf-user" {
+ t.Fatalf("expected OVF username 'ovf-user' as fallback, got %s", cfg.Passwd.Users[0].Name)
+ }
+}
+
+func TestBuildGeneratedConfigNoPassword(t *testing.T) {
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ OSProfile: instanceOSProfile{
+ AdminUsername: "testuser",
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{}
+
+ cfg, err := buildGeneratedConfig(testLogger(t), meta, prov)
+ if err != nil {
+ t.Fatalf("buildGeneratedConfig() err = %v", err)
+ }
+ if cfg.Passwd.Users[0].PasswordHash != nil {
+ t.Fatalf("expected nil password hash when no password provided, got %v", *cfg.Passwd.Users[0].PasswordHash)
+ }
+}
+
+func TestCollectSSHPublicKeysDedup(t *testing.T) {
+ meta := &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ PublicKeys: []instancePublicKey{
+ {KeyData: "ssh-rsa AAAA"},
+ {KeyData: "ssh-rsa AAAA"},
+ },
+ },
+ }
+ prov := &linuxProvisioningConfigurationSet{
+ SSH: sshSection{
+ PublicKeys: []sshPublicKey{
+ {Value: "ssh-rsa BBBB"},
+ {Value: "ssh-rsa AAAA"},
+ },
+ },
+ }
+ keys := collectSSHPublicKeys(meta, prov)
+ if len(keys) != 2 {
+ t.Fatalf("expected 2 unique keys, got %d", len(keys))
+ }
+}
+
+func TestPasswordAuthDisabledParsing(t *testing.T) {
+ trueCases := []string{"true", "TRUE", "1", " yes ", "YES"}
+ for _, tc := range trueCases {
+ prov := linuxProvisioningConfigurationSet{DisableSshPasswordAuthentication: tc}
+ if !prov.passwordAuthDisabled() {
+ t.Fatalf("expected %q to disable password auth", tc)
+ }
+ }
+ falseCases := []string{"false", "0", "no", "", "NO", "False"}
+ for _, tc := range falseCases {
+ prov := linuxProvisioningConfigurationSet{DisableSshPasswordAuthentication: tc}
+ if prov.passwordAuthDisabled() {
+ t.Fatalf("expected %q to allow password auth", tc)
+ }
+ }
+}
+
+func TestHashPassword(t *testing.T) {
+ password := "testpassword123"
+ hash, err := HashPassword(password)
+ if err != nil {
+ t.Fatalf("HashPassword() err = %v", err)
+ }
+
+ // Verify hash format
+ if !strings.HasPrefix(hash, "$6$") {
+ t.Fatalf("expected SHA-512 hash prefix $6$, got %s", hash)
+ }
+
+ // Verify hash has expected structure: $6$$
+ parts := strings.Split(hash, "$")
+ if len(parts) != 4 {
+ t.Fatalf("expected 4 parts in hash, got %d: %s", len(parts), hash)
+ }
+ if parts[1] != "6" {
+ t.Fatalf("expected algorithm identifier '6', got %s", parts[1])
+ }
+ if len(parts[2]) != 16 {
+ t.Fatalf("expected 16 character salt, got %d: %s", len(parts[2]), parts[2])
+ }
+ if len(parts[3]) != 86 {
+ t.Fatalf("expected 86 character hash, got %d: %s", len(parts[3]), parts[3])
+ }
+}
+
+func TestIsPasswordHashed(t *testing.T) {
+ tests := []struct {
+ password string
+ expected bool
+ }{
+ {"$6$salt$hash", true},
+ {"$5$salt$hash", true},
+ {"$y$salt$hash", true},
+ {"$2a$10$hash", true},
+ {"$2b$10$hash", true},
+ {"$2y$10$hash", true},
+ {"$1$salt$hash", true},
+ {"plaintext", false},
+ {"$invalid", false},
+ {"", false},
+ }
+
+ for _, tt := range tests {
+ result := IsPasswordHashed(tt.password)
+ if result != tt.expected {
+ t.Errorf("IsPasswordHashed(%q) = %v, expected %v", tt.password, result, tt.expected)
+ }
+ }
+}
+
+func TestGenerateCloudConfigSuccess(t *testing.T) {
+ t.Cleanup(func() {
+ fetchInstanceMetadataFunc = fetchInstanceMetadata
+ readOvfEnvironmentFunc = readOvfEnvironment
+ })
+
+ fetchInstanceMetadataFunc = func(f *resource.Fetcher) (*instanceMetadata, error) {
+ return &instanceMetadata{
+ Compute: instanceComputeMetadata{
+ OSProfile: instanceOSProfile{AdminUsername: "imds-user"},
+ PublicKeys: []instancePublicKey{
+ {KeyData: "ssh-rsa AAAA"},
+ },
+ },
+ }, nil
+ }
+ ovf := []byte(`
+
+ ovf-user
+ password
+ true
+ ZWNobyBoZWxsbwo=
+
+
+ ssh-ed25519 BBBB
+
+
+
+`)
+ readOvfEnvironmentFunc = func(f *resource.Fetcher, _ []string) ([]byte, error) {
+ return ovf, nil
+ }
+
+ fetcher := newStubFetcher()
+ cfg, err := generateCloudConfig(&fetcher.Fetcher)
+ if err != nil {
+ t.Fatalf("generateCloudConfig() err = %v", err)
+ }
+ if len(cfg.Passwd.Users) != 1 {
+ t.Fatalf("expected 1 user, got %d", len(cfg.Passwd.Users))
+ }
+ if cfg.Passwd.Users[0].Name != "imds-user" {
+ t.Fatalf("expected username imds-user, got %s", cfg.Passwd.Users[0].Name)
+ }
+ if len(cfg.Passwd.Users[0].SSHAuthorizedKeys) != 2 {
+ t.Fatalf("expected merged ssh keys, got %d", len(cfg.Passwd.Users[0].SSHAuthorizedKeys))
+ }
+ if len(cfg.Storage.Files) != 3 {
+ t.Fatalf("expected 3 generated files, got %d", len(cfg.Storage.Files))
+ }
+ customFile := fileByPath(t, cfg.Storage.Files, "/var/lib/waagent/CustomData")
+ if customFile.Contents.Source == nil {
+ t.Fatalf("expected custom data file to have contents")
+ }
+ if got := dataURLContents(t, *customFile.Contents.Source); got != "echo hello\n" {
+ t.Fatalf("unexpected custom data contents: %q", got)
+ }
+}
+
+func TestGenerateCloudConfigNeedNet(t *testing.T) {
+ t.Cleanup(func() {
+ fetchInstanceMetadataFunc = fetchInstanceMetadata
+ readOvfEnvironmentFunc = readOvfEnvironment
+ })
+ wantErr := resource.ErrNeedNet
+ fetchInstanceMetadataFunc = func(f *resource.Fetcher) (*instanceMetadata, error) {
+ return nil, wantErr
+ }
+ readOvfEnvironmentFunc = func(f *resource.Fetcher, _ []string) ([]byte, error) {
+ return nil, fmt.Errorf("should not be called")
+ }
+
+ fetcher := newStubFetcher()
+ _, err := generateCloudConfig(&fetcher.Fetcher)
+ if err == nil || err.Error() != fmt.Sprintf("fetching instance metadata: %v", wantErr) {
+ t.Fatalf("expected wrapped ErrNeedNet, got %v", err)
+ }
+}
+
+func TestGenerateCloudConfigFallbackToProvisioning(t *testing.T) {
+ t.Cleanup(func() {
+ fetchInstanceMetadataFunc = fetchInstanceMetadata
+ readOvfEnvironmentFunc = readOvfEnvironment
+ })
+
+ fetchInstanceMetadataFunc = func(f *resource.Fetcher) (*instanceMetadata, error) {
+ return nil, fmt.Errorf("imds unavailable")
+ }
+ ovf := []byte(`
+
+ ovf-only
+
+
+ ssh-rsa OOOO
+
+
+
+`)
+ readOvfEnvironmentFunc = func(f *resource.Fetcher, _ []string) ([]byte, error) {
+ return ovf, nil
+ }
+
+ fetcher := newStubFetcher()
+ cfg, err := generateCloudConfig(&fetcher.Fetcher)
+ if err != nil {
+ t.Fatalf("expected success, got %v", err)
+ }
+ if cfg.Passwd.Users[0].Name != "ovf-only" {
+ t.Fatalf("expected username from provisioning data, got %s", cfg.Passwd.Users[0].Name)
+ }
+}
diff --git a/internal/providers/azure/password.go b/internal/providers/azure/password.go
new file mode 100644
index 000000000..9acc45217
--- /dev/null
+++ b/internal/providers/azure/password.go
@@ -0,0 +1,84 @@
+// Copyright 2015 CoreOS, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package azure
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "strings"
+
+ "github.com/GehirnInc/crypt/sha512_crypt"
+)
+
+// HashPassword hashes a plaintext password using SHA-512 crypt.
+// Returns a string in the format $6$$.
+func HashPassword(password string) (string, error) {
+ salt, err := generateSalt(16)
+ if err != nil {
+ return "", err
+ }
+
+ crypt := sha512_crypt.New()
+ return crypt.Generate([]byte(password), []byte("$6$"+salt))
+}
+
+// IsPasswordHashed checks if a password string is already hashed.
+// It recognizes common hash formats: SHA-512 ($6$), SHA-256 ($5$),
+// yescrypt ($y$), bcrypt ($2a$, $2b$, $2y$), and MD5 ($1$).
+func IsPasswordHashed(password string) bool {
+ if password == "" {
+ return false
+ }
+
+ // Check for common password hash prefixes
+ hashPrefixes := []string{
+ "$6$", // SHA-512
+ "$5$", // SHA-256
+ "$y$", // yescrypt
+ "$2a$", // bcrypt
+ "$2b$", // bcrypt
+ "$2y$", // bcrypt
+ "$1$", // MD5
+ }
+
+ for _, prefix := range hashPrefixes {
+ if strings.HasPrefix(password, prefix) {
+ // Verify it has the expected structure (at least prefix + something)
+ remaining := password[len(prefix):]
+ if len(remaining) > 0 && strings.Contains(remaining, "$") {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+// generateSalt generates a random salt of the specified length.
+func generateSalt(length int) (string, error) {
+ bytes := make([]byte, length)
+ if _, err := rand.Read(bytes); err != nil {
+ return "", err
+ }
+ // Use base64 encoding and trim to desired length
+ salt := base64.StdEncoding.EncodeToString(bytes)
+ // Remove any characters that might cause issues in crypt salt
+ salt = strings.ReplaceAll(salt, "+", ".")
+ salt = strings.ReplaceAll(salt, "=", "")
+ if len(salt) > length {
+ salt = salt[:length]
+ }
+ return salt, nil
+}
diff --git a/vendor/github.com/GehirnInc/crypt/.travis.yml b/vendor/github.com/GehirnInc/crypt/.travis.yml
new file mode 100644
index 000000000..6a63bc8e8
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/.travis.yml
@@ -0,0 +1,7 @@
+language: go
+go:
+ - 1.6.x
+ - 1.7.x
+ - master
+script:
+ - go test -v -race ./...
diff --git a/vendor/github.com/GehirnInc/crypt/AUTHORS.md b/vendor/github.com/GehirnInc/crypt/AUTHORS.md
new file mode 100644
index 000000000..4490cf22b
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/AUTHORS.md
@@ -0,0 +1,8 @@
+### Initial author
+
+[Jeramey Crawford](https://github.com/jeramey)
+
+### Other authors
+
+- [Jonas mg](https://github.com/kless)
+- [Kohei YOSHIDA](https://github.com/yosida95)
diff --git a/vendor/github.com/GehirnInc/crypt/LICENSE b/vendor/github.com/GehirnInc/crypt/LICENSE
new file mode 100644
index 000000000..7048fecec
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2012, Jeramey Crawford
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/GehirnInc/crypt/README.rst b/vendor/github.com/GehirnInc/crypt/README.rst
new file mode 100644
index 000000000..0608624fc
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/README.rst
@@ -0,0 +1,61 @@
+.. image:: https://travis-ci.org/GehirnInc/crypt.svg?branch=master
+ :target: https://travis-ci.org/GehirnInc/crypt
+
+crypt - A password hashing library for Go
+=========================================
+crypt provides pure golang implementations of UNIX's crypt(3).
+
+The goal of crypt is to bring a library of many common and popular password
+hashing algorithms to Go and to provide a simple and consistent interface to
+each of them. As every hashing method is implemented in pure Go, this library
+should be as portable as Go itself.
+
+All hashing methods come with a test suite which verifies their operation
+against itself as well as the output of other password hashing implementations
+to ensure compatibility with them.
+
+I hope you find this library to be useful and easy to use!
+
+Install
+-------
+
+To install crypt, use the *go get* command.
+
+.. code-block:: sh
+
+ go get github.com/GehirnInc/crypt
+
+
+Usage
+-----
+
+.. code-block:: go
+
+ package main
+
+ import (
+ "fmt"
+
+ "github.com/GehirnInc/crypt"
+ _ "github.com/GehirnInc/crypt/sha256_crypt"
+ )
+
+ func main() {
+ crypt := crypt.SHA256.New()
+ ret, _ := crypt.Generate([]byte("secret"), []byte("$5$salt"))
+ fmt.Println(ret)
+
+ err := crypt.Verify(ret, []byte("secret"))
+ fmt.Println(err)
+
+ // Output:
+ // $5$salt$kpa26zwgX83BPSR8d7w93OIXbFt/d3UOTZaAu5vsTM6
+ //
+ }
+
+Documentation
+-------------
+
+The documentation is available on GoDoc_.
+
+.. _GoDoc: https://godoc.org/github.com/GehirnInc/crypt
diff --git a/vendor/github.com/GehirnInc/crypt/common/base64.go b/vendor/github.com/GehirnInc/crypt/common/base64.go
new file mode 100644
index 000000000..ee5240e10
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/common/base64.go
@@ -0,0 +1,59 @@
+// (C) Copyright 2012, Jeramey Crawford . All
+// rights reserved. Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package common
+
+const (
+ alphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+)
+
+// Base64_24Bit is a variant of Base64 encoding, commonly used with password
+// hashing algorithms to encode the result of their checksum output.
+//
+// The algorithm operates on up to 3 bytes at a time, encoding the following
+// 6-bit sequences into up to 4 hash64 ASCII bytes.
+//
+// 1. Bottom 6 bits of the first byte
+// 2. Top 2 bits of the first byte, and bottom 4 bits of the second byte.
+// 3. Top 4 bits of the second byte, and bottom 2 bits of the third byte.
+// 4. Top 6 bits of the third byte.
+//
+// This encoding method does not emit padding bytes as Base64 does.
+func Base64_24Bit(src []byte) []byte {
+ if len(src) == 0 {
+ return []byte{} // TODO: return nil
+ }
+
+ dstlen := (len(src)*8 + 5) / 6
+ dst := make([]byte, dstlen)
+
+ di, si := 0, 0
+ n := len(src) / 3 * 3
+ for si < n {
+ val := uint(src[si+2])<<16 | uint(src[si+1])<<8 | uint(src[si])
+ dst[di+0] = alphabet[val&0x3f]
+ dst[di+1] = alphabet[val>>6&0x3f]
+ dst[di+2] = alphabet[val>>12&0x3f]
+ dst[di+3] = alphabet[val>>18]
+ di += 4
+ si += 3
+ }
+
+ rem := len(src) - si
+ if rem == 0 {
+ return dst
+ }
+
+ val := uint(src[si+0])
+ if rem == 2 {
+ val |= uint(src[si+1]) << 8
+ }
+
+ dst[di+0] = alphabet[val&0x3f]
+ dst[di+1] = alphabet[val>>6&0x3f]
+ if rem == 2 {
+ dst[di+2] = alphabet[val>>12]
+ }
+ return dst
+}
diff --git a/vendor/github.com/GehirnInc/crypt/common/doc.go b/vendor/github.com/GehirnInc/crypt/common/doc.go
new file mode 100644
index 000000000..8ba84e960
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/common/doc.go
@@ -0,0 +1,10 @@
+// (C) Copyright 2012, Jeramey Crawford . All
+// rights reserved. Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package common contains routines used by multiple password hashing
+// algorithms.
+//
+// Generally, you will never import this package directly. Many of the
+// *_crypt packages will import this package if they require it.
+package common
diff --git a/vendor/github.com/GehirnInc/crypt/common/salt.go b/vendor/github.com/GehirnInc/crypt/common/salt.go
new file mode 100644
index 000000000..54372be0f
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/common/salt.go
@@ -0,0 +1,148 @@
+// (C) Copyright 2012, Jeramey Crawford . All
+// rights reserved. Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package common
+
+import (
+ "bytes"
+ "crypto/rand"
+ "errors"
+ "strconv"
+)
+
+var (
+ ErrSaltPrefix = errors.New("invalid magic prefix")
+ ErrSaltFormat = errors.New("invalid salt format")
+ ErrSaltRounds = errors.New("invalid rounds")
+)
+
+const (
+ roundsPrefix = "rounds="
+)
+
+// Salt represents a salt.
+type Salt struct {
+ MagicPrefix []byte
+
+ SaltLenMin int
+ SaltLenMax int
+
+ RoundsMin int
+ RoundsMax int
+ RoundsDefault int
+}
+
+// Generate generates a random salt of a given length.
+//
+// The length is set thus:
+//
+// length > SaltLenMax: length = SaltLenMax
+// length < SaltLenMin: length = SaltLenMin
+func (s *Salt) Generate(length int) []byte {
+ if length > s.SaltLenMax {
+ length = s.SaltLenMax
+ } else if length < s.SaltLenMin {
+ length = s.SaltLenMin
+ }
+
+ saltLen := (length * 6 / 8)
+ if (length*6)%8 != 0 {
+ saltLen += 1
+ }
+ salt := make([]byte, saltLen)
+ rand.Read(salt)
+
+ out := make([]byte, len(s.MagicPrefix)+length)
+ copy(out, s.MagicPrefix)
+ copy(out[len(s.MagicPrefix):], Base64_24Bit(salt))
+ return out
+}
+
+// GenerateWRounds creates a random salt with the random bytes being of the
+// length provided, and the rounds parameter set as specified.
+//
+// The parameters are set thus:
+//
+// length > SaltLenMax: length = SaltLenMax
+// length < SaltLenMin: length = SaltLenMin
+//
+// rounds < 0: rounds = RoundsDefault
+// rounds < RoundsMin: rounds = RoundsMin
+// rounds > RoundsMax: rounds = RoundsMax
+//
+// If rounds is equal to RoundsDefault, then the "rounds=" part of the salt is
+// removed.
+func (s *Salt) GenerateWRounds(length, rounds int) []byte {
+ if length > s.SaltLenMax {
+ length = s.SaltLenMax
+ } else if length < s.SaltLenMin {
+ length = s.SaltLenMin
+ }
+ if rounds < 0 {
+ rounds = s.RoundsDefault
+ } else if rounds < s.RoundsMin {
+ rounds = s.RoundsMin
+ } else if rounds > s.RoundsMax {
+ rounds = s.RoundsMax
+ }
+
+ saltLen := (length * 6 / 8)
+ if (length*6)%8 != 0 {
+ saltLen += 1
+ }
+ salt := make([]byte, saltLen)
+ rand.Read(salt)
+
+ roundsText := ""
+ if rounds != s.RoundsDefault {
+ roundsText = roundsPrefix + strconv.Itoa(rounds) + "$"
+ }
+
+ out := make([]byte, len(s.MagicPrefix)+len(roundsText)+length)
+ copy(out, s.MagicPrefix)
+ copy(out[len(s.MagicPrefix):], []byte(roundsText))
+ copy(out[len(s.MagicPrefix)+len(roundsText):], Base64_24Bit(salt))
+ return out
+}
+
+func (s *Salt) Decode(raw []byte) (salt []byte, rounds int, isRoundsDef bool, rest []byte, err error) {
+ tokens := bytes.SplitN(raw, []byte{'$'}, 4)
+ if len(tokens) < 3 {
+ err = ErrSaltFormat
+ return
+ }
+ if !bytes.HasPrefix(raw, s.MagicPrefix) {
+ err = ErrSaltPrefix
+ return
+ }
+
+ if bytes.HasPrefix(tokens[2], []byte(roundsPrefix)) {
+ if len(tokens) < 4 {
+ err = ErrSaltFormat
+ return
+ }
+ salt = tokens[3]
+
+ rounds, err = strconv.Atoi(string(tokens[2][len(roundsPrefix):]))
+ if err != nil {
+ err = ErrSaltRounds
+ return
+ }
+ if rounds < s.RoundsMin {
+ rounds = s.RoundsMin
+ }
+ if rounds > s.RoundsMax {
+ rounds = s.RoundsMax
+ }
+ isRoundsDef = true
+ } else {
+ salt = tokens[2]
+ rounds = s.RoundsDefault
+ }
+ if len(salt) > s.SaltLenMax {
+ salt = salt[0:s.SaltLenMax]
+ }
+
+ return
+}
diff --git a/vendor/github.com/GehirnInc/crypt/crypt.go b/vendor/github.com/GehirnInc/crypt/crypt.go
new file mode 100644
index 000000000..1b4151f38
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/crypt.go
@@ -0,0 +1,121 @@
+// (C) Copyright 2013, Jonas mg. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file.
+
+// Package crypt provides interface for password crypt functions and collects
+// common constants.
+package crypt
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/GehirnInc/crypt/common"
+)
+
+var ErrKeyMismatch = errors.New("hashed value is not the hash of the given password")
+
+// Crypter is the common interface implemented by all crypt functions.
+type Crypter interface {
+ // Generate performs the hashing algorithm, returning a full hash suitable
+ // for storage and later password verification.
+ //
+ // If the salt is empty, a randomly-generated salt will be generated with a
+ // length of SaltLenMax and number RoundsDefault of rounds.
+ //
+ // Any error only can be got when the salt argument is not empty.
+ Generate(key, salt []byte) (string, error)
+
+ // Verify compares a hashed key with its possible key equivalent.
+ // Returns nil on success, or an error on failure; if the hashed key is
+ // diffrent, the error is "ErrKeyMismatch".
+ Verify(hashedKey string, key []byte) error
+
+ // Cost returns the hashing cost (in rounds) used to create the given hashed
+ // key.
+ //
+ // When, in the future, the hashing cost of a key needs to be increased in
+ // order to adjust for greater computational power, this function allows one
+ // to establish which keys need to be updated.
+ //
+ // The algorithms based in MD5-crypt use a fixed value of rounds.
+ Cost(hashedKey string) (int, error)
+
+ // SetSalt sets a different salt. It is used to easily create derivated
+ // algorithms, i.e. "apr1_crypt" from "md5_crypt".
+ SetSalt(salt common.Salt)
+}
+
+// Crypt identifies a crypt function that is implemented in another package.
+type Crypt uint
+
+const (
+ APR1 Crypt = 1 + iota // import github.com/GehirnInc/crypt/apr1_crypt
+ MD5 // import github.com/GehirnInc/crypt/md5_crypt
+ SHA256 // import github.com/GehirnInc/crypt/sha256_crypt
+ SHA512 // import github.com/GehirnInc/crypt/sha512_crypt
+ maxCrypt
+)
+
+var crypts = make([]func() Crypter, maxCrypt)
+
+// New returns new Crypter making the Crypt c.
+// New panics if the Crypt c is unavailable.
+func (c Crypt) New() Crypter {
+ if c > 0 && c < maxCrypt {
+ f := crypts[c]
+ if f != nil {
+ return f()
+ }
+ }
+ panic("crypt: requested crypt function is unavailable")
+}
+
+// Available reports whether the Crypt c is available.
+func (c Crypt) Available() bool {
+ return c > 0 && c < maxCrypt && crypts[c] != nil
+}
+
+var cryptPrefixes = make([]string, maxCrypt)
+
+// RegisterCrypt registers a function that returns a new instance of the given
+// crypt function. This is intended to be called from the init function in
+// packages that implement crypt functions.
+func RegisterCrypt(c Crypt, f func() Crypter, prefix string) {
+ if c >= maxCrypt {
+ panic("crypt: RegisterHash of unknown crypt function")
+ }
+ crypts[c] = f
+ cryptPrefixes[c] = prefix
+}
+
+// New returns a new crypter.
+func New(c Crypt) Crypter {
+ return c.New()
+}
+
+// IsHashSupported returns true if hashedKey has a supported prefix.
+// NewFromHash will not panic for this hashedKey
+func IsHashSupported(hashedKey string) bool {
+ for i := range cryptPrefixes {
+ prefix := cryptPrefixes[i]
+ if crypts[i] != nil && strings.HasPrefix(hashedKey, prefix) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// NewFromHash returns a new Crypter using the prefix in the given hashed key.
+func NewFromHash(hashedKey string) Crypter {
+ for i := range cryptPrefixes {
+ prefix := cryptPrefixes[i]
+ if crypts[i] != nil && strings.HasPrefix(hashedKey, prefix) {
+ crypt := Crypt(uint(i))
+ return crypt.New()
+ }
+ }
+
+ panic("crypt: unknown crypt function")
+}
diff --git a/vendor/github.com/GehirnInc/crypt/internal/utils.go b/vendor/github.com/GehirnInc/crypt/internal/utils.go
new file mode 100644
index 000000000..2d36e86ab
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/internal/utils.go
@@ -0,0 +1,41 @@
+// Copyright (c) 2015 Kohei YOSHIDA. All rights reserved.
+// This software is licensed under the 3-Clause BSD License
+// that can be found in LICENSE file.
+package internal
+
+const (
+ cleanBytesLen = 64
+)
+
+var (
+ cleanBytes = make([]byte, cleanBytesLen)
+)
+
+func CleanSensitiveData(b []byte) {
+ l := len(b)
+
+ for ; l > cleanBytesLen; l -= cleanBytesLen {
+ copy(b[l-cleanBytesLen:l], cleanBytes)
+ }
+
+ if l > 0 {
+ copy(b[0:l], cleanBytes[0:l])
+ }
+}
+
+func RepeatByteSequence(input []byte, length int) []byte {
+ var (
+ sequence = make([]byte, length)
+ unit = len(input)
+ )
+
+ j := length / unit * unit
+ for i := 0; i < j; i += unit {
+ copy(sequence[i:length], input)
+ }
+ if j < length {
+ copy(sequence[j:length], input[0:length-j])
+ }
+
+ return sequence
+}
diff --git a/vendor/github.com/GehirnInc/crypt/sha512_crypt/sha512_crypt.go b/vendor/github.com/GehirnInc/crypt/sha512_crypt/sha512_crypt.go
new file mode 100644
index 000000000..1037d73ca
--- /dev/null
+++ b/vendor/github.com/GehirnInc/crypt/sha512_crypt/sha512_crypt.go
@@ -0,0 +1,188 @@
+// (C) Copyright 2012, Jeramey Crawford . All
+// rights reserved. Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package sha512_crypt implements Ulrich Drepper's SHA512-crypt password
+// hashing algorithm.
+//
+// The specification for this algorithm can be found here:
+// http://www.akkadia.org/drepper/SHA-crypt.txt
+package sha512_crypt
+
+import (
+ "bytes"
+ "crypto/sha512"
+ "crypto/subtle"
+ "strconv"
+
+ "github.com/GehirnInc/crypt"
+ "github.com/GehirnInc/crypt/common"
+ "github.com/GehirnInc/crypt/internal"
+)
+
+func init() {
+ crypt.RegisterCrypt(crypt.SHA512, New, MagicPrefix)
+}
+
+const (
+ MagicPrefix = "$6$"
+ SaltLenMin = 1
+ SaltLenMax = 16
+ RoundsMin = 1000
+ RoundsMax = 999999999
+ RoundsDefault = 5000
+)
+
+var _rounds = []byte("rounds=")
+
+type crypter struct{ Salt common.Salt }
+
+// New returns a new crypt.Crypter computing the SHA512-crypt password hashing.
+func New() crypt.Crypter {
+ return &crypter{
+ common.Salt{
+ MagicPrefix: []byte(MagicPrefix),
+ SaltLenMin: SaltLenMin,
+ SaltLenMax: SaltLenMax,
+ RoundsDefault: RoundsDefault,
+ RoundsMin: RoundsMin,
+ RoundsMax: RoundsMax,
+ },
+ }
+}
+
+func (c *crypter) Generate(key, salt []byte) (string, error) {
+ if len(salt) == 0 {
+ salt = c.Salt.GenerateWRounds(SaltLenMax, RoundsDefault)
+ }
+ salt, rounds, isRoundsDef, _, err := c.Salt.Decode(salt)
+ if err != nil {
+ return "", err
+ }
+
+ keyLen := len(key)
+ saltLen := len(salt)
+ h := sha512.New()
+
+ // compute sumB
+ // step 4-8
+ h.Write(key)
+ h.Write(salt)
+ h.Write(key)
+ sumB := h.Sum(nil)
+
+ // Compute sumA
+ // step 1-3, 9-12
+ h.Reset()
+ h.Write(key)
+ h.Write(salt)
+ h.Write(internal.RepeatByteSequence(sumB, keyLen))
+ for i := keyLen; i > 0; i >>= 1 {
+ if i%2 == 0 {
+ h.Write(key)
+ } else {
+ h.Write(sumB)
+ }
+ }
+ sumA := h.Sum(nil)
+ internal.CleanSensitiveData(sumB)
+
+ // Compute seqP
+ // step 13-16
+ h.Reset()
+ for i := 0; i < keyLen; i++ {
+ h.Write(key)
+ }
+ seqP := internal.RepeatByteSequence(h.Sum(nil), keyLen)
+
+ // Compute seqS
+ // step 17-20
+ h.Reset()
+ for i := 0; i < 16+int(sumA[0]); i++ {
+ h.Write(salt)
+ }
+ seqS := internal.RepeatByteSequence(h.Sum(nil), saltLen)
+
+ // step 21
+ for i := 0; i < rounds; i++ {
+ h.Reset()
+
+ if i&1 != 0 {
+ h.Write(seqP)
+ } else {
+ h.Write(sumA)
+ }
+ if i%3 != 0 {
+ h.Write(seqS)
+ }
+ if i%7 != 0 {
+ h.Write(seqP)
+ }
+ if i&1 != 0 {
+ h.Write(sumA)
+ } else {
+ h.Write(seqP)
+ }
+ copy(sumA, h.Sum(nil))
+ }
+ internal.CleanSensitiveData(seqP)
+ internal.CleanSensitiveData(seqS)
+
+ // make output
+ buf := bytes.Buffer{}
+ buf.Grow(len(c.Salt.MagicPrefix) + len(_rounds) + 9 + 1 + len(salt) + 1 + 86)
+ buf.Write(c.Salt.MagicPrefix)
+ if isRoundsDef {
+ buf.Write(_rounds)
+ buf.WriteString(strconv.Itoa(rounds))
+ buf.WriteByte('$')
+ }
+ buf.Write(salt)
+ buf.WriteByte('$')
+ buf.Write(common.Base64_24Bit([]byte{
+ sumA[42], sumA[21], sumA[0],
+ sumA[1], sumA[43], sumA[22],
+ sumA[23], sumA[2], sumA[44],
+ sumA[45], sumA[24], sumA[3],
+ sumA[4], sumA[46], sumA[25],
+ sumA[26], sumA[5], sumA[47],
+ sumA[48], sumA[27], sumA[6],
+ sumA[7], sumA[49], sumA[28],
+ sumA[29], sumA[8], sumA[50],
+ sumA[51], sumA[30], sumA[9],
+ sumA[10], sumA[52], sumA[31],
+ sumA[32], sumA[11], sumA[53],
+ sumA[54], sumA[33], sumA[12],
+ sumA[13], sumA[55], sumA[34],
+ sumA[35], sumA[14], sumA[56],
+ sumA[57], sumA[36], sumA[15],
+ sumA[16], sumA[58], sumA[37],
+ sumA[38], sumA[17], sumA[59],
+ sumA[60], sumA[39], sumA[18],
+ sumA[19], sumA[61], sumA[40],
+ sumA[41], sumA[20], sumA[62],
+ sumA[63],
+ }))
+ return buf.String(), nil
+}
+
+func (c *crypter) Verify(hashedKey string, key []byte) error {
+ newHash, err := c.Generate(key, []byte(hashedKey))
+ if err != nil {
+ return err
+ }
+ if subtle.ConstantTimeCompare([]byte(newHash), []byte(hashedKey)) != 1 {
+ return crypt.ErrKeyMismatch
+ }
+ return nil
+}
+
+func (c *crypter) Cost(hashedKey string) (int, error) {
+ _, rounds, _, _, err := c.Salt.Decode([]byte(hashedKey))
+ if err != nil {
+ return 0, err
+ }
+ return rounds, nil
+}
+
+func (c *crypter) SetSalt(salt common.Salt) { c.Salt = salt }
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 7c78db556..defc53fac 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -123,6 +123,12 @@ github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared
github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/version
github.com/AzureAD/microsoft-authentication-library-for-go/apps/managedidentity
github.com/AzureAD/microsoft-authentication-library-for-go/apps/public
+# github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
+## explicit; go 1.19
+github.com/GehirnInc/crypt
+github.com/GehirnInc/crypt/common
+github.com/GehirnInc/crypt/internal
+github.com/GehirnInc/crypt/sha512_crypt
# github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0
## explicit; go 1.24.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp