Class: Derivator::Key

Inherits:
Object
  • Object
show all
Defined in:
lib/derivator/key.rb

Overview

SLIP10 key derivation.

Constant Summary collapse

SECP256K1_DER_PRIVATE_PREFIX =

secp256k1 EC private key binary prefix when DER-encoded

'303e020100301006072a8648ce3d020106052b8104000a042730250201010420'
NIST256P1_DER_PRIVATE_PREFIX =

nist256p1 EC private key binary prefix when DER-encoded

'3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420'
ED25519_DER_PRIVATE_PREFIX =

ed25519 EC private key binary prefix when DER-encoded

'302e020100300506032b657004220420'
ED25519_DER_PUBLIC_PREFIX =

ed25519 EC public key binary prefix when DER-encoded

'302a300506032b65700321'
SECP256K1_SEED_KEY =

secp256k1 BIP32/SLIP10 seed key

'Bitcoin seed'
NIST256P1_SEED_KEY =

nist256p1 SLIP10 seed key

'Nist256p1 seed'
ED25519_SEED_KEY =

ed25519 SLIP10 seed key

'ed25519 seed'
SECP256K1_LARGEST_KEY =

secp256k1 largest valid private key

0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140
NIST256P1_LARGEST_KEY =

nist256p1 largest valid private key

0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
ED25519_LARGEST_KEY =

ed25519 largest valid private key (unlimited)

0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(private_key, chain_code, curve = :secp256k1) ⇒ Key

Creates private key from binary data. Use from_hex or from_seed instead for convenience.

Parameters:

  • private_key (String)

    private key (in binary format)

  • chain_code (String)

    chain_code (in binary format)

  • curve (Symbol) (defaults to: :secp256k1)

    curve (:secp256k1, :nist256p1 or :ed25519)



111
112
113
114
115
116
117
118
119
120
# File 'lib/derivator/key.rb', line 111

def initialize(private_key, chain_code, curve = :secp256k1)
  @private_key = private_key.dup.freeze
  @chain_code = chain_code.dup.freeze

  unless %i[secp256k1 nist256p1 ed25519].include?(curve)
    raise ArgumentError.new('curve must be :secp256k1, :nist256p1 or :ed25519')
  end

  @curve = curve
end

Instance Attribute Details

#chain_codeString (readonly)

Returns chain code (in binary format).

Returns:

  • (String)

    chain code (in binary format).



36
37
38
# File 'lib/derivator/key.rb', line 36

def chain_code
  @chain_code
end

#curveSymbol (readonly)

Returns EC curve used for the key (:secp256k1, :nist256p1 or :ed25519).

Returns:

  • (Symbol)

    EC curve used for the key (:secp256k1, :nist256p1 or :ed25519).



30
31
32
# File 'lib/derivator/key.rb', line 30

def curve
  @curve
end

#private_keyString (readonly)

Returns private key (in binary format).

Returns:

  • (String)

    private key (in binary format).



33
34
35
# File 'lib/derivator/key.rb', line 33

def private_key
  @private_key
end

Class Method Details

.from_hex(private_key_hex, chain_code_hex, curve = :secp256k1) ⇒ Key

Creates new key from private key and chain code hex strings.

Parameters:

  • private_key_hex (String)

    private key hex string

  • chain_code_hex (String)

    chain code hex string

  • curve (Symbol) (defaults to: :secp256k1)

    curve (:secp256k1, :nist256p1 or :ed25519)

Returns:

  • (Key)

    new key



46
47
48
# File 'lib/derivator/key.rb', line 46

def self.from_hex(private_key_hex, chain_code_hex, curve = :secp256k1)
  new(private_key_hex.from_hex, chain_code_hex.from_hex, curve)
end

.from_seed(seed_hex, curve = :secp256k1) ⇒ Key

Creates new key from seed hex string.

Parameters:

  • seed_hex (String)

    seed hex string

  • curve (Symbol) (defaults to: :secp256k1)

    curve (:secp256k1, :nist256p1 or :ed25519)

Returns:

  • (Key)

    new key



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/derivator/key.rb', line 55

def self.from_seed(seed_hex, curve = :secp256k1)
  seed_bytes = seed_hex.from_hex
  seed_key =
    case curve
    when :secp256k1
      SECP256K1_SEED_KEY
    when :nist256p1
      NIST256P1_SEED_KEY
    when :ed25519
      ED25519_SEED_KEY
    end

  hmac =
    OpenSSL::HMAC.hexdigest(
      "SHA512",
      seed_key,
      seed_bytes
    )

  private_key_hex = hmac[0..63]
  chain_code_hex = hmac[64..-1]
  if valid_private_key?(private_key_hex, curve)
    from_hex(private_key_hex, chain_code_hex, curve)
  else
    from_seed(hmac, curve)
  end
end

.valid_private_key?(private_key_hex, curve = :secp256k1) ⇒ true, false

Checks private key for a particular curve (used internally). Primarily checks whether key value is less than order of the curve.

Parameters:

  • private_key_hex (String)

    private key hex string

  • curve (Symbol) (defaults to: :secp256k1)

    curve (:secp256k1, :nist256p1 or :ed25519)

Returns:

  • (true, false)

    whether the key is valid



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/derivator/key.rb', line 89

def self.valid_private_key?(private_key_hex, curve = :secp256k1)
  return false unless private_key_hex =~ /\A[a-f0-9]{64}\z/

  largest_key =
    case curve
    when :secp256k1
      SECP256K1_LARGEST_KEY
    when :nist256p1
      NIST256P1_LARGEST_KEY
    when :ed25519
      ED25519_LARGEST_KEY
    end
  private_key = private_key_hex.to_i(16)
  private_key <= largest_key && (private_key > 0 || curve == :ed25519)
end

Instance Method Details

#==(other) ⇒ true, false

Compares with another Derivator::Key.

Parameters:

  • other (Key)

    subject of comparison

Returns:



127
128
129
130
131
132
133
# File 'lib/derivator/key.rb', line 127

def ==(other)
  return false unless %i[private_key chain_code curve].all? { |m| other.respond_to?(m) }

  private_key == other.private_key &&
    chain_code == other.chain_code &&
    curve == other.curve
end

#chain_code_hexString

Chain code as hex string.

Returns:

  • (String)


138
139
140
# File 'lib/derivator/key.rb', line 138

def chain_code_hex
  @chain_code_hex ||= @chain_code.to_hex.freeze
end

#derive(path) ⇒ Object

Derive child key.

Parameters:

  • path (String)

    derivation path, e.g. 'm/0/1', '5', '0/3', "m/0/3'/5'/1/2". Use ' for hardened keys.



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/derivator/key.rb', line 190

def derive(path)
  return self if path == 'm' || path.empty?
  path = path.delete_prefix('m').delete_prefix('/')
  path = path.split('/')

  i_string = path.first
  unless i_string =~ /\A[0-9]+'?\z/
    raise ArgumentError.new("Wrong derivation segment: #{i_string.inspect}")
  end
  i = i_string.to_i
  i += 2**31 if i_string[-1] == "'"

  if curve == :ed25519 && i < 2**31
    raise ArgumentError.new("Only hardened derivation supported with ED25519, got #{i_string} instead")
  end

  if curve != :ed25519
    generator = openssl_group.generator
  end

  data =
    if i >= 2**31
      # Data for HMAC-SHA512(Key = cpar, Data = 0x00 || ser256(kpar) || ser32(i))
      "00" + private_key_hex + ("%08x" % i)
    else
      # Data for HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) || ser32(i))
      generator.mul(private_key_hex.to_i(16)).to_octet_string(:compressed).to_hex + ("%08x" % i)
    end
  data = data.from_hex

  hmac =
    OpenSSL::HMAC.hexdigest(
      "SHA512",
      chain_code,
      data
    )
  derived_private_key_hex = hmac[0..63]
  derived_chain_code_hex = hmac[64..-1]

  if curve != :ed25519
    derived_private_key_hex, derived_chain_code_hex =
      finish_derivation(derived_private_key_hex, derived_chain_code_hex, i)
  end

  new_key = self.class.from_hex(derived_private_key_hex, derived_chain_code_hex, curve)
  new_key.derive(path[1..-1].join('/'))
end

#fingerprintString

BIP32 fingerprint (first 4 bytes of HASH160 of public key) as hex string.

Returns:

  • (String)


146
147
148
# File 'lib/derivator/key.rb', line 146

def fingerprint
  @fingerprint ||= public_key.hash160[0..3].to_hex
end

#openssl_groupOpenSSL::PKey::EC::Group

Creates OpenSSL::PKey::EC::Group instance for the key’s #curve.

Returns:

  • (OpenSSL::PKey::EC::Group)


270
271
272
273
274
275
276
277
# File 'lib/derivator/key.rb', line 270

def openssl_group
  case curve
  when :secp256k1
    OpenSSL::PKey::EC::Group.new('secp256k1')
  when :nist256p1
    OpenSSL::PKey::EC::Group.new('prime256v1')
  end
end

#openssl_pkeyOpenSSL::PKey::EC::Point

Creates OpenSSL::PKey instance.

Returns:

  • (OpenSSL::PKey::EC::Point)


263
264
265
# File 'lib/derivator/key.rb', line 263

def openssl_pkey
  OpenSSL::PKey.read(to_pem)
end

#private_key_hexString

Private key as hex string.

Returns:

  • (String)


153
154
155
# File 'lib/derivator/key.rb', line 153

def private_key_hex
  @private_key_hex ||= @private_key.to_hex.freeze
end

#public_keyString

Public key (in binary format). Use #public_key_hex for convenience.

Returns:

  • (String)


161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/derivator/key.rb', line 161

def public_key
  @public_key ||= begin
    case curve
    when :ed25519
      openssl_pkey.
        public_to_der.
        to_hex.
        delete_prefix(ED25519_DER_PUBLIC_PREFIX).
        from_hex.freeze
    else
      openssl_pkey.
        public_key.
        to_octet_string(:compressed).freeze
    end
  end
end

#public_key_hexString

Public key as hex string.

Returns:

  • (String)


181
182
183
# File 'lib/derivator/key.rb', line 181

def public_key_hex
  @public_key_hex ||= public_key.to_hex.freeze
end

#to_pemString

Exports key to PEM format.

Returns:

  • (String)


241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/derivator/key.rb', line 241

def to_pem
  private_prefix =
    case curve
    when :secp256k1
      SECP256K1_DER_PRIVATE_PREFIX
    when :nist256p1
      NIST256P1_DER_PRIVATE_PREFIX
    when :ed25519
      ED25519_DER_PRIVATE_PREFIX
    end

  der = (private_prefix + private_key_hex).from_hex
  pem = <<~END
    -----BEGIN PRIVATE KEY-----
    #{der.to_base64.strip}
    -----END PRIVATE KEY-----
  END
end