Skip to content

Commit

Permalink
obfuscated priv keys in recovery log
Browse files Browse the repository at this point in the history
  • Loading branch information
goatpig committed Apr 20, 2014
1 parent bb22183 commit 70ff71d
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 35 deletions.
62 changes: 50 additions & 12 deletions armoryengine/PyBtcWalletRecovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
import os
import shutil
from time import sleep, ctime
from armoryengine.ArmoryUtils import AllowAsync, emptyFunc, LOGEXCEPT, enum, \
LOGINFO, LOGERROR, enum
from armoryengine.ArmoryUtils import AllowAsync, emptyFunc, LOGEXCEPT, \
LOGINFO, LOGERROR, SECP256K1_ORDER, \
binary_to_int, BIGENDIAN, int_to_binary, \
binary_to_hex, enum
import hmac, hashlib


# 0 1 2 3 4 5
Expand Down Expand Up @@ -49,6 +52,9 @@ def __init__(self):
self.importedErr = [] #all imported keys related errors
self.forkedImports = [] #lists forked imports in wallet

#inconsistent private keys as P/HMAC512(rootKey, 'LogMult') mod N
self.obfuscatedPrivKeys = []

"""
Object wide identifiers. To make sure certain sensitive objects are
deleted after us, they are only defined within ProcessWallet's scope
Expand Down Expand Up @@ -147,6 +153,7 @@ def BuildLogFile(self, errorCode, Progress, returnError=False, nErrors=0):
errors['importedErr'] = self.importedErr
errors['forkedImports'] = self.forkedImports
errors['nErrors'] = nErrors
errors['obfsKeys'] = self.obfuscatedPrivKeys

return errors

Expand Down Expand Up @@ -308,6 +315,13 @@ def BuildLogFile(self, errorCode, Progress, returnError=False, nErrors=0):
(len(self.importedErr)))
for i in range(0, len(self.importedErr)):
self.strOutput.append(' %s\r\n' % (self.importedErr[i]))

if len(self.obfuscatedPrivKeys) > 0:
self.strOutput.append('Inconsistent private keys were found!\r\n')
self.strOutput.append('Logging Multipliers:\r\n')

for i in range(0, len(self.obfuscatedPrivKeys)):
self.strOutput.append(' %s\r\n' % (self.obfuscatedPrivKeys[i]))

####TODO: comments error log
self.strOutput.append('%d errors were found\r\n' % (nErrors))
Expand Down Expand Up @@ -520,7 +534,7 @@ def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None,
if rmode == RECOVERMODE.Stripped:
RecoveredWallet = self.createRecoveredWallet(toRecover, rootAddr, \
SecurePassphrase, Progress, returnError)
rootAddr.binPrivKey32_Plain.destroy()
rootAddr.lock()
if SecurePassphrase: SecurePassphrase.destroy()

if not isinstance(RecoveredWallet, PyBtcWallet):
Expand Down Expand Up @@ -567,7 +581,7 @@ def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None,
if Progress(self.UIreport + UIupdate) == 0:
if SecurePassphrase: SecurePassphrase.destroy()
if toRecover.kdfKey: toRecover.kdfKey.destroy()
rootAddr.binPrivKey32_Plain.destroy()
rootAddr.lock()
return 0

newAddr = None
Expand Down Expand Up @@ -682,7 +696,7 @@ def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None,
self.rawError.append(' chainIndex 0 was not derived from the \
root address')

testroot.binPrivKey32_Plain.destroy()
testroot.lock()

if rmode != RECOVERMODE.Meta:
currSequence = addrDict[0][2]
Expand Down Expand Up @@ -715,7 +729,7 @@ def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None,
if Progress(self.UIreport + UIupdate) == 0:
if SecurePassphrase: SecurePassphrase.destroy()
if toRecover.kdfKey: toRecover.kdfKey.destroy()
rootAddr.binPrivKey32_Plain.destroy()
rootAddr.lock()
return 0
if prgAt:
prgAt[0] = prgAt_in + (0.01 + 0.99*n/prgTotal)*prgAt[1]
Expand Down Expand Up @@ -1016,7 +1030,7 @@ def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None,
if Progress(self.UIreport + UIupdate) == 0:
if SecurePassphrase: SecurePassphrase.destroy()
if toRecover.kdfKey: toRecover.kdfKey.destroy()
rootAddr.binPrivKey32_Plain.destroy()
rootAddr.lock()
return 0
if prgAt:
prgAt[0] = prgAt_in + (0.01 + 0.99*(newAddr.chainIndex +1) \
Expand Down Expand Up @@ -1120,7 +1134,7 @@ def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None,
rootAddr, SecurePassphrase, Progress, returnError)
if SecurePassphrase: RecoveredWallet.kdfKey = \
RecoveredWallet.kdf.DeriveKey(SecurePassphrase)
rootAddr.binPrivKey32_Plain.destroy()
rootAddr.lock()

if not isinstance(RecoveredWallet, PyBtcWallet):
if SecurePassphrase: SecurePassphrase.destroy()
Expand All @@ -1145,6 +1159,11 @@ def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None,
self.UIreport = self.UIreport + UIupdate

#save imported addresses
if rootAddr.isLocked:
rootAddr.unlock(toRecover.kdfKey)
invQ = self.getInvModQ(rootAddr.binPrivKey32_Plain.toBinStr())
rootAddr.lock()

for i in range(0, self.nImports):
UIupdate = '<b>- Saving imported addresses:</b> %d/%d<br>' \
% (i+1, self.nImports)
Expand All @@ -1160,6 +1179,14 @@ def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None,

if newAddr.isLocked:
newAddr.unlock(toRecover.kdfKey)

if newAddr.chainIndex < -2:
obfsPrivKey = CryptoECDSA().ECMultiplyScalars( \
newAddr.binPrivKey32_Plain.toBinStr(),
invQ.toBinStr())
self.obfuscatedPrivKeys.append(binary_to_hex(obfsPrivKey))

if newAddr.isLocked:
newAddr.keyChanged = 1
newAddr.lock(RecoveredWallet.kdfKey)

Expand Down Expand Up @@ -1190,7 +1217,7 @@ def ProcessWallet(self, WalletPath=None, Wallet=None, Passphrase=None,
self.UIreport + UIupdate

if isinstance(rootAddr.binPrivKey32_Plain, SecureBinaryData):
rootAddr.binPrivKey32_Plain.destroy()
rootAddr.lock()

#TODO: nothing to process anymore at this point. if the recovery mode
#is 4 (meta), just return the comments dict
Expand Down Expand Up @@ -1594,8 +1621,19 @@ def createNewWO(self, toRecover, newPath, rootAddr):
newWO.writeFreshWalletFile(newPath)

return newWO

#############################################################################

############################################################################
def getInvModQ(self, Q):
nonce = 0
while 1:
hmacQ = hmac.HMAC(Q, 'LogMult%d' % (nonce), hashlib.sha512).digest()
shaHmacQ = hashlib.new('sha256', hmacQ).digest()

if binary_to_int(shaHmacQ, BIGENDIAN) < SECP256K1_ORDER:
return CryptoECDSA().InvMod(SecureBinaryData(shaHmacQ))

nonce = nonce +1
###############################################################################
def WalletConsistencyCheck(wallet, prgAt=None):
"""
Checks consistency of non encrypted wallet data
Expand Down Expand Up @@ -1714,7 +1752,7 @@ def FixWallet(wltPath, wlt, mode=RECOVERMODE.Full, DoNotMove=False,
Progress(fixer.UIreport + fixer.EndLog)
return -1, fixer.UIreport + fixer.EndLog

#############################################################################
###############################################################################
@AllowAsync
def FixWallets(wallets, dlg, Progress=emptyFunc):

Expand Down
5 changes: 0 additions & 5 deletions cppForSwig/BitcoinArmory.sln
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cryptopp", "cryptopp\cryptopp.vcxproj", "{B1055DA3-83CE-47BF-98E6-2850E3411D39}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BitcoinArmory_SwigDLL", "BitcoinArmory_SwigDLL\BitcoinArmory_SwigDLL.vcxproj", "{19329A6B-FE96-4917-B69A-2B0375C12017}"
ProjectSection(ProjectDependencies) = postProject
{D35F732D-55D7-4037-9C6D-E141F740F802} = {D35F732D-55D7-4037-9C6D-E141F740F802}
{01CD1176-66F7-44AB-9E7B-8AEECC9915E8} = {01CD1176-66F7-44AB-9E7B-8AEECC9915E8}
{B1055DA3-83CE-47BF-98E6-2850E3411D39} = {B1055DA3-83CE-47BF-98E6-2850E3411D39}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BitcoinArmory_CppTests", "BitcoinArmory_CppTests\BitcoinArmory_CppTests.vcxproj", "{D9733F9E-BE30-466F-B100-B686DF663C4D}"
EndProject
Expand Down
7 changes: 4 additions & 3 deletions cppForSwig/EncryptionUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -805,13 +805,14 @@ SecureBinaryData CryptoECDSA::ComputeChainedPublicKey(
}

////////////////////////////////////////////////////////////////////////////////
SecureBinaryData CryptoECDSA::InvMod(const SecureBinaryData& m,
const SecureBinaryData& modulo)
SecureBinaryData CryptoECDSA::InvMod(const SecureBinaryData& m)
{
static BinaryData N = BinaryData::CreateFromHex(
"fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
CryptoPP::Integer cppM;
CryptoPP::Integer cppModulo;
cppM.Decode(m.getPtr(), m.getSize(), UNSIGNED);
cppModulo.Decode(modulo.getPtr(), modulo.getSize(), UNSIGNED);
cppModulo.Decode(N.getPtr(), N.getSize(), UNSIGNED);
CryptoPP::Integer cppResult = cppM.InverseMod(cppModulo);
SecureBinaryData result(32);
cppResult.Encode(result.getPtr(), result.getSize(), UNSIGNED);
Expand Down
3 changes: 1 addition & 2 deletions cppForSwig/EncryptionUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,7 @@ class CryptoECDSA

/////////////////////////////////////////////////////////////////////////////
// We need some direct access to Crypto++ math functions
SecureBinaryData InvMod(const SecureBinaryData& m,
const SecureBinaryData& modulo);
SecureBinaryData InvMod(const SecureBinaryData& m);

/////////////////////////////////////////////////////////////////////////////
// Some standard ECC operations
Expand Down
2 changes: 1 addition & 1 deletion qtdialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13485,7 +13485,7 @@ def LFW(self, wallets):
#dlgIWR = DlgInconsistentWltReport(self, self.main, self.logDirs)
#if dlgIWR.exec_():
#return

return

#################################################################################
class DlgFactoryReset(ArmoryDialog):
Expand Down
89 changes: 77 additions & 12 deletions test/PyBtcWalletRecoveryTest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@

from CppBlockUtils import SecureBinaryData, CryptoECDSA
from CppBlockUtils import SecureBinaryData, CryptoECDSA, CryptoAES
from armoryengine.PyBtcAddress import PyBtcAddress
from armoryengine.ArmoryUtils import hex_to_binary
from armoryengine.ArmoryUtils import hex_to_binary, binary_to_hex
from armoryengine.BinaryUnpacker import BinaryUnpacker
from armoryengine.PyBtcWallet import PyBtcWallet
import unittest
import os
import hmac, hashlib

from armoryengine.ArmoryUtils import SECP256K1_ORDER, binary_to_int, BIGENDIAN

from armoryengine.PyBtcWalletRecovery import PyBtcWalletRecovery

Expand All @@ -21,7 +26,6 @@ def tearDown(self):
os.unlink('armory_%s_RECOVERED_backup.wallet' % self.wltID)

def buildCorruptWallet(self, walletPath):
from armoryengine.PyBtcWallet import PyBtcWallet
crpWlt = PyBtcWallet()
crpWlt.createNewWallet(walletPath, securePassphrase='testing', doRegisterWithBDM=False)
#not registering with the BDM, have to fill the wallet address pool manually
Expand All @@ -43,7 +47,8 @@ def buildCorruptWallet(self, walletPath):
newAddr.chainIndex = 250
PrivKey = hex_to_binary('e3b0c44298fc1c149afbf4c8996fb92427ae41e5978fe51ca495991b7852b855')
newAddr.binPrivKey32_Plain = SecureBinaryData(PrivKey)
newAddr.binPublicKey65 = CryptoECDSA().ComputePublicKey(newAddr.binPrivKey32_Plain)
newAddr.binPublicKey65 = CryptoECDSA().ComputePublicKey( \
newAddr.binPrivKey32_Plain)
newAddr.addrStr20 = newAddr.binPublicKey65.getHash160()
newAddr.isInitialized = True

Expand All @@ -57,17 +62,77 @@ def buildCorruptWallet(self, walletPath):

def testWalletRecovery(self):
#run recovery on broken wallet
brkWltResult = PyBtcWalletRecovery().RecoverWallet(self.corruptWallet, 'testing', 'Full', returnError = 'Dict')
self.assertTrue(len(brkWltResult['sequenceGaps'])==1, "Sequence Gap Undetected")
self.assertTrue(len(brkWltResult['forkedPublicKeyChain'])==2, "Address Chain Forks Undetected")
self.assertTrue(len(brkWltResult['unmatchedPair'])==100, "Unmatched Priv/Pub Key Undetected")
self.assertTrue(len(brkWltResult['misc'])==50, "Wallet Encryption Inconsistency Undetected")
self.assertTrue(brkWltResult['nErrors']==153, "Unexpected Errors Found")
recThread = PyBtcWalletRecovery().RecoverWallet(self.corruptWallet, \
'testing', 'Full', \
returnError = 'Dict')
recThread.join()
brkWltResult = recThread.output

self.assertTrue(len(brkWltResult['sequenceGaps'])==1, \
"Sequence Gap Undetected")
self.assertTrue(len(brkWltResult['forkedPublicKeyChain'])==2, \
"Address Chain Forks Undetected")
self.assertTrue(len(brkWltResult['unmatchedPair'])==100, \
"Unmatched Priv/Pub Key Undetected")
self.assertTrue(len(brkWltResult['misc'])==50, \
"Wallet Encryption Inconsistency Undetected")
self.assertTrue(brkWltResult['nErrors']==153, \
"Unexpected Errors Found")

#check obfuscated keys yield the valid key
#grab root key
badWlt = PyBtcWallet()
badWlt.readWalletFile(self.corruptWallet, False, False)
rootAddr = badWlt.addrMap['ROOT']

SecurePassphrase = SecureBinaryData('testing')
secureKdfOutput = badWlt.kdf.DeriveKey(SecurePassphrase)

#HMAC Q
rootAddr.unlock(secureKdfOutput)
Q = rootAddr.binPrivKey32_Plain.toBinStr()

nonce = 0
while 1:
hmacQ = hmac.HMAC(Q, 'LogMult%d' % (nonce), hashlib.sha512).digest()
shaHmacQ = hashlib.new('sha256', hmacQ).digest()

if binary_to_int(shaHmacQ, BIGENDIAN) < SECP256K1_ORDER:
hmacQ = SecureBinaryData(shaHmacQ)
break
nonce = nonce +1

#Bad Private Keys
import operator
badKeys = [addrObj for addrObj in (sorted(badWlt.addrMap.values(),
key=operator.attrgetter('chainIndex')))]

#run through obdsPrivKey
for i in range(0, len(brkWltResult['obfsKeys'])):
obfsPKey = SecureBinaryData(
hex_to_binary(brkWltResult['obfsKeys'][i]))
pKey = CryptoECDSA().ECMultiplyScalars(obfsPKey.toBinStr(), \
hmacQ.toBinStr())

try:
badKeys[i+201].unlock(secureKdfOutput)
except:
continue

self.assertTrue(binary_to_hex(pKey) == \
badKeys[i+201].binPrivKey32_Plain.toHexStr(), \
'obfs key error')

#run recovery on recovered wallet
rcvWltResult = PyBtcWalletRecovery().RecoverWallet('armory_%s_RECOVERED.wallet' % self.wltID, 'testing', 'Full', returnError = 'Dict')
recThread = PyBtcWalletRecovery().RecoverWallet( \
'armory_%s_RECOVERED.wallet' % self.wltID, \
'testing', 'Full', returnError = 'Dict')
recThread.join()
rcvWltResult = recThread.output

self.assertTrue(rcvWltResult['nErrors']==0, "Unexpected Errors Found")
self.assertTrue(len(rcvWltResult['forkedImports'])==50, "Missing Forked Imports")
self.assertTrue(len(rcvWltResult['forkedImports'])==50, \
"Missing Forked Imports")

###############################################################################
if __name__ == "__main__":
Expand Down

0 comments on commit 70ff71d

Please sign in to comment.