自动生成 Windows 密码


借助 gcloud compute reset-windows-password 命令,对 Compute Engine 项目具有写入权限的用户可以安全的方式检索 Windows 实例上账号的密码。

该命令通过向实例发送用户名和 RSA 公钥来实现此目的。在实例上运行的代理随后会执行以下一项操作:

  • 在实例上为该用户名创建一个账号,并生成随机密码。
  • 如果账号已经存在,则将密码重置为随机值。

在实例上运行的代理会使用所提供的公钥对密码加密,并将其发送回客户端,等待其使用相应的私钥进行解密。

本部分介绍此过程的工作原理,并向您提供一些以编程方式复制这些步骤的实例脚本。如果您要手动执行这些步骤,请阅读手动说明部分。

准备工作

  • 创建 Windows 实例
  • 如果您尚未设置身份验证,请进行设置。身份验证是通过其进行身份验证以访问 Google Cloud 服务和 API 的过程。如需从本地开发环境运行代码或示例,您可以按如下方式向 Compute Engine 进行身份验证。

    Select the tab for how you plan to use the samples on this page:

    Console

    When you use the Google Cloud console to access Google Cloud services and APIs, you don't need to set up authentication.

    gcloud

    1. 安装 Google Cloud CLI,然后通过运行以下命令初始化 Google Cloud CLI:

      gcloud init
    2. Set a default region and zone.
    3. REST

      如需在本地开发环境中使用本页面上的 REST API 示例,请使用您提供给 gcloud CLI 的凭据。

        安装 Google Cloud CLI,然后通过运行以下命令初始化 Google Cloud CLI:

        gcloud init

      如需了解详情,请参阅 Google Cloud 身份验证文档中的使用 REST 时进行身份验证

自动生成密码

Go


//  Copyright 2018 Google Inc. All Rights Reserved.
//
//  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 main

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha1"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"log"
	"strings"
	"time"

	daisyCompute "github.com/GoogleCloudPlatform/compute-image-tools/daisy/compute"
	"google.golang.org/api/compute/v1"
)

var (
	instance = flag.String("instance", "", "instance to reset password on")
	zone     = flag.String("zone", "", "zone instance is in")
	project  = flag.String("project", "", "project instance is in")
	user     = flag.String("user", "", "user to reset password for")
)

func getInstanceMetadata(client daisyCompute.Client, i, z, p string) (*compute.Metadata, error) {
	ins, err := client.GetInstance(p, z, i)
	if err != nil {
		return nil, fmt.Errorf("error getting instance: %v", err)
	}

	return ins.Metadata, nil
}

type windowsKeyJSON struct {
	ExpireOn string
	Exponent string
	Modulus  string
	UserName string
}

func generateKey(priv *rsa.PublicKey, u string) (*windowsKeyJSON, error) {
	bs := make([]byte, 4)
	binary.BigEndian.PutUint32(bs, uint32(priv.E))

	return &windowsKeyJSON{
		ExpireOn: time.Now().Add(5 * time.Minute).Format(time.RFC3339),
		// This is different than what the other tools produce,
		// AQAB vs AQABAA==, both are decoded as 65537.
		Exponent: base64.StdEncoding.EncodeToString(bs),
		Modulus:  base64.StdEncoding.EncodeToString(priv.N.Bytes()),
		UserName: u,
	}, nil
}

type credsJSON struct {
	ErrorMessage      string `json:"errorMessage,omitempty"`
	EncryptedPassword string `json:"encryptedPassword,omitempty"`
	Modulus           string `json:"modulus,omitempty"`
}

func getEncryptedPassword(client daisyCompute.Client, i, z, p, mod string) (string, error) {
	out, err := client.GetSerialPortOutput(p, z, i, 4, 0)
	if err != nil {
		return "", err
	}

	for _, line := range strings.Split(out.Contents, "\n") {
		var creds credsJSON
		if err := json.Unmarshal([]byte(line), &creds); err != nil {
			continue
		}
		if creds.Modulus == mod {
			if creds.ErrorMessage != "" {
				return "", fmt.Errorf("error from agent: %s", creds.ErrorMessage)
			}
			return creds.EncryptedPassword, nil
		}
	}
	return "", errors.New("password not found in serial output")
}

func decryptPassword(priv *rsa.PrivateKey, ep string) (string, error) {
	bp, err := base64.StdEncoding.DecodeString(ep)
	if err != nil {
		return "", fmt.Errorf("error decoding password: %v", err)
	}
	pwd, err := rsa.DecryptOAEP(sha1.New(), rand.Reader, priv, bp, nil)
	if err != nil {
		return "", fmt.Errorf("error decrypting password: %v", err)
	}
	return string(pwd), nil
}

func resetPassword(client daisyCompute.Client, i, z, p, u string) (string, error) {
	md, err := getInstanceMetadata(client, *instance, *zone, *project)
	if err != nil {
		return "", fmt.Errorf("error getting instance metadata: %v", err)
	}

	fmt.Println("Generating public/private key pair")
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return "", err
	}

	winKey, err := generateKey(&key.PublicKey, u)
	if err != nil {
		return "", err
	}

	data, err := json.Marshal(winKey)
	if err != nil {
		return "", err
	}

	winKeys := string(data)
	var found bool
	for _, mdi := range md.Items {
		if mdi.Key == "windows-keys" {
			val := fmt.Sprintf("%s\n%s", *mdi.Value, winKeys)
			mdi.Value = &val
			found = true
			break
		}
	}
	if !found {
		md.Items = append(md.Items, &compute.MetadataItems{Key: "windows-keys", Value: &winKeys})
	}

	fmt.Println("Setting new 'windows-keys' metadata")
	if err := client.SetInstanceMetadata(p, z, i, md); err != nil {
		return "", err
	}

	fmt.Println("Fetching encrypted password")
	var trys int
	var ep string
	for {
		time.Sleep(1 * time.Second)
		ep, err = getEncryptedPassword(client, i, z, p, winKey.Modulus)
		if err == nil {
			break
		}
		if trys > 10 {
			return "", err
		}
		trys++
	}

	fmt.Println("Decrypting password")
	return decryptPassword(key, ep)
}

func main() {
	flag.Parse()
	if *instance == "" {
		log.Fatal("-instance flag required")
	}
	if *zone == "" {
		log.Fatal("-zone flag required")
	}
	if *project == "" {
		log.Fatal("-project flag required")
	}
	if *user == "" {
		log.Fatal("-user flag required")
	}

	ctx := context.Background()
	client, err := daisyCompute.NewClient(ctx)
	if err != nil {
		log.Fatalf("Error creating compute service: %v", err)
	}

	fmt.Printf("Resetting password on instance %q for user %q\n", *instance, *user)
	pw, err := resetPassword(client, *instance, *zone, *project, *user)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("- Username: %s\n- Password: %s\n", *user, pw)
}

Python


#!/usr/bin/env python

# Copyright 2015 Google Inc. All Rights Reserved.
#
# 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.

import base64
import copy
import datetime
import json
import time

# PyCrypto library: https://pypi.python.org/pypi/pycrypto
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Util.number import long_to_bytes

# Google API Client Library for Python:
# https://developers.google.com/api-client-library/python/start/get_started
from oauth2client.client import GoogleCredentials
from googleapiclient.discovery import build

def GetCompute():
    """Get a compute object for communicating with the Compute Engine API."""
    credentials = GoogleCredentials.get_application_default()
    compute = build('compute', 'v1', credentials=credentials)
    return compute

def GetInstance(compute, instance, zone, project):
    """Get the data for a Google Compute Engine instance."""
    cmd = compute.instances().get(instance=instance, project=project,
                                  zone=zone)
    return cmd.execute()

def GetKey():
    """Get an RSA key for encryption."""
    # This uses the PyCrypto library
    key = RSA.generate(2048)
    return key

def GetModulusExponentInBase64(key):
    """Return the public modulus and exponent for the key in bas64 encoding."""
    mod = long_to_bytes(key.n)
    exp = long_to_bytes(key.e)

    modulus = base64.b64encode(mod)
    exponent = base64.b64encode(exp)

    return modulus, exponent

def GetExpirationTimeString():
    """Return an RFC3339 UTC timestamp for 5 minutes from now."""
    utc_now = datetime.datetime.utcnow()
    # These metadata entries are one-time-use, so the expiration time does
    # not need to be very far in the future. In fact, one minute would
    # generally be sufficient. Five minutes allows for minor variations
    # between the time on the client and the time on the server.
    expire_time = utc_now + datetime.timedelta(minutes=5)
    return expire_time.strftime('%Y-%m-%dT%H:%M:%SZ')

def GetJsonString(user, modulus, exponent, email):
    """Return the JSON string object that represents the windows-keys entry."""
    expire = GetExpirationTimeString()
    data = {'userName': user,
            'modulus': modulus,
            'exponent': exponent,
            'email': email,
            'expireOn': expire}
    return json.dumps(data)

def UpdateWindowsKeys(old_metadata, metadata_entry):
    """Return updated metadata contents with the new windows-keys entry."""
    # Simply overwrites the "windows-keys" metadata entry. Production code may
    # want to append new lines to the metadata value and remove any expired
    # entries.
    new_metadata = copy.deepcopy(old_metadata)
    new_metadata['items'] = [{
        'key': "windows-keys",
        'value': metadata_entry
    }]
    return new_metadata

def UpdateInstanceMetadata(compute, instance, zone, project, new_metadata):
    """Update the instance metadata."""
    cmd = compute.instances().setMetadata(instance=instance, project=project,
                                          zone=zone, body=new_metadata)
    return cmd.execute()

def GetSerialPortFourOutput(compute, instance, zone, project):
    """Get the output from serial port 4 from the instance."""
    # Encrypted passwords are printed to COM4 on the windows server:
    port = 4
    cmd = compute.instances().getSerialPortOutput(instance=instance,
                                                  project=project,
                                                  zone=zone, port=port)
    output = cmd.execute()
    return output['contents']

def GetEncryptedPasswordFromSerialPort(serial_port_output, modulus):
    """Find and return the correct encrypted password, based on the modulus."""
    # In production code, this may need to be run multiple times if the output
    # does not yet contain the correct entry.
    output = serial_port_output.split('\n')
    for line in reversed(output):
        try:
            entry = json.loads(line)
            if modulus == entry['modulus']:
                return entry['encryptedPassword']
        except ValueError:
            pass

def DecryptPassword(encrypted_password, key):
    """Decrypt a base64 encoded encrypted password using the provided key."""
    decoded_password = base64.b64decode(encrypted_password)
    cipher = PKCS1_OAEP.new(key)
    password = cipher.decrypt(decoded_password)
    return password

def main(instance, zone, project, user, email):
    # Setup
    compute = GetCompute()
    key = GetKey()
    modulus, exponent = GetModulusExponentInBase64(key)

    # Get existing metadata
    instance_ref = GetInstance(compute, instance, zone, project)
    old_metadata = instance_ref['metadata']

    # Create and set new metadata
    metadata_entry = GetJsonString(user, modulus,
                                   exponent, email)
    new_metadata = UpdateWindowsKeys(old_metadata, metadata_entry)
    result = UpdateInstanceMetadata(compute, instance, zone, project,
                                    new_metadata)

    # For this sample code, just sleep for 30 seconds instead of checking for
    # responses. In production code, this should monitor the status of the
    # metadata update operation.
    time.sleep(30)

    # Get and decrypt password from serial port output
    serial_port_output = GetSerialPortFourOutput(compute, instance,
                                                 zone, project)
    enc_password = GetEncryptedPasswordFromSerialPort(serial_port_output,
                                                      modulus)
    password = DecryptPassword(enc_password, key)

    # Display the username, password and IP address for the instance
    print 'Username:   {0}'.format(user)
    print 'Password:   {0}'.format(password)
    ip = instance_ref['networkInterfaces'][0]['accessConfigs'][0]['natIP']
    print 'IP Address: {0}'.format(ip)

if __name__ == '__main__':
    instance = 'my-instance'
    zone = 'us-central1-a'
    project = 'my-project'
    user = 'example-user'
    email = 'user@example.com'
    main(instance, zone, project, user, email)

Java


/**
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * 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.
 */

/**
 * This package demonstrates how to reset Windows passwords in Java.
 */

package cloud.google.com.windows.example;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64;
import com.google.api.services.compute.Compute;
import com.google.api.services.compute.model.Instance;
import com.google.api.services.compute.model.Metadata;
import com.google.api.services.compute.model.Metadata.Items;
import com.google.api.services.compute.model.SerialPortOutput;
import com.google.common.io.BaseEncoding;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;

import javax.crypto.Cipher;

public class ExampleCode {

  public ExampleCode() {}

  // Constants used to configure behavior.
  private static final String ZONE_NAME = "us-central1-a";
  private static final String PROJECT_NAME = "example-project-1234";
  private static final String INSTANCE_NAME = "test-instance";
  private static final String APPLICATION_NAME = "windows-pw-reset";

  // Constants for configuring user name, email, and SSH key expiration.
  private static final String USER_NAME = "example_user";
  private static final String EMAIL = "example_user@test.com";

  // Keys are one-time use, so the metadata doesn't need to stay around for long.
  // 5 minutes chosen to allow for differences between time on the client
  // and time on the server.
  private static final long EXPIRE_TIME = 300000;

  // HttpTransport and JsonFactory used to create the Compute object.
  private static HttpTransport httpTransport;
  private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();

  public static void main(String[] args) {
    ExampleCode ec = new ExampleCode();
    try {
      // Initialize Transport object.
      httpTransport = GoogleNetHttpTransport.newTrustedTransport();

      // Reset the password.
      ec.resetPassword();
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
  }

  public void resetPassword() throws Exception {
    // Get credentials to setup a connection with the Compute API.
    Credential cred = GoogleCredential.getApplicationDefault();

    // Create an instance of the Compute API.
    Compute compute = new Compute.Builder(httpTransport, JSON_FACTORY, null)
        .setApplicationName(APPLICATION_NAME).setHttpRequestInitializer(cred).build();

    // Get the instance object to gain access to the instance's metadata.
    Instance inst = compute.instances().get(PROJECT_NAME, ZONE_NAME, INSTANCE_NAME).execute();
    Metadata metadata = inst.getMetadata();

    // Generate the public/private key pair for encryption and decryption.
    KeyPair keys = generateKeys();

    // Update metadata from instance with new windows-keys entry.
    replaceMetadata(metadata, buildKeyMetadata(keys));

    // Tell Compute Engine to update the instance metadata with our changes.
    compute.instances().setMetadata(PROJECT_NAME, ZONE_NAME, INSTANCE_NAME, metadata).execute();

    System.out.println("Updating metadata...");

    // Sleep while waiting for metadata to propagate - production code may
    // want to monitor the status of the metadata update operation.
    Thread.sleep(30000);

    System.out.println("Getting serial output...");

    // Request the output from serial port 4.
    // In production code, this operation should be polled.
    SerialPortOutput output = compute.instances()
        .getSerialPortOutput(PROJECT_NAME, ZONE_NAME, INSTANCE_NAME).setPort(4).execute();

    // Get the last line - this will be a JSON string corresponding to the
    // most recent password reset attempt.
    String[] entries = output.getContents().split("\n");
    String outputEntry = entries[entries.length - 1];

    // Parse output using the json-simple library.
    JSONParser parser = new JSONParser();
    JSONObject passwordDict = (JSONObject) parser.parse(outputEntry);

    String encryptedPassword = passwordDict.get("encryptedPassword").toString();

    // Output user name and decrypted password.
    System.out.println("\nUser name: " + passwordDict.get("userName").toString());
    System.out.println("Password: " + decryptPassword(encryptedPassword, keys));
  }

  private String decryptPassword(String message, KeyPair keys) {
    try {
      // Add the bouncycastle provider - the built-in providers don't support RSA
      // with OAEPPadding.
      Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

      // Get the appropriate cipher instance.
      Cipher rsa = Cipher.getInstance("RSA/NONE/OAEPPadding", "BC");

      // Add the private key for decryption.
      rsa.init(Cipher.DECRYPT_MODE, keys.getPrivate());

      // Decrypt the text.
      byte[] rawMessage = Base64.decodeBase64(message);
      byte[] decryptedText = rsa.doFinal(rawMessage);

      // The password was encoded using UTF8. Transform into string.
      return new String(decryptedText, "UTF8");
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
    return "";
  }

  private void replaceMetadata(Metadata input, JSONObject newMetadataItem) {
    // Transform the JSON object into a string that the API can use.
    String newItemString = newMetadataItem.toJSONString();

    // Get the list containing all of the Metadata entries for this instance.
    List<Items> items = input.getItems();

    // If the instance has no metadata, items can be returned as null.
    if (items == null)
    {
      items = new LinkedList<Items>();
      input.setItems(items);
    }

    // Find the "windows-keys" entry and update it.
    for (Items item : items) {
      if (item.getKey().compareTo("windows-keys") == 0) {
        // Replace item's value with the new entry.
        // To prevent race conditions, production code may want to maintain a
        // list where the oldest entries are removed once the 32KB limit is
        // reached for the metadata entry.
        item.setValue(newItemString);
        return;
      }
    }

    // "windows.keys" entry doesn't exist in the metadata - append it.
    // This occurs when running password-reset for the first time on an instance.
    items.add(new Items().setKey("windows-keys").setValue(newItemString));
  }

  private KeyPair generateKeys() throws NoSuchAlgorithmException {
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");

    // Key moduli for encryption/decryption are 2048 bits long.
    keyGen.initialize(2048);

    return keyGen.genKeyPair();
  }

  @SuppressWarnings("unchecked")
  private JSONObject buildKeyMetadata(KeyPair pair) throws NoSuchAlgorithmException,
      InvalidKeySpecException {
    // Object used for storing the metadata values.
    JSONObject metadataValues = new JSONObject();

    // Encode the public key into the required JSON format.
    metadataValues.putAll(jsonEncode(pair));

    // Add username and email.
    metadataValues.put("userName", USER_NAME);
    metadataValues.put("email", EMAIL);

    // Create the date on which the new keys expire.
    Date now = new Date();
    Date expireDate = new Date(now.getTime() + EXPIRE_TIME);

    // Format the date to match rfc3339.
    SimpleDateFormat rfc3339Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    rfc3339Format.setTimeZone(TimeZone.getTimeZone("UTC"));
    String dateString = rfc3339Format.format(expireDate);

    // Encode the expiration date for the returned JSON dictionary.
    metadataValues.put("expireOn", dateString);

    return metadataValues;
  }

  @SuppressWarnings("unchecked")
  private JSONObject jsonEncode(KeyPair keys) throws NoSuchAlgorithmException,
      InvalidKeySpecException {
    KeyFactory factory = KeyFactory.getInstance("RSA");

    // Get the RSA spec for key manipulation.
    RSAPublicKeySpec pubSpec = factory.getKeySpec(keys.getPublic(), RSAPublicKeySpec.class);

    // Extract required parts of the key.
    BigInteger modulus = pubSpec.getModulus();
    BigInteger exponent = pubSpec.getPublicExponent();

    // Grab an encoder for the modulus and exponent to encode using RFC 3548;
    // Java SE 7 requires an external library (Google's Guava used here)
    // Java SE 8 has a built-in Base64 class that can be used instead. Apache also has an RFC 3548
    // encoder.
    BaseEncoding stringEncoder = BaseEncoding.base64();

    // Strip out the leading 0 byte in the modulus.
    byte[] arr = Arrays.copyOfRange(modulus.toByteArray(), 1, modulus.toByteArray().length);

    JSONObject returnJson = new JSONObject();

    // Encode the modulus, add to returned JSON object.
    String modulusString = stringEncoder.encode(arr).replaceAll("\n", "");
    returnJson.put("modulus", modulusString);

    // Encode exponent, add to returned JSON object.
    String exponentString = stringEncoder.encode(exponent.toByteArray()).replaceAll("\n", "");
    returnJson.put("exponent", exponentString);

    return returnJson;
  }
}

手册说明

本手册中的步骤使用 OpenSSL 处理加密功能,并且使用 Bash shell/Linux 工具处理其他一些功能,但您也可以采用许多其他实现方式。

  1. 生成一个 2048 位 RSA 密钥对。在 OpenSSL 中,运行以下命令来生成此密钥对:

    $ openssl genrsa -out private_key 2048
    

    此命令将创建一个名为 private_key 的私钥文件,其内容如下所示:

    $ cat private_key
    -----BEGIN RSA PRIVATE KEY-----
    MIIEpAIBAAKCAQEAwgsquN4IBNPqIUnu+h/5Za1kujb2YRhX1vCQVQAkBwnWigcC
    qOBVfRa5JoZfx6KIvEXjWqa77jPvlsxM4WPqnDIM2qiK36up3SKkYwFjff6F2ni/
    ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswVZmX2X67naOvJXfY5v0hGPWqCADao
    +xVxrmxsZD4IWnKl1UaZzI5lhAzr8fw6utHwx1EZ/MSgsEki6tujcZfN+GUDRnmJ
    GQSnPTXmsf7Q4DKreTZk49cuyB3prV91S0x3DYjCUpSXrkVy1Ha5XicGD/q+ystu
    FsJnrrhbNXJbpSjM6sjo/aduAkZJl4FmOt0R7QIDAQABAoIBAQCsT6hHc/tg9iIC
    H5pUiRI55Uj+R5JwVGKkXwl8Qdy8V1MpTOJivpuLsiMGf+sL51xO/CzRsiBOfdYz
    bgaTW9vZimR5w5NW3iTAV2Ps+y2zk9KfV/y3/0nzvUSG70OXgBGj+7GhaBQZwS5Z
    5HZOsOYMAV1QSIv8Uu2FQAK1xuOA4seJ/NK42iXgVB1XvYe2AxCWNqCBJylk9F5N
    8a213oJWw2mwQWCSfZhuvwYRO7w/V+mInKPkKlWvf3SLuMCWeDI8s0jLsJMQ0rbp
    jYXRzc2G+LF1aLxjatiGeLsqfVYerNohufGAajpNkSvcMciDXvD9aJhZqior+x2Q
    rCnMuNRNAoGBAPI6r32wIf8H9GmcvGrXk9OYLq0uJGqAtJDgGmJM5BSX4mlSz+Ni
    SYlQOfi24ykQDo3XbA59Lb6H0L64czi2a3OmpG8s6h4ymp+3cSd1k1AER1oZudwH
    9UScGfSgT/nMgufBwEGlQkCMp5x4Sl20clCHZ49p9eNiXML3wxpCZPIjAoGBAM0T
    NKt/rjqMs0qOWAJKemSPk0zV+1RSjCoOdKC6jmHRGr/MIoKiJLIkywV2m53yv8Wu
    BF3gVUDlwojoOKcVR8588tek5L0j9RshGovKj4Uxz9uPPhzeNnlSA+5PS284VtKz
    LX8xZ/b+MNCyor9jT0qoWylqym0w+M4aFL2tUQSvAoGABJvnQO38B51AIk5QK3xE
    nM8VfEgXe0tNpEAPYHV0FYw6S6S+veXd3lX/dGMOeXaLwFkr/i6Vkz2EVEywLJEU
    BFRUZqUlI0P1OzrDVWvgTLJ4JRe+OJiSKycJO2VdgDRK/Vvra5RYaWADxG9pgtTv
    I+cfqlPq0NPLTg5m0PYYc58CgYBpGt/SygTNA1Hc82mN+wgRxDhVmBJRHGG0KGaD
    /jl9TsOr638AfwPZvdvD+A83+7NoKJEaYCCxu1BiBMsMb263GPkJpvyJKAW2mtfV
    L8MxG9+Rgy/tccJvmaZkHIXoAfMV2DmISBUl1Q/F1thsyQRZmkHmz1Hidsf+MgXR
    VSQCBwKBgQCxwJtGZGPdQbDXcZZtL0yJJIbdt5Q/TrW0es17IPAoze+E6zFg9mo7
    ea9AuGxOGDQwO9n5DBn/3XcSjRnhvXaW60Taz6ZC60Zh/s6IilCmav+n9ewFHJ3o
    AglSJZRJ1Eer0m5m6s2FW5U0Yjthxwkm3WCWS61cOOTvb6xhQ5+WSw==
    -----END RSA PRIVATE KEY-----
    
  2. 生成公钥。要创建公钥,请运行以下命令:

    $ openssl rsa -pubout -in private_key -out public_key
    

    此命令将创建一个类似如下的 public_key 文件:

    $ cat public_key
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwgsquN4IBNPqIUnu+h/5
    Za1kujb2YRhX1vCQVQAkBwnWigcCqOBVfRa5JoZfx6KIvEXjWqa77jPvlsxM4WPq
    nDIM2qiK36up3SKkYwFjff6F2ni/ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswV
    ZmX2X67naOvJXfY5v0hGPWqCADao+xVxrmxsZD4IWnKl1UaZzI5lhAzr8fw6utHw
    x1EZ/MSgsEki6tujcZfN+GUDRnmJGQSnPTXmsf7Q4DKreTZk49cuyB3prV91S0x3
    DYjCUpSXrkVy1Ha5XicGD/q+ystuFsJnrrhbNXJbpSjM6sjo/aduAkZJl4FmOt0R
    7QIDAQAB
    -----END PUBLIC KEY-----
    
  3. 提取模数和指数。公钥和私钥由模数和指数构成。从公钥中提取模数和指数:

    $ openssl rsa -in public_key -pubin -text -noout
    Public-Key: (2048 bit)
    Modulus:
        00:c2:0b:2a:b8:de:08:04:d3:ea:21:49:ee:fa:1f:
        f9:65:ad:64:ba:36:f6:61:18:57:d6:f0:90:55:00:
        24:07:09:d6:8a:07:02:a8:e0:55:7d:16:b9:26:86:
        5f:c7:a2:88:bc:45:e3:5a:a6:bb:ee:33:ef:96:cc:
        4c:e1:63:ea:9c:32:0c:da:a8:8a:df:ab:a9:dd:22:
        a4:63:01:63:7d:fe:85:da:78:bf:af:2f:2f:af:05:
        c2:5f:7b:06:67:58:5b:20:79:4a:d0:ed:35:d8:7a:
        40:dc:84:9e:12:cc:15:66:65:f6:5f:ae:e7:68:eb:
        c9:5d:f6:39:bf:48:46:3d:6a:82:00:36:a8:fb:15:
        71:ae:6c:6c:64:3e:08:5a:72:a5:d5:46:99:cc:8e:
        65:84:0c:eb:f1:fc:3a:ba:d1:f0:c7:51:19:fc:c4:
        a0:b0:49:22:ea:db:a3:71:97:cd:f8:65:03:46:79:
        89:19:04:a7:3d:35:e6:b1:fe:d0:e0:32:ab:79:36:
        64:e3:d7:2e:c8:1d:e9:ad:5f:75:4b:4c:77:0d:88:
        c2:52:94:97:ae:45:72:d4:76:b9:5e:27:06:0f:fa:
        be:ca:cb:6e:16:c2:67:ae:b8:5b:35:72:5b:a5:28:
        cc:ea:c8:e8:fd:a7:6e:02:46:49:97:81:66:3a:dd:
        11:ed
    Exponent: 65537 (0x10001)
    
  4. 对模数和指数进行编码。必须提取模数和指数并将其转换为 base64 编码。对模数进行编码前,移除模数的前导零字节。默认情况下,public_key 文件已经是一个 base64 编码的字节串,其中包含以下信息:

    • 32 个字节的标头信息
    • 1 个字节的模数前导零
    • 256 个字节的模数
    • 2 个字节的指数标头
    • 3 个字节的指数

    模数和指数必须与文件内容的其余部分分开提取和编码。请使用以下命令提取模数和指数并对其进行编码:

    $ cat public_key | grep -v -- ----- | base64 -d | dd bs=1 skip=33 count=256 2>/dev/null | base64 -w 0; echo
            wgsquN4IBNPqIUnu+h/5Za1kujb2YRhX1vCQVQAkBwnWigcCqOBVfRa5JoZfx6KIvEXjWqa77jPvlsx
    M4WPqnDIM2qiK36up3SKkYwFjff6F2ni/ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswVZmX2X67naO
    vJXfY5v0hGPWqCADao+xVxrmxsZD4IWnKl1UaZzI5lhAzr8fw6utHwx1EZ/MSgsEki6tujcZfN+GUDR
    nmJGQSnPTXmsf7Q4DKreTZk49cuyB3prV91S0x3DYjCUpSXrkVy1Ha5XicGD/q+ystuFsJnrrhbNXJb
    pSjM6sjo/aduAkZJl4FmOt0R7Q==
    
    $ cat public_key | grep -v -- ----- | base64 -d | dd bs=1 skip=291 count=3 2>/dev/null | base64
    AQAB
    

    如果遇到模数编码问题,请确保在尝试对模数进行编码前,从模数中移除前导零字节。

  5. 使用用户名和公钥信息创建 JSON 对象。使用以下数据创建 JSON 对象:

    • userName:用于登录实例的用户名。
    • modulus:公钥的 base64 编码模数。
    • exponent:公钥的 base64 编码指数
    • email:请求获取密码的用户的电子邮件地址。此地址应该是已通过 API 身份验证的 Google 账号的电子邮件地址。
    • expireOn:密钥到期时间的 RFC 3399 编码时间戳。此时间应该为世界协调时间 (UTC),设置为接下来的五分钟左右。由于这些密钥仅用于生成用户名和密码,因此创建密码之后就不再需要这些密钥。代理不会使用已过期的密钥。

    例如:

    {\"userName\": \"example-user\",  \"modulus\": \"wgsquN4IBNPqIUnu+h/5Za1kujb2YRhX1
    vCQVQAkBwnWigcCqOBVfRa5JoZfx6KIvEXjWqa77jPvlsxM4WPqnDIM2qiK36up3SKkYwFjff6F
    2ni/ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswVZmX2X67naOvJXfY5v0hGPWqCADao+xVxrmx
    sZD4IWnKl1UaZzI5lhAzr8fw6utHwx1EZ/MSgsEki6tujcZfN+GUDRnmJGQSnPTXmsf7Q4DKreT
    Zk49cuyB3prV91S0x3DYjCUpSXrkVy1Ha5XicGD/q+ystuFsJnrrhbNXJbpSjM6sjo/aduAkZJl
    4FmOt0R7Q==\", \"exponent\": \"AQAB\", \"email\": \"example.user@example.com\",
    \"expireOn\": \"2015-04-14T01:37:19Z\"}
    

    请注意,JSON 字符串中不应该有换行符。

  6. 将 JSON 对象添加到实例元数据。使用元数据键 windows-keys 设置实例元数据,并将 JSON 对象设置为键值。

    要更新 API 中的实例元数据,您必须在请求中提供指纹。可以通过向实例发出 GET 请求来获取实例的当前指纹:

    GET  https://compute.googleapis.com/compute/v1/projects/myproject/zones/us-central1-f/instances/test-windows-auth
    [..snip..]
    "metadata": {
    "kind": "compute#metadata",
    "fingerprint": "5sFotm8Ee0I=",
    "items": [
     {
     …
     }
    [..snip]..
    

    接下来,向 setMetadata 方法发送 POST 请求,提供指纹和您创建的 JSON 对象:

    POST https://compute.googleapis.com/compute/v1/projects/myproject/zones/us-central1-f/instances/test-windows-auth/setMetadata
    
    {
     "fingerprint": "5sFotm8Ee0I=",
     "items": [
      {
       "value": "{\"userName\": \"example-user\",  \"modulus\": \"wgsquN4IBNPqIUnu+h/5Za1kujb2YRhX1vCQVQAkBwnWigcCqOBVfRa5JoZfx6KIvEXjWqa77jPvlsxM4WPqnDIM2qiK36up3SKkYwFjff6F2ni/ry8vrwXCX3sGZ1hbIHlK0O012HpA3ISeEswVZmX2X67naOvJXfY5v0hGPWqCADao+xVxrmxsZD4IWnKl1UaZzI5lhAzr8fw6utHwx1EZ/MSgsEki6tujcZfN+GUDRnmJGQSnPTXmsf7Q4DKreTZk49cuyB3prV91S0x3DYjCUpSXrkVy1Ha5XicGD/q+ystuFsJnrrhbNXJbpSjM6sjo/aduAkZJl4FmOt0R7Q==\", \"exponent\": \"AQAB\", \"email\": \"user@example.com\", \"expireOn': '2015\"04-14T01:37:19Z\"}\n",
       "key": "windows-keys"
      } ]
    }
    

    键的名称应为 windows-keys,且值应该设置为一个或多个类似于上述字符串的 JSON 字符串。多个字符串应该使用换行符隔开。添加多个条目时,请确保元数据值不超过 32 KB。

  7. 从串行端口号 4 读取输出。实例上的代理会自动获取 windows-keys 的值并创建加密密码。通过查询串行端口 4 来读取加密密码。在 API 中,向 getSerialPortOutput 方法发出 GET 请求,并将 port=4 作为查询参数传递:

    GET https://compute.googleapis.com/compute/v1/projects/myproject/zones/us-central1-f/instances/test-windows-auth/serialPort?port=4
    
    {
     "kind": "compute#serialPortOutput",
     "selfLink": "https://www.googleapis.com/compute/v1/projects/myproject/zones/_/instances/test-api-auth/serialPort",
     "contents": "{\"ready\":true,\"version\":\"Microsoft Windows NT 6.1.7601 Service Pack 1\"}\n{\"encryptedPassword\":\"uiHDEhxyvj6lF5GalH
     h9TsMZb4bG6Y9qGmFb9S3XI29yvVsDCLdp4IbUg21MncHcaxP0rFu0kyjxlEXDs8y4L1KOhy6iyB42Lh+vZ4XIMjmvU4rZrjsBZ5TxQo9hL0lBW7o3FRM\\/UIXCeRk39ObUl2A
     jDmQ0mcw1byJI5v9KVJnNMaHdRCy\\/kvN6bx3qqjIhIMu0JExp4UVkAX2Mxb9b+c4o2DiZF5pY6ZfbuEmjSbvGRJXyswkOJ4jTZl+7e6+SZfEal8HJyRfZKiqTjrz+DLjYSlXr
     fIRqlvKeAFGOJq6IRojNWiTOOh8Zorc0iHDTIkf+MY0scfbBUo5m30Bf4w==\",\"exponent\":\"AQAB\",\"modulus\":\"0tiKdO2JmBHss26jnrSAwb583KG\\/ZIw5Jw
     wMPXrCVsFAPwY1OV3RlT1Hp4Xvpibr7rvJbOC+f\\/Gd0cBrK5pccQfccB+OHKpbBof473zEfRbdtFwPn10RfAFj\\/xikW0r\\/XxgG\\/c8tz9bmALBStGqmwOVOLRHxjwgtG
     u4poeuwmFfG6TuwgCadxpllW74mviFd4LZVSuCSni5YJnBM2HSJ8NP6g1fqI17KDXt2XO\\/7kSItubmMk+HGEXdH4qiugHYewaIf1o4XSQROC8xlRl7t\\/RaD4U58hKYkVwg0
     Ir7WzYzAVpG2UR4Co\\/GDG9Hct7HOYekDqVQ+sSZbwzajnVunkw==\",\"passwordFound\":true,\"userName\":\"example-user\"}\n"
    }
    

    串行端口输出可能包含多个由换行符隔开的响应。要找到正确的响应,请将您传入的模数与串行端口的输出进行匹配。每个响应都是一个 JSON 编码的字符串,含有以下字段:

    • userName:传递给实例的用户名。
    • passwordFound:一个布尔值,指示密码是否成功生成。
    • encryptedPassword:经过 base64 编码的加密密码。
    • modulus:之前传入的模数。
    • exponent:之前传入的指数。

    如需了解串行端口输出保留,请参阅查看串行端口输出

  8. 对密码进行解密。要获取密码,请使用之前创建的私钥,对加密密码进行解密。必须使用最优非对称加密填充 (OAEP) 对密码进行解密。对于 OpenSSL,对输入数据进行解密的命令是:

    $ openssl rsautl -decrypt -inkey private_key -oaep
    

    要对上面的密码进行解密,请提供 encryptedPassword 值。请记住提前移除字符串中的 \\ 转义字符,否则解密将失败:

    $ echo 'uiHDEhxyvj6lF5GalHh9TsMZb4bG6Y9qGmFb9S3XI291MncHcaxP0rFu0kyjxlEXDs8y4L1KOhy6iyB42Lh+vZ4XIMjmvU4rZrjsBZ5Tx
    Qo9hL0lBW7o3FRM/UIXCeRk39ObUl2AjDmQ0mcw1byJI5v9KVJnNMaHdRCy/kvN6bx3qqjIhIMu0JExp4UVkAX2Mxb9b+c4o2DiZF5pY6ZfbuEmjS
    bvGRJXyswkOJ4jTZl+7e6+SZfEal8HJyRfZKiqTjrz+DLjYSlXrfIRqlvKeAFGOJq6IRojNWiTOOh8Zorc0iHDTIkf+MY0scfbBUo5m30Bf4w==' |
    base64 -d | openssl rsautl -decrypt -inkey private_key -oaep
    

    命令会输出解密后的密码:

    dDkJ_3]*QYS-#>X
    

    此账号的用户名和密码将是:

    username: example-user
    password: dDkJ_3]*QYS-#>X
    
  9. 舍弃密钥。与 SSH 密钥不同,用于检索/重置 Windows 密码的密钥应该是临时密钥。建议不要重复使用公钥/私钥对,否则效果可能不如预期。如果已将密钥保存到磁盘,则应该在整个过程结束时删除这些文件。如果可能的话,最好将密钥保存在内存中,并在整个过程完成时将密钥舍弃。

后续步骤