mirror of
https://github.com/tcsenpai/hmywallet.git
synced 2025-06-07 03:15:19 +00:00
first upload
This commit is contained in:
commit
2c3d0ecae5
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
data/credential.pkl
|
||||||
|
__pycache__
|
28
README.md
Normal file
28
README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# HMyWallet
|
||||||
|
|
||||||
|
## Description
|
||||||
|
This is a small proof of concept yet functional and working utility to safely manage a 24 words mnemonic seed using a FIDO2 authenticator and a password.
|
||||||
|
|
||||||
|
You can use it to reliably manage your mnemonic seed and to recover it later without having to remember it or to write it down.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
The script will create a set of credentials from your FIDO2 authenticator during the first run. This set of credentials only works with your FIDO2 authenticator and must be in the data/credential.pkl file. If you lose your FIDO2 authenticator or your credential file, you will not be able to recover your mnemonic seed.
|
||||||
|
|
||||||
|
The script will then ask you to enter your password to authenticate your FIDO2 authenticator.
|
||||||
|
As the password is not stored anywhere, it cannot be compromised by anyone with access to your computer.
|
||||||
|
For the same reason, you will lose access to your mnemonic seed if you lose your password.
|
||||||
|
|
||||||
|
Using the provided password, the script will then generate a 24 words mnemonic seed and print it to the console.
|
||||||
|
|
||||||
|
You can then use this mnemonic seed to access or recover your wallet anywhere.
|
||||||
|
|
||||||
|
# How it works
|
||||||
|
The password is hashed using SHA256, and is used as a seed to generate a hmac_secret from the provided credential.pkl file.
|
||||||
|
This hmac_secret is then used to generate a 24 words mnemonic seed using a wordlist.txt file.
|
||||||
|
This way, both the password and the hmac_secret are only kept in memory until the mnemonic seed is generated, and are not stored anywhere.
|
||||||
|
|
||||||
|
# Credits
|
||||||
|
This script is based on the fido2 library by Yubico.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
This script is provided as is and should be used with caution. It is not tested for usage in managing mnemonic seeds in a production environment.
|
2048
data/wordlist.txt
Normal file
2048
data/wordlist.txt
Normal file
File diff suppressed because it is too large
Load Diff
42
libs/cred_manager.py
Normal file
42
libs/cred_manager.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def create_credential(client, rp, user, challenge):
|
||||||
|
# Create a random challenge
|
||||||
|
challenge = os.urandom(16)
|
||||||
|
# Create a credential with a HmacSecret
|
||||||
|
result = client.make_credential(
|
||||||
|
{
|
||||||
|
"rp": rp,
|
||||||
|
"user": user,
|
||||||
|
"challenge": challenge,
|
||||||
|
"pubKeyCredParams": [{"type": "public-key", "alg": -7}],
|
||||||
|
"extensions": {"hmacCreateSecret": True},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# HmacSecret result:
|
||||||
|
if not result.extension_results.get("hmacCreateSecret"):
|
||||||
|
print("Failed to create credential with HmacSecret")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
credential = result.attestation_object.auth_data.credential_data
|
||||||
|
print("New credential created, with the HmacSecret extension.")
|
||||||
|
# Saving credential to file with pickle
|
||||||
|
with open("credential.pkl", "wb") as f:
|
||||||
|
pickle.dump(credential, f)
|
||||||
|
return credential
|
||||||
|
|
||||||
|
def load_credential():
|
||||||
|
try:
|
||||||
|
with open("data/credential.pkl", "rb") as f:
|
||||||
|
print("Loading credential from file")
|
||||||
|
return pickle.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("No credential found, creating new one")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print("Failed to load credential from file:", e)
|
||||||
|
return None
|
22
libs/word_gen.py
Normal file
22
libs/word_gen.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import binascii
|
||||||
|
|
||||||
|
# Can generate a mnemonic seed from data of 16, 20, 24, 28 or 32 bytes
|
||||||
|
def gen_from_data(data):
|
||||||
|
# Checking if the data length is correct
|
||||||
|
if len(data) not in [16, 20, 24, 28, 32]:
|
||||||
|
raise ValueError(
|
||||||
|
"Data length should be one of the following: [16, 20, 24, 28, 32], but it is not (%d).” % len(data)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Converting the data to binary
|
||||||
|
bindata = bin(int(binascii.hexlify(data), 16))[2:].zfill(8 * len(data))
|
||||||
|
|
||||||
|
# Opening wordlist
|
||||||
|
with open("data/wordlist.txt", "r") as f:
|
||||||
|
wordlist = [w.strip() for w in f.readlines()]
|
||||||
|
# Generating a seed from the binary data
|
||||||
|
seed = []
|
||||||
|
for i in range(len(bindata)//11):
|
||||||
|
index = int(bindata[11*i:11*(i+1)],2)
|
||||||
|
seed.append(wordlist[index])
|
||||||
|
return seed
|
132
main.py
Normal file
132
main.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Copyright (c) 2018 Yubico AB
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or
|
||||||
|
# without modification, are permitted provided that the following
|
||||||
|
# conditions are met:
|
||||||
|
#
|
||||||
|
# 1. Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# 2. Redistributions in binary form must reproduce the above
|
||||||
|
# copyright notice, this list of conditions and the following
|
||||||
|
# disclaimer in the documentation and/or other materials provided
|
||||||
|
# with the distribution.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||||
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||||
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Connects to the first FIDO device found which supports the HmacSecret extension,
|
||||||
|
creates a new credential for it with the extension enabled, and uses it to
|
||||||
|
derive two separate secrets.
|
||||||
|
"""
|
||||||
|
from fido2.hid import CtapHidDevice
|
||||||
|
from fido2.client import Fido2Client, UserInteraction
|
||||||
|
from getpass import getpass
|
||||||
|
import libs.word_gen as word_gen
|
||||||
|
import libs.cred_manager as cred_manager
|
||||||
|
import hashlib
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
|
||||||
|
def sha256(data):
|
||||||
|
enc = hashlib.sha256()
|
||||||
|
enc.update(data.encode())
|
||||||
|
return enc.digest()
|
||||||
|
|
||||||
|
# FIDO2
|
||||||
|
|
||||||
|
try:
|
||||||
|
from fido2.pcsc import CtapPcscDevice
|
||||||
|
except ImportError:
|
||||||
|
CtapPcscDevice = None
|
||||||
|
|
||||||
|
|
||||||
|
def enumerate_devices():
|
||||||
|
for dev in CtapHidDevice.list_devices():
|
||||||
|
yield dev
|
||||||
|
if CtapPcscDevice:
|
||||||
|
for dev in CtapPcscDevice.list_devices():
|
||||||
|
yield dev
|
||||||
|
|
||||||
|
# Handle user interaction
|
||||||
|
class CliInteraction(UserInteraction):
|
||||||
|
def prompt_up(self):
|
||||||
|
print("\nTouch your authenticator device now...\n")
|
||||||
|
|
||||||
|
def request_pin(self, permissions, rd_id):
|
||||||
|
return getpass("Enter PIN: ")
|
||||||
|
|
||||||
|
def request_uv(self, permissions, rd_id):
|
||||||
|
print("User Verification required.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def locate_device():
|
||||||
|
# Locate a device
|
||||||
|
for dev in enumerate_devices():
|
||||||
|
client = Fido2Client(dev, "https://localhost", user_interaction=CliInteraction())
|
||||||
|
if "hmac-secret" in client.info.extensions:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("No Authenticator with the HmacSecret extension found!")
|
||||||
|
sys.exit(1)
|
||||||
|
return client
|
||||||
|
|
||||||
|
# Main
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Prepare parameters for makeCredential and getAssertion
|
||||||
|
rp = {"id": "localhost", "name": "HMyWallet"}
|
||||||
|
user = {"id": b"1", "name": "HMyWalletUser"}
|
||||||
|
|
||||||
|
# Locate a device
|
||||||
|
client = CliInteraction.locate_device()
|
||||||
|
|
||||||
|
# Loading or creating a new credential
|
||||||
|
credential = cred_manager.load_credential()
|
||||||
|
if not credential:
|
||||||
|
credential = cred_manager.create_credential(client, rp, user)
|
||||||
|
|
||||||
|
# Prepare parameters for getAssertion
|
||||||
|
challenge = os.urandom(16) # Use a new challenge for each call.
|
||||||
|
allow_list = [{"type": "public-key", "id": credential.credential_id}]
|
||||||
|
|
||||||
|
# Ask user for password
|
||||||
|
password = getpass("[+] Enter your password: ")
|
||||||
|
# Generate a salt for HmacSecret:
|
||||||
|
password_hash = sha256(password) #os.urandom(32)
|
||||||
|
print("[OK] Password hashed and ready for authentication")
|
||||||
|
|
||||||
|
# Authenticate the credential
|
||||||
|
result = client.get_assertion(
|
||||||
|
{
|
||||||
|
"rpId": rp["id"],
|
||||||
|
"challenge": challenge,
|
||||||
|
"allowCredentials": allow_list,
|
||||||
|
"extensions": {"hmacGetSecret": {"salt1": password_hash}},
|
||||||
|
},
|
||||||
|
).get_response(
|
||||||
|
0
|
||||||
|
) # Only one cred in allowList, only one response.
|
||||||
|
|
||||||
|
secret = result.extension_results["hmacGetSecret"]["output1"]
|
||||||
|
print("[OK] Authenticated and secret retrieved")
|
||||||
|
|
||||||
|
mnemonic_words = word_gen.gen_from_data(secret)
|
||||||
|
mnemonic_string = " ".join(mnemonic_words)
|
||||||
|
print("\nALERT: This is your mnemonic seed. Keep it in a safe place. You will need it to recover your wallet.")
|
||||||
|
print("If you lose it, you will lose access to your wallet. There is no way to recover it later.")
|
||||||
|
print("Anyone with access to your mnemonic seed can recover your wallet. Keep it in a secure location.")
|
||||||
|
print("For security reasons, it is adviced to not save it in plain text: you can use your password and this script to recover it.")
|
||||||
|
print("\nMnemonic words:\n", mnemonic_string)
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
fido2
|
Loading…
x
Reference in New Issue
Block a user