Skip to content

Commit

Permalink
Merge pull request #495 from dvanbrug/windows_password_reset_feature
Browse files Browse the repository at this point in the history
Add support to reset Windows passwords
  • Loading branch information
Temikus authored Apr 10, 2020
2 parents e2ca89b + 158e665 commit d4603bf
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 2 deletions.
2 changes: 2 additions & 0 deletions lib/fog/compute/google.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ class Google < Fog::Service

request :deprecate_image

request :reset_windows_password

model_path "fog/compute/google/models"
model :server
collection :servers
Expand Down
8 changes: 6 additions & 2 deletions lib/fog/compute/google/models/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,10 @@ def stop(async = true)
operation
end

def serial_port_output
def serial_port_output(port: 1)
requires :identity, :zone

service.get_server_serial_port_output(identity, zone_name).to_h[:contents]
service.get_server_serial_port_output(identity, zone_name, :port => port).to_h[:contents]
end

def set_disk_auto_delete(auto_delete, device_name = nil, async = true)
Expand Down Expand Up @@ -581,6 +581,10 @@ def ensure_key_comment(key, default_comment = "fog-user")
parts.join(" ")
end

def reset_windows_password(user)
service.reset_windows_password(:server => self, :user => user)
end

private

def metadata_to_item_list(metadata)
Expand Down
154 changes: 154 additions & 0 deletions lib/fog/compute/google/requests/reset_windows_password.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# 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.
#
# Changes:
# March 2020: Modified example found here:
# https://github.com/GoogleCloudPlatform/compute-image-windows/blob/master/examples/windows_auth_python_sample.py
# to enable fog-google to change windows passwords.

require "openssl"
require "base64"
require "json"

module Fog
module Compute
class Google
class Mock
def reset_windows_password(_server:, _user:)
Fog::Mock.not_implemented
end
end

class Real
##
# Resets Windows passwords for users on Google's Windows based images. Code based on Google provided example.
#
# @param instance [String] the name of the instance
# @param zone [String] the name of the zone of the instance
# @param user [String] the user whose password should be reset
#
# @return [String] new password
#
# @see https://cloud.google.com/compute/docs/instances/windows/automate-pw-generation
def reset_windows_password(server:, user:)
# Pull the e-mail address of user authenticated to API
email = @compute.request_options.authorization.issuer

# Create a new key
key = OpenSSL::PKey::RSA.new(2048)
modulus, exponent = get_modulus_exponent_in_base64(key)

# Get Old Metadata
old_metadata = server.metadata

# Create JSON Object with needed information
metadata_entry = get_json_string(user, modulus, exponent, email)

# Create new metadata object
new_metadata = update_windows_keys(old_metadata, metadata_entry)

# Set metadata on instance
server.set_metadata(new_metadata, false)

# Get encrypted password from Serial Port 4 Output

# If machine is booting for the first time, there appears to be a
# delay before the password appears on the serial port.
sleep(1) until server.ready?
serial_port_output = server.serial_port_output(:port => 4)
loop_cnt = 0
while serial_port_output.empty?
if loop_cnt > 12
Fog::Logger.warning("Encrypted password never found on Serial Output Port 4")
raise "Could not reset password."
end
sleep(5)
serial_port_output = server.serial_port_output(:port => 4)
loop_cnt += 1
end

# Parse and decrypt password
enc_password = get_encrypted_password_from_serial_port(serial_port_output, modulus)
password = decrypt_password(enc_password, key)

return password
end

def get_modulus_exponent_in_base64(key)
mod = [key.n.to_s(16)].pack("H*").strip
exp = [key.e.to_s(16)].pack("H*").strip
modulus = Base64.strict_encode64(mod).strip
exponent = Base64.strict_encode64(exp).strip
return modulus, exponent
end

def get_expiration_time_string
utc_now = Time.now.utc
expire_time = utc_now + 5 * 60
return expire_time.strftime("%Y-%m-%dT%H:%M:%SZ")
end

def get_json_string(user, modulus, exponent, email)
expire = get_expiration_time_string
data = { 'userName': user,
'modulus': modulus,
'exponent': exponent,
'email': email,
'expireOn': expire }
return ::JSON.dump(data)
end

def update_windows_keys(old_metadata, metadata_entry)
if old_metadata[:items]
new_metadata = Hash[old_metadata[:items].map { |item| [item[:key], item[:value]] }]
else
new_metadata = {}
end
new_metadata["windows-keys"] = metadata_entry
return new_metadata
end

def get_encrypted_password_from_serial_port(serial_port_output, modulus)
output = serial_port_output.split("\n")
output.reverse_each do |line|
begin
if line.include?("modulus") && line.include?("encryptedPassword")
entry = ::JSON.parse(line)
if modulus == entry["modulus"]
return entry["encryptedPassword"]
end
else
next
end
rescue ::JSON::ParserError
Fog::Logger.warning("Parsing encrypted password from serial output
failed. Trying to parse next matching line.")
next
end
end
end

def decrypt_password(enc_password, key)
decoded_password = Base64.strict_decode64(enc_password)
begin
return key.private_decrypt(decoded_password, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
rescue OpenSSL::PKey::RSAError
Fog::Logger.warning("Error decrypting password received from Google.
Maybe check output on Serial Port 4 and Metadata key: windows-keys?")
end
end
end
end
end
end
15 changes: 15 additions & 0 deletions test/integration/compute/core_compute/test_servers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,19 @@ def test_start_stop_reboot

assert server.ready?
end

def test_reset_windows_password
win_disk = @disks.create(
:name => "fog-test-1-testservers-test-reset-windows-password-2",
:source_image => "windows-server-1909-dc-core-v20200310",
:size_gb => 32
)
server = @factory.create(:disks => [win_disk])
server.wait_for { ready? }
server.reset_windows_password("test_user")
serial_output = server.serial_port_output(:port => 4)

assert_includes(serial_output, "encryptedPassword")
assert_includes(serial_output, "\"userName\":\"test_user\"")
end
end

0 comments on commit d4603bf

Please sign in to comment.