Storing HD wallet seed phrases is stressful. Current methods, including paper and offline storage, have risks of misplacement and hacking, leading to significant cryptocurrency losses.
Our solution is a user-friendly decentralized seed phrase manager for Cardano. It encrypts seed phrases using On-Chain Encrypted Storage, enhancing security and convenience. By distributing seed phrases on the blockchain, the solution mitigates the risks associated with centralized storage, making it challenging for a single entity to compromise multiple seed phrases.
To secure the user's Seed Phrase,
- The user interface would allow the user to either generate a new seed phrase or put in an existing seed phrase they want secure.
- The user submits 23/24 seed words, keeping 1 hidden for extra security.
- Index of the hidden word and passphrase are provided.
- Personal info (hashed) is added to the script to produce parameterized contracts for UTxO identification.
- The Dapp encrypts 23 words and their respective indexes using AES encryption using the user passphrase + seed + index as the encryption key, and then stores the encrypted result int the datum of a UTxO on-chain.
This solution will build a parameterized Smart contract on the Cardano blockchain that will use the hashed personal information as a parameter to personalize the script in which the UTxO securing the Seed Phrase in it's Datum.
- Parameterized smart contracts on Cardano Blockchain written in Plutus/Plutarch are used to secure the UTxO.
- The UTxO in parameterized script is used mainly to secure the UTxO, with an option to withdraw the min ADA if needed.
- The Off-chain code is written using Lucid for AES encryption and UTxO locking.
The following information would be required for the Seed Phrase recovery:
- Passphrase/password.
- 1 hidden word from the Seed Phrase + it's index.
- Personal Information used to find the Script holding the UTxO where the Seed Phrase is secure.
To ensur a safe recovery mechanism, the users would recover their Seed Phrases by providing the key i.e their passphrase + 1 seed word + word index and personal information in the exact order to find and to decrypt the rest of the 23 words and their respective indexes. At the point of recovery, the off-chain component uses this provided information to find the parameterized script in which the encrypted seed phrase is secured on the Cardano blockchain using the hash of the user's personal information and seed phrase is then decrypted using the AES encryption algorithm.
- It offers an easy-to-use, and secure recovery mechanism for lost seed phrases.
- It Simplifies information security and storage for recovery, encouraging users to define convenient and secure passphrases and personal information rather than storing their seed phases directly on their computer, on a piece of paper, using some other insecure/expensive storage mechanism or depending on expensive centralized solutions.
- Enhances security by allowing multiple storage locations without compromising funds.
This solution will use the password + 1 word left out with it's index as the encryption key for the encryption process to secure the rest of the 23 words using the AES (Advanced Encryption Standard) Encryption Algorithm.
Illustrated below is an example. In this example our encrypted string is U2FsdGVkX1+tz5nkrdI4eX34/tBFPy+MeSM2AHTTU2A+CEyqORTdZXqPF5TVXBfn
The User would keep the recovery info of user Password, 1 word and index and the personal information provided safely in any normal cloud storage or other methods, where its easily recoverable with 2FA authentication knowing that this piece of information isn't used for wallet recovery on its own and pose no meaning to anyone who finds it apart from the user.
To increase the computational cost against Brute Force attacks, we will be implementing a 3-stage recursive encryption process.
This solution will use 100 times + index of word times recursive encryption. The idea is to increase the decryption computational time cost to make Brute Force algorithms ridiculously expensive to attempt.
U2FsdGVkX1+tz5nkrdI4eX34/tBFPy+MeSM2AHTTU2A+CEyqORTdZXqPF5TVXBfn
U2FsdGVkX1+9Y/FrplmURq+6DxLddfn9lZx9zWka6yLydnXhqNBdX3DcNOGmleaq45kP3XuW/Oi2syaONiioXyE0O1te/9tC1NH4ITp2iAzbFsWzWFkaLtSKA19R80dL
Step 3: USing the output of the 2nd encryption process
U2FsdGVkX19B1Qg9uG4LBlXTJ0I695Hj/ys9kRgUukgH7PTtESyIpEYFZRASyIt+JhKBBxsg/d7punQLGEGXcgOuTHPhgiJwgV4tANa7scKtRp2FJoJkjEDLhs3YG8K3DDUZ93Rs5t2Mozu1tK261Mp0Q2J4tvyir9a5agn6796EiIHmdakzfK1yPMrEqo3XC+KWhjZqoa4Lkusc3dzIQ==
To get back the original 23 words.
https://github.com/rchak007/plutusAppsJambhala/blob/main/src/Contracts/SeedPhraseManager.hs
data SeedPhraseDatum = SeedPhraseDatum
{ encryptedWordsWithIndex :: BuiltinByteString,
ownerPKH :: PubKeyHash
}
unstableMakeIsData ''SeedPhraseDatum
data SeedPhraseParam = SeedPhraseParam
{ pInfoHash :: BuiltinByteString
}
deriving (Haskell.Show)
data SeedPhraseRedeem = Unit ()
mkRequestValidator :: SeedPhraseParam -> SeedPhraseDatum -> () -> ScriptContext -> Bool
mkRequestValidator sParam dat _ ctx =
traceIfFalse "signedByOwner: Not signed by ownerPKH" signedByOwner
where
txinfo :: TxInfo
txinfo = scriptContextTxInfo ctx
signedByOwner :: Bool
signedByOwner = txSignedBy txinfo $ ownerPKH dat
{-# INLINEABLE mkRequestValidator #-}
-- referenceInstance :: Scripts.Validator
referenceInstance :: Validator
-- referenceInstance = Api.Validator $ Api.fromCompiledCode $$(PlutusTx.compile [||wrap||])
referenceInstance = Validator $ fromCompiledCode $$(PlutusTx.compile [||wrap||])
where
-- wrap l1 = Scripts.mkUntypedValidator $ mkRequestValidator (PlutusTx.unsafeFromBuiltinData l1)
wrap l1 = mkUntypedValidator $ mkRequestValidator (PlutusTx.unsafeFromBuiltinData l1)
referenceSerialized :: Haskell.String
referenceSerialized =
C.unpack $
B16.encode $
serialiseToCBOR
-- ((PlutusScriptSerialised $ SBS.toShort . LBS.toStrict $ serialise $ Api.unValidatorScript referenceInstance) :: PlutusScript PlutusScriptV2)
((PlutusScriptSerialised $ SBS.toShort . LBS.toStrict $ serialise $ unValidatorScript referenceInstance) :: PlutusScript PlutusScriptV2)
To get the .plutus script
https://github.com/rchak007/plutusAppsJambhala/blob/main/src/Deploy.hs
scripts :: Scripts
scripts = Scripts {reference = Contracts.SeedPhraseManager.referenceSerialized}
main :: IO ()
main = do
-- writeInitDatum
-- writeContractDatum
-- _ <- writeValidatorScript
-- _ <- writeLucidValidatorScript
I.writeFile "scripts.json" (encodeToLazyText scripts)
putStrLn "Scripts compiled"
return ()
https://github.com/rchak007/decentralSeedRecover/blob/main/pages/offChainV2.tsx
const ParamsSchema = Data.Tuple([
Data.Object({
pInfoHash: Data.Bytes()
})
]);
type Params = Data.Static<typeof ParamsSchema>;
const Params = ParamsSchema as unknown as Params;
const MyDatumSchema =
Data.Object({
encryptedWordsWithIndex: Data.Bytes(),
ownerPKH: Data.Bytes()
})
type MyDatum = Data.Static<typeof MyDatumSchema>;
const MyDatum = MyDatumSchema as unknown as MyDatum;
const Redeemer = () => Data.void();
// Function that will Lock the UTXO with this Datum
const sLockEncryptedSeedPhrase = async () => {
....
....
Gets the personal info from UI and applies the hash to Plutus script.
const decentralSeedPlutus = "59087f5908...."
const paramInit : Params = [{
pInfoHash: fromText(hashedDataInString)
}];
const sValidator : SpendingValidator = {
type: "PlutusV2",
script: applyParamsToScript<Params>(
decentralSeedPlutus,
paramInit,
Params,
),
}
seed23Words = seed23InputValue + ' ' + indexInputValue;
passPhrase = passPhraseinputValue;
const encryptedS = encryptD( seed23Words, passPhrase)
// const encryptedS = encryptSeedPassInfo( seed23Words, passPhrase);
console.log("encryptedS = ", encryptedS)
// const encryptedString = String(encryptedS)
const encryptedString = "testSample1"
const datumInit : MyDatum =
{
encryptedWordsWithIndex: fromText(encryptedS) ,
ownerPKH: paymentCredential?.hash! // pubkey hash
};
...
// Encrypt function
function encryptD(text: string, key: string): string {
const encrypted = CryptoJS.AES.encrypt(text, key);
return encrypted.toString(); // Convert to Base64 string
}
...
....
const tx = await lucid.newTx()
.payToContract(sValAddress, {inline: Data.to( datumInit, MyDatum)}, {lovelace: BigInt(2000000)})
.complete();
const signedTx = await tx.sign().complete();
const txHash = await signedTx.submit();
console.log("Lock Test TxHash: " + txHash)
settxHash(txHash);
return txHash;
Function const sDecentralSeedRedeem = async () => {
decrypts the encrypted seed phrase.
const sDecentralSeedRedeem = async () => {
Gets the personal info from UI and applies the hash to Plutus script to get the script address where UTXO is stored
const decentralSeedPlutus = "59087f5908...."
const paramInit : Params = [{
pInfoHash: fromText(hashedDataInString)
}];
const sValidator : SpendingValidator = {
type: "PlutusV2",
script: applyParamsToScript<Params>(
decentralSeedPlutus,
paramInit,
Params,
),
}
// this gets our address
const sValAddress = lucid.utils.validatorToAddress(sValidator)
....
// we get the UTXO's at this address
const valUtxos = await lucid.utxosAt(sValAddress)
....
for ( let i=0; i<valUtxos.length; i++ ) {
console.log("I = ", i)
const curr = valUtxos[i]
console.log("Curr on i = ", curr)
console.log("Curr datum on i = ", curr)
if (!curr.datum) {
console.log ("came here after the 1st IF")
if (!curr.datumHash) {
console.log ("came here after the 1st IF")
continue;
}
}
const encryptedWordsWithIndexFound = utxoInDatum.encryptedWordsWithIndex
const ownerPubKeyHashFound = utxoInDatum.ownerPKH
console.log("Encrypted words = ", encryptedWordsWithIndexFound)
console.log("Owner pubkeyHash = ", ownerPubKeyHashFound)
// Now Decrypt from the Encrypted word on Datum
// const decryptedS = decryptD( toText(utxoInDatum.encryptedWordsWithIndex), passPhrase)
const decryptedS = decryptD( toText(encryptedWordsWithIndexFound), passPhrase)
console.log("Decrupted 23 words with Index = ", decryptedS)
varDecryptWord = decryptedS;
const decryptWord = varDecryptWord;
// console.log("decryptWord output = ", decryptWord)
setdecryptWord(varDecryptWord); // set the output retrieved words
// Decrypt function
function decryptD(encryptedText: string, key: string): string {
// const keyWordArray = CryptoJS.enc.Utf8.parse(key);
const decrypted = CryptoJS.AES.decrypt(encryptedText, key);
// const decrypted = CryptoJS.PBKDF2(encryptedText, key);
return decrypted.toString(CryptoJS.enc.Utf8);
}
After providing the 23 / 24 words of seed phrase, index of left out word, Pass phrase for recovery and personal info (for Unique script address) the Dapp will put the encrypted seed phrase with index onChain as Datum.
User will provide the Personal info to locate the script address, the passphrase to decrypt the encrypted words.
This will decrypt the seed phrase and also redeem the min Ada stored at the script.
This way the user has now recovered their lost Seed phrase.