5

I'm trying to generate a keypair and reuse it as a payer with the new web3.js.

import { generateKeyPairSigner } from "@solana/web3.js";

const keypair = await generateKeyPairSigner();
console.log("keypair: ", keypair);

Which logs:

keypair:  {
  address: 'EpEN5FrT2wjp9gbLP9xRfMT2yMQWnv71oAPLNdfD81LT',
  keyPair: [Object: null prototype] {
    privateKey: CryptoKey {
      type: 'private',
      extractable: false,
      algorithm: { name: 'Ed25519' },
      usages: [ 'sign' ]
    },
    publicKey: CryptoKey {
      type: 'public',
      extractable: true,
      algorithm: { name: 'Ed25519' },
      usages: [ 'verify' ]
    }
  },
  signMessages: [Function: signMessages],
  signTransactions: [Function: signTransactions]
}

If I try to get the privatekey it throws the following error:

'key is not extractable', 'InvalidAccessException'

What is the recommended way to generate and save a keypair to reuse?

3 Answers 3

5

@solana/web3.js is designed to restrict access to private key bytes generated in-app. Its key generation APIs are only designed to be used to create ephemeral keys, such as those needed to sign for the creation of new accounts (eg. nonce accounts).

With that said, you still have options to save and retrieve keypairs that you've generated in-app.

Native CryptoKey

You can create the kind of CryptoKey instances compatible with @solana/web3.js without using @solana/web3.js – they are native to JavaScript.

const keypair = await crypto.subtle.generateKeypair(
    // The algorithm relevant to Solana.
    'Ed25519',
    // Whether you want private key exfiltration to be allowed.
    extractable,
    // The requisite usages for an `Ed25519` keypair.
    ['sign', 'verify'],
);

This, is what is used internally when you call the generateKeypair() method of @solana/web3.js. We hardcode extractable to false to make it impossible to obtain the bytes of the private key.

Saving a CryptoKey itself

Despite not being allowed to export the bytes of a non-extractable CryptoKey, you can save it in a browser using the IndexedDB API.

const dbPromise = indexedDB.open('MyDatabase', 1);
dbPromise.onupgradeneeded = e => {
    const db = e.target.result;
    db.createObjectStore('MyKeypairStore');
};

async function saveKey(keypair: CryptoKeyPair) {
    const db = (await dbPromise).result;
    return new Promise((resolve, reject) => {
        const request = db
            .transaction('MyKeypairStore', 'readwrite')
            .objectStore('MyKeypairStore')
            .put(keypair, 'myStoredKey');
        request.onsuccess = () => {
            resolve();
        };
        request.onerror = e => {
            reject(e.target.error);
        };
    }); 
}

async function getSavedKey() {
    const db = (await dbPromise).result;
    return new Promise((resolve, reject) => {
        const request = db
            .transaction('MyKeypairStore', 'readonly')
            .objectStore('MyKeypairStore')
            .get('myStoredKey');
        request.onsuccess = () => {
            if (request.result) {
                resolve(request.result);
            } else {
                reject('Not found');
            }
        };
        request.onerror = e => {
            reject(e.target.error);
        };
    });
}

This will only work in environments that support IndexedDB and Ed25519 keys, natively.

Exfiltrating the bytes of the private key

If you wish to be able to export the private key material directly, you will need to generate your own CryptoKey with extractable set to true.

const keypair = await crypto.subtle.generateKeypair(
    /* algorithm */   'Ed25519',
    /* extractable */ true,
    /* usages */      ['sign', 'verify'],
);

You can then use that CryptoKey with the crypto.subtle.exportKey() and crypto.subtle.importKey() APIs. This is not recommended, and you should take extreme care when doing so. The private key bytes exported through these APIs are vulnerable to theft (eg. by code running in your JavaScript sandbox) and accidental logging (eg. to the console or to a third-party logger).

1

Adding this snippet as reference for one way to generate and reuse the privatekey from a burner keypair. If someone finds a simpler way, please also share.

import {
  getBase58Decoder,
  createKeyPairSignerFromPrivateKeyBytes,
} from "@solana/web3.js";

// Generate a keypair
const keypair = await crypto.subtle.generateKey("Ed25519", true, [
  "sign",
  "verify",
]);
console.log("keypair", keypair);
// keypair [Object: null prototype] {
//   privateKey: CryptoKey {
//     type: 'private',
//     extractable: true,
//     algorithm: { name: 'Ed25519' },
//     usages: [ 'sign' ]
//   },
//   publicKey: CryptoKey {
//     type: 'public',
//     extractable: true,
//     algorithm: { name: 'Ed25519' },
//     usages: [ 'verify' ]
//   }
// }

// Export private key as JWK
const privateKeyJwk = await crypto.subtle.exportKey("jwk", keypair.privateKey);
console.log("privateKeyJwk", privateKeyJwk);
// privateKeyJwk {
//   key_ops: [ 'sign' ],
//   ext: true,
//   crv: 'Ed25519',
//   d: '6nwXBth_bVK0po7ub3rq4DW95Gp5V2k7KEHCwipfqjs',
//   x: '1tk7CFLGL2kOkwfVxqwsTMFcfk0k79UUmNtY1cJ0PyM',
//   kty: 'OKP'
// }

// The 'd' parameter in JWK contains the base64 private key
const privateKeyBase64 = privateKeyJwk.d;
if (!privateKeyBase64) throw new Error("Failed to get private key bytes");
console.log("privateKeyBase64", privateKeyBase64);
// privateKeyBase64 6nwXBth_bVK0po7ub3rq4DW95Gp5V2k7KEHCwipfqjs

// Decode base64url to get the raw 32 bytes private key
const privateKeyBytes = new Uint8Array(Buffer.from(privateKeyBase64, "base64"));
console.log("privateKeyBytes", privateKeyBytes);
// privateKeyBytes Uint8Array(32) [
//   234, 124,  23,   6, 216, 127, 109,  82,
//   180, 166, 142, 238, 111, 122, 234, 224,
//    53, 189, 228, 106, 121,  87, 105,  59,
//    40,  65, 194, 194,  42,  95, 170,  59
// ]

// Get public key and convert to base58
const publicKey = await crypto.subtle.exportKey("raw", keypair.publicKey);
const publicKeyBytes = new Uint8Array(publicKey);
const publicKeyBase58 = getBase58Decoder().decode(publicKeyBytes);
console.log("publicKey:", publicKeyBase58);
// publicKey: FTgNJ8XB4ajD1BYir1MjNMS1PyPvNo8eJkika8nWGh9k

// Create web3js KeyPairSigner
const keypairSigner = await createKeyPairSignerFromPrivateKeyBytes(
  privateKeyBytes
);
console.log("keypairSigner", keypairSigner);
// keypairSigner {
//   address: 'FTgNJ8XB4ajD1BYir1MjNMS1PyPvNo8eJkika8nWGh9k',
//   keyPair: {
//     privateKey: CryptoKey {
//       type: 'private',
//       extractable: false,
//       algorithm: { name: 'Ed25519' },
//       usages: [ 'sign' ]
//     },
//     publicKey: CryptoKey {
//       type: 'public',
//       extractable: true,
//       algorithm: { name: 'Ed25519' },
//       usages: [ 'verify' ]
//     }
//   },
//   signMessages: [Function: signMessages],
//   signTransactions: [Function: signTransactions]
// }
1
  • 1
    You can use getAddressFromPublicKey() for the public key -> base58 address part. Commented Nov 16 at 4:51
0

The function createKeyPairSignerFromBytes has a bool extractable that you need to set to true if you want to be able to access the private key.

What is use to reuse a keypair is this:

export async function loadDefaultKeypair(): Promise<CryptoKeyPair> {
  return await loadKeypairFromFile("~/.config/solana/id.json");
}

export async function loadDefaultKeypairWithAirdrop(
  cluster: string
): Promise<CryptoKeyPair> {
  const keypair = await loadDefaultKeypair();
  const rpc = createSolanaRpc(devnet(`https://api.${cluster}.solana.com`));
  const rpcSubscriptions = createSolanaRpcSubscriptions(
    devnet(`wss://api.${cluster}.solana.com`)
  );
  try {
    const result = await rpc
      .getBalance(await getAddressFromPublicKey(keypair.publicKey))
      .send();

    console.log(`Balance: ${result.value} lamports`);
    if (result.value < lamports(500_000n)) {
      console.log(`Balance low requesting airdrop`);
      const airdrop = airdropFactory({ rpc, rpcSubscriptions });
      await airdrop({
        commitment: "confirmed",
        lamports: lamports(1000_000n),
        recipientAddress: await getAddressFromPublicKey(keypair.publicKey),
      });
    }
  } catch (err) {
    console.error("Error fetching balance:", err);
  }
  return keypair;
}

export async function loadKeypairFromFile(
  filePath: string
): Promise<CryptoKeyPair> {
  const resolvedPath = path.resolve(
    filePath.startsWith("~") ? filePath.replace("~", os.homedir()) : filePath
  );
  const loadedKeyBytes = Uint8Array.from(
    JSON.parse(fs.readFileSync(resolvedPath, "utf8"))
  );
  const keypair = await createKeyPairFromBytes(loadedKeyBytes);
  return keypair;
}
2
  • what do you use to generate and save the keypair initially? createKeyPairFromBytes assumes you already have one you're loading
    – john
    Commented Nov 11 at 17:07
  • Oh i used solana keygen. I forwarded to luscher he probably knows
    – Jonas H.
    Commented Nov 12 at 10:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.