Class: KindleHacks::Update

Inherits:
Object
  • Object
show all
Defined in:
lib/kindle_hacks/update.rb,
lib/kindle_hacks/update_hl.rb

Overview

:nodoc: documented in update_hl.rb

Constant Summary collapse

DEFAULT_UNKNOWN =

Default value for the unknown byte in the update header.

0x13
DEFAULT_MIN_VERSION =

Default value for updates’ minimum-allowed version.

0
DEFAULT_MAX_VERSION =

Default value for updates’ maximum-allowed version.

0x7fffffff
DEVICE_IDS =

Kindle devices ID (numbers in the 3rd and 4th digits of the serial number).

{:kindle => 1, :kindle2 => 2, :kindle_dx => 4}
BLOCK_SIZE =

Flash partition block size.

{:kindle => 131072, :kindle2 => 131072, :kindle_dx => 131072 }
HEADER_SIZES =

Header sizes, based on update types.

{:manual => 131072, :ota => 64}
TARGET_IDS =

Target IDs in the update manifest.

{ :base_fs => 6, :contents_fs => 7, :temp => 128,
:exec => 129 }
UPDATE_IDS =

Update file signatures.

{:manual => 'FB01', :ota => 'FC02'}
SCRAMBLE_KEY =

The key used to scramble update bytes.

0x7A

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(manifest = {}) ⇒ Update

Initializes an update from a potentially incomplete manifest.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/kindle_hacks/update.rb', line 49

def initialize(manifest = {})    
  @device = manifest[:device] || :kindle
  @device = @device.to_sym
  @files = manifest[:files] || []
  @files.each do |file|
    file[:display] ||= file[:name] + '_file'
    file[:target] = file[:target].to_sym
  end
  @min_version = manifest[:min_version] || DEFAULT_MIN_VERSION
  @max_version = manifest[:max_version] || DEFAULT_MAX_VERSION
  @name = manifest[:name] || ''
  @optional = manifest[:optional] || 0
  @unknown = manifest[:unknown] || DEFAULT_UNKNOWN
  @update = manifest[:signature] || :ota
  @update = @update.to_sym
end

Instance Attribute Details

#deviceObject (readonly)

The device that the update targets, e.g. :kindle_dx.



20
21
22
# File 'lib/kindle_hacks/update.rb', line 20

def device
  @device
end

#filesObject (readonly)

The files that constitute the update.



23
24
25
# File 'lib/kindle_hacks/update.rb', line 23

def files
  @files
end

#max_versionObject (readonly)

The maximum firmware version that this update applies to.

The recommended value is DEFAULT_MAX_VERSION.



33
34
35
# File 'lib/kindle_hacks/update.rb', line 33

def max_version
  @max_version
end

#min_versionObject (readonly)

The minimum firmware version that this update applies to.

The recommended value is DEFAULT_MIN_VERSION.



28
29
30
# File 'lib/kindle_hacks/update.rb', line 28

def min_version
  @min_version
end

#nameObject (readonly)

The update’s name, appended at the end of the update image and manifest.

For example, an update named _Savory-0.06 will have its update file named update_Savory-0.06.bin and the manifest will be named update-Savory-0.06.dat.



40
41
42
# File 'lib/kindle_hacks/update.rb', line 40

def name
  @name
end

#optionalObject (readonly)

Whether this update is optional or not.



43
44
45
# File 'lib/kindle_hacks/update.rb', line 43

def optional
  @optional
end

#updateObject (readonly)

The update’s type, e.g. :ota.



46
47
48
# File 'lib/kindle_hacks/update.rb', line 46

def update
  @update
end

Class Method Details

.decode_files(raw_tgz) ⇒ Object

Decodes an update’s file contents.

Args:

raw_tgz:: a string containing the raw update tar.gz bytes

Returns a hash that can be used as the options argument to Update’s constructor.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/kindle_hacks/update.rb', line 132

def self.decode_files(raw_tgz)
  raw_files = {}
  
  gz_reader = Zlib::GzipReader.new StringIO.new(raw_tgz)
  tar_reader = Archive::Tar::Minitar::Reader.new gz_reader
  tar_reader.each_entry do |entry|
    raise 'Directories unsupported' if entry.directory?
    
    name = entry.full_name      
    contents = entry.read
    raw_files[name] = contents
  end
  tar_reader.close
  gz_reader.close
  
  manifest_file = raw_files.keys.find { |name| /^update.*\.dat$/ =~ name }
  raise 'No update*.dat manifest found' unless manifest_file
  
  files = decode_manifest(raw_files[manifest_file])
  files.each { |file| file[:contents] = raw_files[file[:name]] }
  { :name => manifest_file[6...-4], :files => files }
end

.decode_header(raw_header) ⇒ Object

Decodes an update’s header.

Args:

raw_header:: a string containing the raw update header

Returns a hash that can be used as an argument to Update’s constructor.



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/kindle_hacks/update.rb', line 107

def self.decode_header(raw_header)
  update_id, min_version, max_version, device_id, optional, unknown =
      *raw_header[0, 16].unpack('a4VVvCC')
  
  update = UPDATE_IDS.keys.find { |k| UPDATE_IDS[k] == update_id }
  device = DEVICE_IDS.keys.find { |k| DEVICE_IDS[k] == device_id }
  
  { :update => update, :min_version => min_version,
    :max_version => max_version, :device => device, :optional => optional,
    :unknown => unknown }
end

.decode_manifest(raw_manifest) ⇒ Object

Decodes an update file’s manifest (update*.dat).

Args:

raw_manifest:: a string containing the raw bytes in the manifest file

Returns a hash that can be used as the :files key in the options argument to Update’s constructor.



184
185
186
187
188
189
190
191
# File 'lib/kindle_hacks/update.rb', line 184

def self.decode_manifest(raw_manifest)
  raw_manifest.split("\n").map do |line|
    target_id, md5, filename, block_count, display_name = *line.split
  
    target = TARGET_IDS.keys.find { |k| TARGET_IDS[k].to_s == target_id }
    { :target => target, :name => filename, :display => display_name }
  end
end

.read(raw_update) ⇒ Object

Creates an Update from raw update bytes.

Returns an Update object.



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/kindle_hacks/update.rb', line 69

def self.read(raw_update)
  header = decode_header raw_update
  
  header_size = HEADER_SIZES[header[:update]]
  descramble_key = (SCRAMBLE_KEY >> 4 | SCRAMBLE_KEY << 4) & 0xff
  md5 = scramble! raw_update[16, 32], descramble_key
  tgz = scramble! raw_update[header_size..-1], descramble_key    
  raise 'Update signature is invalid' unless Digest::MD5.hexdigest(tgz) == md5
  
  Update.new header.merge(decode_files(tgz))
end

.read_dir(path) ⇒ Object



14
15
16
17
18
19
20
21
22
23
# File 'lib/kindle_hacks/update_hl.rb', line 14

def self.read_dir(path)
  manifest = YAML.load File.read(File.join(path, 'update.yml'))
  symbolize_keys! manifest
  
  manifest[:files].each do |file|
    file[:contents] = File.read File.join(path, file[:name])
  end

  Update.new manifest
end

.scramble!(bytes, key) ⇒ Object

Scrambles update bytes (either the MD5 or the TGZ).

Args:

bytes:: the bytes to be scrambled (in-place)
key:: the scrambling key (between 0 and 255)

Returns the same array passed in the bytes argument.



210
211
212
213
214
215
216
217
# File 'lib/kindle_hacks/update.rb', line 210

def self.scramble!(bytes, key)
  0.upto(bytes.length - 1) do |i|
    b = bytes[i]
    b = (((b >> 4) | (b << 4)) & 0xFF) ^ key
    bytes[i] = b
  end
  bytes
end

.stringify_keys!(obj) ⇒ Object

Recursively converts a hash’s keys and symbol values to strings.



67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/kindle_hacks/update_hl.rb', line 67

def self.stringify_keys!(obj)
  if obj.kind_of? Hash
    obj.keys.each do |key|
      value = obj.delete key
      value = value.to_s if value.kind_of? Symbol
      stringify_keys! value
      obj[key.to_s] = value
    end
  elsif obj.kind_of? Array
    obj.each { |value| stringify_keys! value }
  end
  obj
end

.symbolize_keys!(obj) ⇒ Object

Recursively converts a hash’s keys to symbols.

Returns the same hash given as an argument.



53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/kindle_hacks/update_hl.rb', line 53

def self.symbolize_keys!(obj)
  if obj.kind_of? Hash
    obj.keys.each do |key|
      value = obj.delete key
      symbolize_keys! value
      obj[key.to_sym] = value
    end
  elsif obj.kind_of? Array
    obj.each { |value| symbolize_keys! value }
  end
  obj
end

Instance Method Details

#binary_file_nameObject

The update’s binary file name.



97
98
99
# File 'lib/kindle_hacks/update.rb', line 97

def binary_file_name
  "update#{@name}.bin"
end

#encoded_filesObject

The encoded file data (tgz) for this update.



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/kindle_hacks/update.rb', line 156

def encoded_files
  raw_tgz = ""
  gz_writer = Zlib::GzipWriter.new StringIO.new(raw_tgz)
  tar_writer = Archive::Tar::Minitar::Writer.new gz_writer
  
  data = Hash[*@files.map { |file| [file[:name], file[:contents]] }.flatten]
  data["update#{@name}.dat"] = encoded_manifest
  mtime = Time.now.to_i
  data.each do |name, contents|
    tar_writer.add_file_simple(name, :mode => 0100755, :uid => 0, :gid => 0,
        :user => 'root', :group => 'root', :mtime => mtime,
        :size => contents.length) do |file|
      file.write contents
    end
  end
  tar_writer.close
  gz_writer.close
  
  raw_tgz
end

#encoded_headerObject

The encoded header for this update.



120
121
122
123
# File 'lib/kindle_hacks/update.rb', line 120

def encoded_header
  [UPDATE_IDS[@update], @min_version, @max_version, DEVICE_IDS[@device],
   @optional, @unknown].pack('a4VVvCC')
end

#encoded_manifestObject

The encoded manifest for this update.



194
195
196
197
198
199
200
201
# File 'lib/kindle_hacks/update.rb', line 194

def encoded_manifest
  @files.map { |file|
    target_id = TARGET_IDS[file[:target]]
    md5 = Digest::MD5.hexdigest file[:contents]
    block_count = file[:contents].length / BLOCK_SIZE[@device]
    [target_id, md5, file[:name], block_count, file[:display]].join ' '
  }.join("\n") + "\n"
end

#manifestObject

The update’s manifest.



36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/kindle_hacks/update_hl.rb', line 36

def manifest
  m = { :device => @device, :files => @files.map { |f| f.clone },
        :min_version => @min_version, :max_version => @max_version,
        :name => @name, :optional => @optional, :update => @update } 

  m[:files].each do |file|
    file.delete :contents
    file.delete :display if file[:display] == file[:name] + '_file'
  end
  m.delete :min_version if m[:min_version] == DEFAULT_MIN_VERSION
  m.delete :max_version if m[:max_version] == DEFAULT_MAX_VERSION
  m
end

#to_binaryObject

Raw update bytes corresponding to an update.

Returns a string that can be written to a .bin file for updating a device.



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/kindle_hacks/update.rb', line 84

def to_binary
  header = encoded_header
  tgz = encoded_files
  md5 = Digest::MD5.hexdigest tgz
  scrambled_md5 = Update.scramble! md5, SCRAMBLE_KEY
  scrambled_tgz = Update.scramble! tgz, SCRAMBLE_KEY
  
  [header, scrambled_md5,
   "\0" * (HEADER_SIZES[@update] - header.length - md5.length),
   scrambled_tgz].join
end

#to_dir(path) ⇒ Object



25
26
27
28
29
30
31
32
33
# File 'lib/kindle_hacks/update_hl.rb', line 25

def to_dir(path)
  m = Update.stringify_keys! manifest    
  File.open(File.join(path, 'update.yml'), 'w') { |f| YAML.dump m, f }
  @files.each do |file|
    File.open File.join(path, file[:name]), 'w' do |f|
      f.write file[:contents]
    end
  end
end