[Design] Automatically lock wallet due to inactivity
Author: Alex Ruzenhack [email protected] Reviewers: André Carneiro, Pedro Ferreira
Motivation
Add an extra level of security for the user by protecting the wallet in case of inactivity, avoiding a malicious agent to operate the wallet in the absence of its owner.
Introduction
What inactivity means for our purpose? A person can be using the computer but not using the wallet; a person can be out of the computer and leave the wallet open; a person can have the wallet open as the primary window, but just looking the screen without interacting with the wallet.
"Activity" presumes the action of interacting with the wallet, if the interaction ceases then the wallet is not being used and it can be marked as inactive. However, how many times do we need to mark the wallet as inactive?
The time span of no interaction that characterizes inactivity in the wallet has an inverse relation with the protection of the wallet. We can say the smaller the time span, the more protected the wallet will be. On the other hand, the time span also has a direct relation with convenience. The smaller the time span, the smaller the convenience for the user.
The time span to lock the wallet should provide a new layer of protection for the user at their discretion of the desired convenience.
Proposed solution
Taking activity by interaction, we should monitor each interaction with the wallet: click, touch, roll, type, tab navigation; establish an elapsed time for the last interaction and verify periodically if the elapsed time has reached the time span threshold for inactivity, then lock the wallet if reached.
Guide-level explanation
Global interaction event listener
We can set an event listener for each targeted interaction in the application element level, which means any event bubbling up will be captured if not halted by stopPropagation. Every time an event is processed the interaction time is updated.
Last interaction state
We should create a state called lastInteractionAt to hold the timestamp of the last interaction with the wallet in Redux.
Inactivity threshold state
We should create a state called inactivityThreshold to hold the time in seconds that characterizes the inactivity in the wallet. This state can be populated with a default value of 300 seconds (5min).
Lock function
A function to lock the wallet is to be run as a callback for setTimeout, which should set timeout as inactivityThreshold value. For every interaction, the timeout is canceled and set again with the current inactivityThreshold value.
Hardware wallet lock
The lock function in this context will redirect the user to the screen wallet type selection.
Jitter protection
To avoid flooding the event processing a jitter must be placed in the event listeners. Or maybe not, to avoid over-engineering.
Reference-level Explanation
Global interaction state
We can add an event listener in the Root element by the component reference and use the hook useEffect to add the listener and program its removal once the component is unmounted.
Last interaction state
We can initialize the state lastInteractionAt in the initial state of Redux with the value of the timestamp in UTC as new Date().getTime().
const initialState = {
// last user interaction with the wallet
lastInteractionAt: new Date().getTime(),
...
}
Inactivity threshold state
We can initialize the state inactivityThreshold with a default value that can be a configured constant as DEFAULT_INACTIVITY_THRESHOLD with the value of 300 seconds.
const initialState = {
// last user interaction with the wallet
inactivityThreshold: DEFAULT_INACTIVITY_THRESHOLD,
...
}
Lock function
Software wallet
Should call the lock function from HathorLib.
Hardware wallet
Should navigate to the page "wallet type selection".
Interaction listener
The interaction listener should update lastInteractionAt state and set the timeout for the lock function. However, for the lock function, it must be set only if the wallet status is ready.
Hardware wallet
When the wallet is used with a device like Ledger, the wallet also listens to the device's events, therefore these events also need to be hooked to update lastInteractionAt and schedule or reschedule the lock function.
Close event
When the Ledger device is inactive with Hathor App for 30 sec, an event of ledger:closed (see ledger.js:124) is emitted in the electron. This event is captured on a listener defined at App.js (see App.js:81), which updates the state ledgerWasClosed.
- What is the meaning of
ledgerWasClosed: truefor the wallet renderization state? (see WalletType.js:92) - Does it impact on redirection to "wallet type selection"?
Assess the impact of Multiple Wallets PR
See feat: Add support for multiple wallets.
Software wallet
When the wallet is loaded the function hathorLib.wallet.markWalletAsStarted() is called. Therefore there is no real change here because we already consider triggering the setTimeout with the lock function once the wallet is ready/started.
See LoadWallet.js:104.
Hardware wallet
When initializing the wallet, in the handlePublicKeyData logic there is a call to hathorLib.wallet.unlock(), we may use it to trigger the first interaction with the wallet and set the setTimeout with the lock function.
See StartHardwareWallet.js:129.
When adding a wallet
In this situation, the setTimout for the lock function should be canceled, to avoid a lock call in the middle of a new wallet load.
See ChooseWallet.js:69(addWallet).
When going to hardware wallet
As this type of wallet if more ephemeral for the application, the setTimeout in this situation also needs to be canceled.
See ChooseWallet.js:79(goToHardwareWallet).
Task Breakdown
- Implement a global event handler to capture user interaction with the wallet, update
lasInteractionAtstate, and schedule/reschedule the lock function. 1 dev day - Implement a selector for options of inactive threshold values in the settings page, and update the state
inactivityThreshold. 1 dev day - Add documentation for feature usage from the user's perspective. 0.5 dev day
Future work
- Implement reactions for power-monitor events, like capturing the screen lock event to lock the wallet as well without the need to wait the inactivity time threshold.
- Should we implement the configurable
inactiveThreshold? If so, let the user choose a larger time for inactivity depending on battery conditions.
Alternative solutions
If the meaning of “inactivity“ is other than “interaction” we could think of it as “visible activity” and use window information to handle inactivity.
Active window
Native solution through visibility of window
As documented in “Page visibility”, the visibility property is not consistent, therefore it will be tricky to implement any feature relying on it. I think it is better to avoid it.

Read at: BrowserWindow | Electron (electronjs.org)
External solution through **electron-active-window package
The project https://github.com/nullxx/electron-active-window has few stars, and downloads, and the last maintenance was 15 months ago. Despite these facts, there is a testimony very interesting about compatibility with macOS and privacy settings.
We should consider this path only if we intend to give maintenance to the package.

Read at: Comparison with active-win lib · Issue #3 · nullxx/electron-active-window (github.com)
Design for the issue https://github.com/HathorNetwork/hathor-wallet/issues/80
I like the design, I just have some comments
Inactivity threshold state
I don't see any downsides of this being a redux state but I feel that this could be a constant in our constants.js file.
Inactivity checker
Where should we implement this checker? Should we create a clearInterval as well, or maybe just accept that this will be running until the wallet application is closed.
Apart from that, I think the wallet checker should lock the wallet only if the wallet is loaded. Imagine if you are starting a new wallet and while you are writing down your seed (it took you some minutes to get a pen and paper), then the screen changes automatically. So it's a simple check that we have in the lib wallet.loaded() that would prevent any problems on that case.
Also the hardware wallet is a bit tricky because we don't have a PIN (and a locked wallet for that), but we can send the user back to the wallet type selection screen.
I don't see any downsides of this being a redux state but I feel that this could be a constant in our
constants.jsfile.
Sure! I though to initialize the state with a constant DEFAULT_INACTIVITY_THRESHOLD, it can be set in the constants.js indeed. What do you think? We can set value 300 meaning 300 seconds (5 minutes).
Where should we implement this checker? Should we create a
clearIntervalas well, or maybe just accept that this will be running until the wallet application is closed.
Apart from that, I think the wallet checker should lock the wallet only if the wallet is loaded. Imagine if you are starting a new wallet and while you are writing down your seed (it took you some minutes to get a pen and paper), then the screen changes automatically. So it's a simple check that we have in the lib
wallet.loaded()that would prevent any problems on that case.
I agree we should trigger the inactivity checker only after the wallet be loaded.
But I have a question: what is the difference between when the wallet is loaded for when the wallet is ready?
Also the hardware wallet is a bit tricky because we don't have a PIN (and a locked wallet for that), but we can send the user back to the wallet type selection screen.
Oh! Thank you for this context, I was totally unaware. But yep, this works fine.
However I would like to propose in this case a page in which the user can insert their passphrase and we validate, if it matchs with the wallet we have saved, then it is unlock to be used. What do you think?
I like the design, but what are the downsides of using a simpler setTimeout to lock and any interaction just re-schedules the timeout? This would eliminate the need for a checker and the "multiple of 5" requirement.
But I have a question: what is the difference between when the wallet is loaded for when the wallet is ready?
They are the same. The wallet loaded/ready means that the user is interacting with a specific wallet, not importing/creating one.
However I would like to propose in this case a page in which the user can insert their passphrase and we validate, if it matchs with the wallet we have saved, then it is unlock to be used. What do you think?
I didn't understand this. We don't have passphrase when using the hardware wallet.
I like the design, but what are the downsides of using a simpler
setTimeoutto lock and any interaction just re-schedules the timeout? This would eliminate the need for a checker and the "multiple of 5" requirement.
I like this approach. I agree!
They are the same. The wallet loaded/ready means that the user is interacting with a specific wallet, not importing/creating one.
Got it.
Also the hardware wallet is a bit tricky because we don't have a PIN (and a locked wallet for that), but we can send the user back to the wallet type selection screen.
Ok, understood. I need to get more context about the hardware wallet.
We don't have a passphrase when using the hardware wallet.
Humm. Ok. Let's use your suggestion. But why we do not have a passphrase in this case? There are any material I can read about this decision?
Summary
Inactivity checker (drop)
We can drop it as suggested by @r4mmer in favor of a setTimeout with a lock function to be run after elapsed the inactive treshold from the lastInteractionAt.
Hardware wallet lock
The lock function in this context will redirect the user to the screen wallet type selection.
We agree with the change in the Summary? If yes, I will proceed with the Reference-level explanation for these topics.
We agree with the change in the Summary? If yes, I will proceed with the Reference-level explanation for these topics.
For me it's good.
But why we do not have a passphrase in this case? There are any material I can read about this decision?
Are you talking about the passphrase or the password? The passphrase is set in the ledger when using the hardware wallet.
But why we do not have a passphrase in this case? There are any material I can read about this decision?
It is not a decision on our part, the Ledger is a device which holds the seed and no external software can access it we have methods to confirm addresses by their index (to check they are from the wallet), sign transactions (we send the transaction and it comes back signed) and other methods, but the pin is on the Ledger device and not on our wallet.
The user unlocks the device with the pin then connects to our desktop wallet (so that it can make requests to it), so we don't have the need for pin and password on the desktop wallet. In other words, the desktop wallet becomes a GUI for the Hathor app on the Ledger device.
If you want to learn more, theres a Ledger developer portal and to a lesser extent there's the Ledger academy but the academy is just a group of posts explaining web3 and the crypto concepts, the developer portal is more focused on the device itself.
For me this is approved.
✅ Approved.