Karadocteur Menu

Générer un wallet Bitcoin en Python

Générer un wallet Bitcoin en Python

Il y a quelques temps, j'ai appris le langage de programmation Python. Ce dernier permet de faire à peu près tout ce que l'on veut, c'est génial ! Etant donné que je suis également passionné par les cryptomonnaies, j'ai voulu comprendre comment fonctionne le protocole Bitcoin au plus profond de lui-même et c'est pourquoi je me suis intéressé de plus près à comment générer des clés privés / wallets Bitcoin en Python pour stocker et sécuriser mes cryptomonnaies.

Si vous souhaitez voir le rendu final intégré dans l'ergonomie de ce blog ou si vous souhaitez simplement générer un nouveau wallet pour y stocker vos cryptomonnaies, je vous invite à créer votre propre wallet sur une page spécialisée de ce blog !

Pour comprendre la théorie, je vous propose de lire ou relire mon article précédent qui définit précisément ce qu'est une clé privée, une clé publique, une adresse et un wallet de Bitcoins.

Python : comment créer un wallet Bitcoin

L'objectif de cet article est de coder un script en python permettant de créer un nouveau couple clé privée au format WIF et adresse de Bitcoin. Pour cela, on va diviser notre programme en plusieurs étapes :

  • Développer les fonctions de hashage (sha256 et ripemd160) et d'encodage (b58)
  • Programmer la courbe elliptique ECDSA Secp256k1
  • Générer une clé privée aléatoirement
  • Obtenir une adresse bitcoin (compressée et non-compressée) depuis la clé privée
  • Convertir la clé privée au format WIF non-compressée et au format WIF compressé
Pour rappel, à partir d'une seule clé privée, il est possible de générer une adresse non-compressée correspondante à la clé privée au format WIF non-compressé et une adresse compressée d'après la clé privée exprimée au format WIF compressé.

Fonctions nécessaires à Bitcoin

Certaines fonctions sont nécessaires au fonctionnement de Bitcoin. Nous allons en coder 3 principales.

Fonction de hashage SHA256

Tout d'abord, il faut programmer les fonctions de hashage. Pour rappel, une telle fonction a pour rôle de transformer une information quelconque (un mot de passe, une clé privée, un document Word, etc) en un hash unique, de telle sorte que le hash représente une signature unique de l'information initiale. En revanche, ces fonctions sont faites pour qu'à partir du hash seul, il soit impossible de retrouver l'information initiale.

Pour coder ces fonctions, il est indispensable d'importer le module "hashlib" de python :

import hashlib

La fonction SHA256 peut s'implémenter de la manière suivante :

def sha256(data):
    digest = hashlib.new("sha256")
    digest.update(data)
    return digest.digest()

Fonction de hashage RIPEMD160

La fonction de hashage RIPEMD160 peut s'implémenter de la manière suivante :

def ripemd160(data):
    digest = hashlib.new("ripemd160")
    digest.update(data)
    return digest.digest()

Fonction d'encodage en B58

L'encodage B58 permet d'écrire des informations en utilisant tous les caractères alpha-numériques (chiffres, lettres minuscules, lettres majuscules) sauf les caractères 0 (zéro), O (o majuscule), l (L minuscule) et I (i majuscule), qui peuvent être confondus entre eux et constituer une source d’erreurs. Cette fonction peut donc s'implémenter de la manière suivante :

def b58(data):
    B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
    if data[0] == 0:
        return "1" + b58(data[1:])
    x = sum([v * (256 ** i) for i, v in enumerate(data[::-1])])
    ret = ""
    while x > 0:
        ret = B58[x % 58] + ret
        x = x // 58
    return ret

Courbe elliptique ECDSA Secp256k1

Dans Bitcoin, comme vous le savez peut être, les clés privées et publiques correspondent en fait à de simples nombres. Ceux-ci peuvent être vraiment extrêmement grands et ne sont donc pas représentés au format décimal, comme nous en avons l'habitude. En effet, pour diminuer la longueur d'écriture de ces nombres dans la mémoire des ordinateurs et ainsi gagner en mémoire, ils sont notés au format héxadécimal (base 16) ou en base 58 (que nous venons de voir ci-dessus).

Dans cette technologie, la génération et la vérification des clés publiques à partir des clés privées se fait par l'intermédiare de techniques de cryptographie asymétrique, qui utilisent notamment la courbe elliptique (ECDSA) Secp256k1.

L'équation de cette courbe est : y^2 = x^3 + 7. Autrement dit, si l'on trace le graphique de cette courbe, tous les points de coordonnées (x;y) présents sur le tracé vérifient cette équation. Ainsi, pour une clé privée donnée, il est définit que la clé publique non-compressée correspond aux deux coordonnées X et Y écrites à la suite, auxquelles est ajouté le préfixe "0x04". Si l'on souhaite obtenir une clé publique compressée, on devra écrire seulement le point X (sans ajouter le Y), mais celui-ci sera préfixé par "0x02" si Y est pair ou par "0x03" si Y est impair.

On pourrait implémenter cela de la manière suivante :

class ECDSAPoint:
    def __init__(self,
        x=0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
        y=0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
        p=2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1):
        self.x = x
        self.y = y
        self.p = p

    def __add__(self, other):
        return self.__radd__(other)

    def __mul__(self, other):
        return self.__rmul__(other)

    def __rmul__(self, other):
        n = self
        q = None
        for i in range(256):
            if other & (1 << i):
                q = q + n
            n = n + n
        return q

    def __radd__(self, other):
        if other is None:
            return self
        x1 = other.x
        y1 = other.y
        x2 = self.x
        y2 = self.y
        p = self.p
        if self == other:
            l = pow(2 * y2 % p, p-2, p) * (3 * x2 * x2) % p
        else:
            l = pow(x1 - x2, p-2, p) * (y1 - y2) % p
        newX = (l ** 2 - x2 - x1) % p
        newY = (l * x2 - l * newX - y2) % p
        return ECDSAPoint(newX, newY)

    def toBytes(self, compressed):
        x = self.x.to_bytes(32, "big")
        y = self.y.to_bytes(32, "big")
        if compressed:
            if (self.y % 2) == 0:
                return b"\x02" + x
            else:
                return b"\x03" + x
        else:
            return b"\x04" + x + y

Développer une classe Bitcoin

Pour générer les clés privées, clés publiques et adresses, il peut être utile d'utiliser une classe spéciale :

class Wallet:
    def __init__(self, private_key = None):
        if private_key == None:
            self.private_key = self.new_private_key()
        else:
            self.private_key = private_key
        self.address_uncompressed = None
        self.address_compressed = None
        self.wif_uncompressed = None
        self.wif_compressed = None

Créer une clé privée sécurisée et aléatoire

Comme dit précédemment, une clé privée est simplement un nombre de 256 bits, soit 32 octets, que l'on peut simplement générer de la manière suivante :

    def new_private_key(self):
        return os.urandom(32)
Pour utiliser cette fonction, il est indispensable d'importer le module "os" de python.

Générer une adresse Bitcoin depuis la clé privée

Etant donné que Bitcoin utilise la courbe ECDSA Spec256k1, pour obtenir une clé publique à partir de la clé privée, il faut multiplier cette dernière par le point initial de la courbe. Ensuite, il faut convertir la clé en un tableau d'octets et la hacher, d'abord avec SHA-256, puis avec RIPEMD-160. De plus, il faut ajouter "0x00" à la clé publique hachée si le réseau cible est le réseau principal (mainnet). Egalement, il faut ajouter la somme de contrôle (checksum) à la fin de l'adresse publique. Celle-ci correspond aux 4 derniers octets du double hash SHA-256 de l'adresse publique calculée. Enfin, il suffit d'encoder la clé calculée en base58.

On peut donc implémenter notre méthode comme ceci :

    def get_address(self, compressed):
        SPEC256k1 = ECDSAPoint()
        pk = int.from_bytes(self.private_key, "big")
        hash160 = ripemd160(sha256((SPEC256k1 * pk).toBytes(compressed)))
        address = b"\x00" + hash160
        address = b58(address + sha256(sha256(address))[:4])
        return address
Cette méthode prend un paramètre "compressed" qui peut valloir True ou False selon si l'on souhaite obtenir l'adresse compressée ou non depuis une clé privée.

On peut donc coder une fonction plus simple pour obtenir l'adresse non-compressée :

    def get_address_uncompressed(self):
        if self.address_uncompressed == None:
            self.address_uncompressed = self.get_address(compressed = False)
        return self.address_uncompressed

Et aussi une fonction pour obtenir l'adresse compresssée :

    def get_address_compressed(self):
        if self.address_compressed == None:
            self.address_compressed = self.get_address(compressed = True)
        return self.address_compressed

Convertir la clé privée au format WIF

Afin de rendre les clés privés lisibles par les humains, il est nécessaire de les encoder au format WIF. Pour l'obtenir, il faut ajouter l'octet "0x80" au début de la clé privée. A noter que pour obtenir le format WIF compressé, il faut rajouter aussi l'octet "0x01" à la fin. Ensuite, il faut rajouter à la fin de la clé WIF partielle que nous avons calculée la somme de contrôle (checksum), c’est-à-dire les 4 derniers octets du double SHA-256. Enfin, il faut encoder le tout au format b58.

Dans notre script, on pourrait le programmer comme ceci :

    def get_wif(self, compressed):
        if compressed:
            wif = b"\x80" + self.private_key + b"\x01"
        else:
            wif = b"\x80" + self.private_key
        wif = b58(wif + sha256(sha256(wif))[:4])
        return wif
A noter que cette méthode aussi nécessite le paramètre "compressed" à mettre à True ou False, selon si l'on souhaite obtenir le format WIF non-compressé ou le format WIF compressé.

On peut alors coder une fonction pour obtenir directement le format WIF non-compressé :

    def get_wif_uncompressed(self):
        if self.wif_uncompressed == None:
            self.wif_uncompressed = self.get_wif(compressed = False)
        return self.wif_uncompressed

Et une fonction pour obtenir le format WIF compressé :

    def get_wif_compressed(self):
        if self.wif_compressed == None:
            self.wif_compressed = self.get_wif(compressed = True)
        return self.wif_compressed

Code final

Le code final qui permet de générer vos propres wallets Bitcoin en python de manière aléatoire est le suivant :

import os
import hashlib

def sha256(data):
    digest = hashlib.new("sha256")
    digest.update(data)
    return digest.digest()

def ripemd160(data):
    digest = hashlib.new("ripemd160")
    digest.update(data)
    return digest.digest()

def b58(data):
    B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
    if data[0] == 0:
        return "1" + b58(data[1:])
    x = sum([v * (256 ** i) for i, v in enumerate(data[::-1])])
    ret = ""
    while x > 0:
        ret = B58[x % 58] + ret
        x = x // 58
    return ret

class ECDSAPoint:
    def __init__(self,
        x=0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
        y=0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
        p=2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1):
        self.x = x
        self.y = y
        self.p = p

    def __add__(self, other):
        return self.__radd__(other)

    def __mul__(self, other):
        return self.__rmul__(other)

    def __rmul__(self, other):
        n = self
        q = None
        for i in range(256):
            if other & (1 << i):
                q = q + n
            n = n + n
        return q

    def __radd__(self, other):
        if other is None:
            return self
        x1 = other.x
        y1 = other.y
        x2 = self.x
        y2 = self.y
        p = self.p
        if self == other:
            l = pow(2 * y2 % p, p-2, p) * (3 * x2 * x2) % p
        else:
            l = pow(x1 - x2, p-2, p) * (y1 - y2) % p
        newX = (l ** 2 - x2 - x1) % p
        newY = (l * x2 - l * newX - y2) % p
        return ECDSAPoint(newX, newY)

    def toBytes(self, compressed):
        x = self.x.to_bytes(32, "big")
        y = self.y.to_bytes(32, "big")
        if compressed:
            if (self.y % 2) == 0:
                return b"\x02" + x
            else:
                return b"\x03" + x
        else:
            return b"\x04" + x + y

class Wallet:

    def __init__(self, private_key = None):
        if private_key == None:
            self.private_key = self.new_private_key()
        else:
            self.private_key = private_key
        self.address_uncompressed = None
        self.address_compressed = None
        self.wif_uncompressed = None
        self.wif_compressed = None

    def new_private_key(self):
        return os.urandom(32)

    def get_address(self, compressed):
        SPEC256k1 = ECDSAPoint()
        pk = int.from_bytes(self.private_key, "big")
        hash160 = ripemd160(sha256((SPEC256k1 * pk).toBytes(compressed)))
        address = b"\x00" + hash160
        address = b58(address + sha256(sha256(address))[:4])
        return address

    def get_wif(self, compressed):
        if compressed:
            wif = b"\x80" + self.private_key + b"\x01"
        else:
            wif = b"\x80" + self.private_key
        wif = b58(wif + sha256(sha256(wif))[:4])
        return wif

    def get_address_uncompressed(self):
        if self.address_uncompressed == None:
            self.address_uncompressed = self.get_address(compressed = False)
        return self.address_uncompressed

    def get_address_compressed(self):
        if self.address_compressed == None:
            self.address_compressed = self.get_address(compressed = True)
        return self.address_compressed

    def get_wif_uncompressed(self):
        if self.wif_uncompressed == None:
            self.wif_uncompressed = self.get_wif(compressed = False)
        return self.wif_uncompressed

    def get_wif_compressed(self):
        if self.wif_compressed == None:
            self.wif_compressed = self.get_wif(compressed = True)
        return self.wif_compressed

if __name__ == "__main__":
    wallet = Wallet()
    print("----------")
    print("     Bitcoin Wallet Generator")
    print("----------")
    print("     WIF uncompressed     :", wallet.get_wif_uncompressed())
    print("     Address uncompressed :", wallet.get_address_uncompressed())
    print("----------")
    print("     WIF compressed       :", wallet.get_wif_compressed())
    print("     Address compressed   :", wallet.get_address_compressed())
    print("----------")
    print("     Karadocteur, from https://karadocteur.fr")
    print("----------")
    os.system("pause")

Télécharger le script

Voilà, le développement de ce script touche à sa fin. Je vous propose de télécharger le fichier directement via le lien ci-dessus ou d'accéder au projet sur GitHub. Il ne vous reste plus qu'à lancer le programme avec Python et vous obtiendrez de tout nouveaux wallets Bitcoin !

A partir de là, il est possible de copier-coller le couple adresse / clé privée (WIF) sur un document texte (type Bloc-note ou Word) et de l'imprimer ou de le conserver secrètement sur une clé USB, au même titre qu'un paper wallet.

Attention toutefois : stocker vos Bitcoins sur des wallets générés de cette manière est moins sécurisé que d'utiliser un hardware wallet, comme les Ledger nano par exemple.

Si vous avez des questions sur le fonctionnement de ce script, n'hésitez pas à les poser en commentaire ci-dessous ! A bientôt.