mirror of
https://github.com/status-im/status-keycard-go.git
synced 2025-02-06 11:33:32 +00:00
feat: Session API (#13)
* feat: KeycardContextV2 * feat: ApplicationInfoV2 with string version * feat: rpc service * feat: http server for the session API * feat: ensure api mutual exclusivity * chore: go mod tidy
This commit is contained in:
parent
6e63788d31
commit
efec05e104
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,5 +1,7 @@
|
||||
.vscode
|
||||
.idea
|
||||
.idea/*
|
||||
!.idea/runConfigurations
|
||||
/keycard
|
||||
/build
|
||||
/status-keycard-go
|
||||
/api/http-client.private.env.json
|
||||
|
12
.idea/runConfigurations/status_keycard_server.xml
generated
Normal file
12
.idea/runConfigurations/status_keycard_server.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="status-keycard-server" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="status-keycard-go" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="--address=localhost:12346" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/status-im/status-keycard-go/cmd/status-keycard-server" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/cmd/status-keycard-server/main.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
30
README.md
Normal file
30
README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# status-keycard-go
|
||||
|
||||
This library provides a higher level Keycard API for Status app. It is currently only used in [status-desktop](https://github.com/status-im/status-desktop/).
|
||||
|
||||
There are 2 types of API provided.
|
||||
|
||||
## Flow API
|
||||
|
||||
Each keycard command is executed in a single _flow_. A flow roughly looks like this:
|
||||
1. List available readers
|
||||
2. Look for a keycard
|
||||
3. Set up a connection
|
||||
4. Execute the command
|
||||
5. Close the connection
|
||||
|
||||
If client interaction is required at any stage (e.g. insert a card, input a PIN), the flow is "paused" and signals to the client. The client should manually continue the flow when the required action was performed. This basically drives the UI right from `status-keycard-go` library.
|
||||
|
||||
> [!NOTE]
|
||||
> status-desktop doesn't use this API anymore. Consider switching to Session API.
|
||||
|
||||
## Session API
|
||||
|
||||
The main problem with Flow API is that it does not signal certain changes, e.g. "reader disconnected" and "card removed". Session API addresses this issue.
|
||||
|
||||
The journey begins with `Start` endpoint. When the keycard service is started, it monitors all connected readers and cards. This allows to track the state of reader+card and notify the client on any change. As soon as a keycard is found, a "connection" (pair and open secure channel) is established automatically and will be reused until `Stop` is called or the keycard is removed.
|
||||
|
||||
In the `Ready`/`Authorized` states client can execute wanted commands, each as a separate endpoint.
|
||||
|
||||
Check out the detailed usage in ./api/README.md
|
||||
|
12
api/Authorize.http
Normal file
12
api/Authorize.http
Normal file
@ -0,0 +1,12 @@
|
||||
# @name Authorize
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.Authorize",
|
||||
"params": [
|
||||
{
|
||||
"pin": "654321"
|
||||
}
|
||||
]
|
||||
}
|
12
api/ChangePIN.http
Normal file
12
api/ChangePIN.http
Normal file
@ -0,0 +1,12 @@
|
||||
# @name ChangePIN
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.ChangePIN",
|
||||
"params": [
|
||||
{
|
||||
"newPIN": "654321"
|
||||
}
|
||||
]
|
||||
}
|
12
api/ChangePUK.http
Normal file
12
api/ChangePUK.http
Normal file
@ -0,0 +1,12 @@
|
||||
# @name ChangePUK
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.ChangePUK",
|
||||
"params": [
|
||||
{
|
||||
"newPuk": "654321654322"
|
||||
}
|
||||
]
|
||||
}
|
8
api/ExportLoginKeys.http
Normal file
8
api/ExportLoginKeys.http
Normal file
@ -0,0 +1,8 @@
|
||||
# @name ExportLoginKeys
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.ExportLoginKeys",
|
||||
"params": []
|
||||
}
|
8
api/ExportRecoverKeys.http
Normal file
8
api/ExportRecoverKeys.http
Normal file
@ -0,0 +1,8 @@
|
||||
# @name ExportRecoverKeys
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.ExportRecoverKeys",
|
||||
"params": []
|
||||
}
|
8
api/FactoryReset.http
Normal file
8
api/FactoryReset.http
Normal file
@ -0,0 +1,8 @@
|
||||
# @name FactoryReset
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.FactoryReset",
|
||||
"params": []
|
||||
}
|
12
api/GenerateMnemonic.http
Normal file
12
api/GenerateMnemonic.http
Normal file
@ -0,0 +1,12 @@
|
||||
# @name Initialize
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.GenerateMnemonic",
|
||||
"params": [
|
||||
{
|
||||
"length": 12
|
||||
}
|
||||
]
|
||||
}
|
8
api/GetMetadata.http
Normal file
8
api/GetMetadata.http
Normal file
@ -0,0 +1,8 @@
|
||||
# @name GetMetadata
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.GetMetadata",
|
||||
"params": []
|
||||
}
|
8
api/GetStatus.http
Normal file
8
api/GetStatus.http
Normal file
@ -0,0 +1,8 @@
|
||||
# @name GetStatus
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.GetStatus",
|
||||
"params": []
|
||||
}
|
13
api/Initialize.http
Normal file
13
api/Initialize.http
Normal file
@ -0,0 +1,13 @@
|
||||
# @name Initialize
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.Initialize",
|
||||
"params": [
|
||||
{
|
||||
"pin": "654321",
|
||||
"puk": "654321654321"
|
||||
}
|
||||
]
|
||||
}
|
12
api/LoadMnemonic.http
Normal file
12
api/LoadMnemonic.http
Normal file
@ -0,0 +1,12 @@
|
||||
# @name LoadMnemonic
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.LoadMnemonic",
|
||||
"params": [
|
||||
{
|
||||
"mnemonic": "tiger worth relief food limb glad recycle similar wreck work region uncover"
|
||||
}
|
||||
]
|
||||
}
|
190
api/README.md
Normal file
190
api/README.md
Normal file
@ -0,0 +1,190 @@
|
||||
> [!NOTE]
|
||||
> This guide is not comprehensive and relies on your wisdom and intelligence.
|
||||
> Only _session_ API is considered. For the _flow_ API please check out the source code.
|
||||
|
||||
# Description
|
||||
|
||||
This directory contains `*.http` request for each available endpoint in the Session API.
|
||||
|
||||
# Usage
|
||||
|
||||
Session API uses JSON-RPC protocol. All commands are available at `keycard` service. Here is an example:
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"method": "keycard.Authorize",
|
||||
"params": [
|
||||
{
|
||||
"pin": "654321"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
There are 2 ways to access the API.
|
||||
|
||||
## HTTP
|
||||
|
||||
This way is easier to use for testing and debugging.
|
||||
|
||||
1. Run the server:
|
||||
```shell
|
||||
go run ./cmd/status-keycard-server/main.go --address=localhost:12346
|
||||
```
|
||||
|
||||
2. Connect to signals websocket at `ws://localhost:12346/signals`
|
||||
|
||||
3. Send requests to `http://localhost:12346/rpc`
|
||||
|
||||
## C bindings
|
||||
|
||||
This is the way to integrate `status-keycard-go` library, e.g. how `status-desktop` uses it.
|
||||
|
||||
To subscribe to signals, set a callback function with `setSignalEventCallback`.
|
||||
|
||||
For the RPC server, there are 2 methods provided:
|
||||
1. `InitializeRPC` - must be called once at the start of the application, before making any RPC calls.
|
||||
2. `CallRPC` - call with a single JSON string argument according to the JSON-RPC protocol. Returns a single JSON string response.
|
||||
|
||||
|
||||
# Setup
|
||||
|
||||
1. Connect to signals
|
||||
For the session API, the only emitted signal is `status-changed`.
|
||||
It provides current status of the session and information about connected keycard.
|
||||
2. Call `Start`
|
||||
From this moment, until `Stop` is called, the keycard service will take care of watching readers/cards and keeping a secure "connection" with a keycard.
|
||||
Provide `StorageFilePath`, e.g. `~/pairings.json`. This file will be used to store pairing keys for all paired keycards.
|
||||
3. If planning to execute any authorization-required commands, call `Authorize`
|
||||
4. Monitor state of the session, execute any needed commands.
|
||||
NOTE: Some of the keycard commands can only be executed in `ready` or `authorized` state.
|
||||
5. Call `Stop`
|
||||
|
||||
# Simulation
|
||||
|
||||
Because it is difficult (perhaps nearly impossible) to implement proper simulation of a keycard,
|
||||
this library provides a way to simulate certain errors, which is not simple/possible to achieve with hardware.
|
||||
|
||||
Check [`SimulateErrro`](#simulateerror) method for details
|
||||
|
||||
# API
|
||||
|
||||
## Signals
|
||||
|
||||
Signals follow the structure described here: https://github.com/keycard-tech/status-keycard-go/blob/b1e1f7f0bf534269a5c18fcd31649d2056b13e5b/signal/signals.go#L27-L31
|
||||
|
||||
The only signal type used in Session API is `status-changed`. For event structure, check out [Status](#status)
|
||||
|
||||
## Service endpoints
|
||||
|
||||
These endpoints are related to the `status-keycard-go` library itself:
|
||||
|
||||
## `Start`
|
||||
|
||||
Starts the monitoring of readers and cards.
|
||||
|
||||
The monitoring starts with _detect_ mode.
|
||||
In this mode it checks all connected readers for a smart cards. Monitoring supports events (like reader connection and card insertion) to happen even after calling `Start`.
|
||||
|
||||
As soon as a reader with a Keycard is found, the monitoring switches to _watch_ mode:
|
||||
- Only the reader with the keycard is watched. If the keycard is removed, or the reader is disconnected, the monitoring goes back to _detect_ mode.
|
||||
- Any new connected readers, or inserted smart cards on other readers, are ignored.
|
||||
|
||||
[//]: # (TODO: Diagram)
|
||||
|
||||
## `Stop`
|
||||
|
||||
Stops the monitoring.
|
||||
|
||||
## `SimulateError`
|
||||
|
||||
Marks that certain error should be simulated.
|
||||
|
||||
For the `simulated-not-a-keycard` error, `InstanceUID` argument must be provided. Only keycards with such `InstanceUID` will be treated as not keycards.
|
||||
Other errors are applied no matter of the `InstanceUID` value.
|
||||
|
||||
`SimulateError` can also be called before `Start`, e.g. to simulate `simulated-no-pcsc` error, as this one can only happen during `Start` operation.
|
||||
|
||||
Use `SimulateError` method with one of the supported simulation errors: https://github.com/keycard-tech/status-keycard-go/blob/a3804cc8848a93a277895e508dd7c423f1f8338c/internal/keycard_context_v2_state.go#L55-L62
|
||||
|
||||
## `GetStatus`
|
||||
|
||||
Returns current status of the session.
|
||||
|
||||
In most scenarios `status-changed` signal should be used to get status. Yet in some debugging circumstances this method can be handy to get the latest status.
|
||||
|
||||
## Status
|
||||
|
||||
Here is the structure of the status: https://github.com/keycard-tech/status-keycard-go/blob/a3804cc8848a93a277895e508dd7c423f1f8338c/internal/keycard_context_v2_state.go#L30-L35
|
||||
|
||||
The main field is `state`
|
||||
|
||||
### State
|
||||
|
||||
Check the source code for the list of possible states and their description.
|
||||
https://github.com/keycard-tech/status-keycard-go/blob/75b1c9eac08de708724e2ee36909764fcafa858e/internal/keycard_context_v2_state.go#L9-L71
|
||||
|
||||
## Commands
|
||||
|
||||
Apart from the service endpoints listed above, all other endpoints represent the actual [Keycard API](https://keycard.tech/docs/apdu).
|
||||
|
||||
Most of the commands have to be executed in `ready` or `authorized` states. Service will return a readable error if the keycard is not in the proper state for the command.
|
||||
|
||||
Please check out the Keycard documentation for more details.
|
||||
|
||||
## Examples
|
||||
|
||||
The examples are presented in a "you'll get it" form.
|
||||
`<-` represents a reception of `status-changed` signal.
|
||||
|
||||
### Initialize a new Keycard
|
||||
|
||||
```go
|
||||
Start("~/pairings.json")
|
||||
<- "waiting-for-reader"
|
||||
// connect a reader
|
||||
<- "waiting-for-card"
|
||||
// insert a keycard
|
||||
<- "connecting-card"
|
||||
<- "empty-keycard", AppInfo: { InstanceUID: "abcd" }, AppStatus: null
|
||||
Initialize(pin: "123456", puk: "123456123456")
|
||||
<- "ready", Appinfo: ..., AppStatus: { remainingAttemptsPIN: 3, remainingAttemptsPUK: 5, ... }
|
||||
Authorize(pin: "123456")
|
||||
<- "autorized", AppInfo: ..., AppStatus ...
|
||||
ExportLoginKeys()
|
||||
```
|
||||
|
||||
### Unblock a Keycard
|
||||
|
||||
```go
|
||||
Start("~/pairings.json")
|
||||
<- "waiting-for-reader"
|
||||
// connect a reader with a keycard
|
||||
<- "connecting-card"
|
||||
<- "blocked-pin", AppInfo: { InstanceUID: "abcd" }, AppStatus: { remainingAttemptsPIN: 0, remainingAttemptsPUK: 5, ... }
|
||||
UnblockPIN(puk: "123456123456")
|
||||
<- "authorized", AppInfo: ..., AppStatus: { remainingAttemptsPIN: 3, remainingAttemptsPUK: 5, ... }
|
||||
```
|
||||
|
||||
### Factory reset a completely blocked Keycard
|
||||
|
||||
```go
|
||||
Start("~/pairings.json")
|
||||
<- "waiting-for-reader"
|
||||
// connect a reader with a keycard
|
||||
<- "connecting-card"
|
||||
<- "blocked-puk", AppInfo: { InstanceUID: "abcd" }, AppStatus: { remainingAttemptsPIN: 0, remainingAttemptsPUK: 0, ... }
|
||||
FactoryReset()
|
||||
<- "factory-resetting"
|
||||
<- "empty-keycard"
|
||||
```
|
||||
|
||||
# Implementation decisions
|
||||
|
||||
1. Monitoring detect mode utilizes [`\\?PnP?\Notification`](https://blog.apdu.fr/posts/2024/08/improved-scardgetstatuschange-for-pnpnotification-special-reader/) feature to detect new connected readers without any CPU load.
|
||||
2. Monitoring watch mode could use a blocking call to `GetStatusChange`, but this did not work on Linux (Ubuntu), although worked on MacOS.
|
||||
So instead there is a loop that checks the state of the reader each 500ms.
|
||||
3. JSON-RPC was chosen for 2 reasons:
|
||||
- to expose API to HTTP for testing/debugging
|
||||
- to simplify the adding new methods to the API
|
||||
gRPC was also considered, but this would require more work on `status-desktop`.
|
3
api/Signals.http
Normal file
3
api/Signals.http
Normal file
@ -0,0 +1,3 @@
|
||||
# @name Signals
|
||||
WEBSOCKET ws://{{address}}/signals
|
||||
Content-Type: application/json
|
12
api/SimulateError.http
Normal file
12
api/SimulateError.http
Normal file
@ -0,0 +1,12 @@
|
||||
# @name SimulateError
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.SimulateError",
|
||||
"params": [
|
||||
{
|
||||
"error": "simulated-no-pcsc"
|
||||
}
|
||||
]
|
||||
}
|
12
api/Start.http
Normal file
12
api/Start.http
Normal file
@ -0,0 +1,12 @@
|
||||
# @name Start
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.Start",
|
||||
"params": [
|
||||
{
|
||||
"storageFilePath": "{{storageFilePath}}"
|
||||
}
|
||||
]
|
||||
}
|
8
api/Stop.http
Normal file
8
api/Stop.http
Normal file
@ -0,0 +1,8 @@
|
||||
# @name Stop
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.Stop",
|
||||
"params": []
|
||||
}
|
15
api/StoreMetadata.http
Normal file
15
api/StoreMetadata.http
Normal file
@ -0,0 +1,15 @@
|
||||
# @name StoreMetadata
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.StoreMetadata",
|
||||
"params": [
|
||||
{
|
||||
"name": "{{$timestamp}}",
|
||||
"paths": [
|
||||
"m/44'/60'/0'/0/1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
13
api/Unblock.http
Normal file
13
api/Unblock.http
Normal file
@ -0,0 +1,13 @@
|
||||
# @name Unblock
|
||||
POST {{address}}/rpc
|
||||
|
||||
{
|
||||
"id": "{{$random.uuid}}",
|
||||
"method": "keycard.Unblock",
|
||||
"params": [
|
||||
{
|
||||
"puk": "654321654321",
|
||||
"newPIN": "654321"
|
||||
}
|
||||
]
|
||||
}
|
60
cmd/status-keycard-server/main.go
Normal file
60
cmd/status-keycard-server/main.go
Normal file
@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/status-im/status-keycard-go/cmd/status-keycard-server/server"
|
||||
)
|
||||
|
||||
var (
|
||||
address = flag.String("address", "127.0.0.1:0", "host:port to listen")
|
||||
rootLogger = zap.NewNop()
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
config := zap.NewDevelopmentConfig()
|
||||
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
rootLogger, err = config.Build()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to initialize log: %v\n", err)
|
||||
}
|
||||
zap.ReplaceGlobals(rootLogger)
|
||||
}
|
||||
|
||||
func main() {
|
||||
logger := rootLogger.Named("main")
|
||||
|
||||
flag.Parse()
|
||||
go handleInterrupts()
|
||||
|
||||
srv := server.NewServer(rootLogger)
|
||||
srv.Setup()
|
||||
|
||||
err := srv.Listen(*address)
|
||||
if err != nil {
|
||||
logger.Error("failed to start server", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("keycard-server started", zap.String("address", srv.Address()))
|
||||
srv.Serve()
|
||||
}
|
||||
|
||||
// handleInterrupts catches interrupt signal (SIGTERM/SIGINT) and
|
||||
// gracefully logouts and stops the node.
|
||||
func handleInterrupts() {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer signal.Stop(ch)
|
||||
|
||||
<-ch
|
||||
os.Exit(0)
|
||||
}
|
161
cmd/status-keycard-server/server/server.go
Normal file
161
cmd/status-keycard-server/server/server.go
Normal file
@ -0,0 +1,161 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/status-im/status-keycard-go/pkg/session"
|
||||
"github.com/status-im/status-keycard-go/signal"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
logger *zap.Logger
|
||||
server *http.Server
|
||||
listener net.Listener
|
||||
mux *http.ServeMux
|
||||
connectionsLock sync.Mutex
|
||||
connections map[*websocket.Conn]struct{}
|
||||
address string
|
||||
}
|
||||
|
||||
func NewServer(logger *zap.Logger) *Server {
|
||||
return &Server{
|
||||
logger: logger.Named("server"),
|
||||
connections: make(map[*websocket.Conn]struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Address() string {
|
||||
return s.address
|
||||
}
|
||||
|
||||
func (s *Server) Port() (int, error) {
|
||||
_, portString, err := net.SplitHostPort(s.address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(portString)
|
||||
}
|
||||
|
||||
func (s *Server) Setup() {
|
||||
signal.SetKeycardSignalHandler(s.signalHandler)
|
||||
}
|
||||
|
||||
func (s *Server) signalHandler(data []byte) {
|
||||
s.connectionsLock.Lock()
|
||||
defer s.connectionsLock.Unlock()
|
||||
|
||||
deleteConnection := func(connection *websocket.Conn) {
|
||||
delete(s.connections, connection)
|
||||
err := connection.Close()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to close connection", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
for connection := range s.connections {
|
||||
err := connection.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
if err != nil {
|
||||
s.logger.Error("failed to set write deadline", zap.Error(err))
|
||||
deleteConnection(connection)
|
||||
continue
|
||||
}
|
||||
|
||||
err = connection.WriteMessage(websocket.TextMessage, data)
|
||||
if err != nil {
|
||||
deleteConnection(connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Listen(address string) error {
|
||||
if s.server != nil {
|
||||
return errors.New("server already started")
|
||||
}
|
||||
|
||||
_, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid address")
|
||||
}
|
||||
|
||||
s.server = &http.Server{
|
||||
Addr: address,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
rpcServer, err := session.CreateRPCServer()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to create PRC server", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
s.mux = http.NewServeMux()
|
||||
s.mux.HandleFunc("/signals", s.signals)
|
||||
s.mux.Handle("/rpc", rpcServer)
|
||||
s.server.Handler = s.mux
|
||||
|
||||
s.listener, err = net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.address = s.listener.Addr().String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Serve() {
|
||||
err := s.server.Serve(s.listener)
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Error("signals server closed with error", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop(ctx context.Context) {
|
||||
for connection := range s.connections {
|
||||
err := connection.Close()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to close connection", zap.Error(err))
|
||||
}
|
||||
delete(s.connections, connection)
|
||||
}
|
||||
|
||||
err := s.server.Shutdown(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to shutdown signals server", zap.Error(err))
|
||||
}
|
||||
|
||||
s.server = nil
|
||||
s.address = ""
|
||||
}
|
||||
|
||||
func (s *Server) signals(w http.ResponseWriter, r *http.Request) {
|
||||
s.connectionsLock.Lock()
|
||||
defer s.connectionsLock.Unlock()
|
||||
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // Accepting all requests
|
||||
},
|
||||
}
|
||||
|
||||
connection, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to upgrade connection", zap.Error(err))
|
||||
return
|
||||
}
|
||||
s.logger.Debug("new websocket connection")
|
||||
|
||||
s.connections[connection] = struct{}{}
|
||||
}
|
17
go.mod
17
go.mod
@ -5,14 +5,25 @@ go 1.22
|
||||
require (
|
||||
github.com/ebfe/scard v0.0.0-20241214075232-7af069cabc25
|
||||
github.com/ethereum/go-ethereum v1.10.26
|
||||
github.com/go-playground/validator/v10 v10.24.0
|
||||
github.com/gorilla/rpc v1.2.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/status-im/keycard-go v0.3.3
|
||||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/text v0.4.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/text v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
)
|
||||
|
42
go.sum
42
go.sum
@ -12,19 +12,45 @@ github.com/ebfe/scard v0.0.0-20241214075232-7af069cabc25 h1:vXmXuiy1tgifTqWAAaU+
|
||||
github.com/ebfe/scard v0.0.0-20241214075232-7af069cabc25/go.mod h1:BkYEeWL6FbT4Ek+TcOBnPzEKnL7kOq2g19tTQXkorHY=
|
||||
github.com/ethereum/go-ethereum v1.10.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s=
|
||||
github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
|
||||
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/status-im/keycard-go v0.3.3 h1:qk/JHSkT9sMka+lVXrTOIVSgHIY7lDm46wrUqTsNa4s=
|
||||
github.com/status-im/keycard-go v0.3.3/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
867
internal/keycard_context_v2.go
Normal file
867
internal/keycard_context_v2.go
Normal file
@ -0,0 +1,867 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ebfe/scard"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/status-im/keycard-go"
|
||||
"github.com/status-im/keycard-go/derivationpath"
|
||||
"github.com/status-im/keycard-go/io"
|
||||
"github.com/status-im/keycard-go/types"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/status-im/status-keycard-go/pkg/pairing"
|
||||
"github.com/status-im/status-keycard-go/signal"
|
||||
)
|
||||
|
||||
const (
|
||||
infiniteTimeout = -1
|
||||
zeroTimeout = 0
|
||||
monitoringTick = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
errKeycardNotConnected = errors.New("keycard not connected")
|
||||
errKeycardNotInitialized = errors.New("keycard not initialized")
|
||||
errKeycardNotReady = errors.New("keycard not ready")
|
||||
errKeycardNotAuthorized = errors.New("keycard not authorized")
|
||||
errKeycardNotBlocked = errors.New("keycard not blocked")
|
||||
)
|
||||
|
||||
type KeycardContextV2 struct {
|
||||
KeycardContext
|
||||
|
||||
shutdown func()
|
||||
forceScanC chan struct{}
|
||||
logger *zap.Logger
|
||||
pairings *pairing.Store
|
||||
status *Status
|
||||
|
||||
// simulation options
|
||||
simulatedError error
|
||||
}
|
||||
|
||||
func NewKeycardContextV2(pairingsStoreFilePath string) (*KeycardContextV2, error) {
|
||||
pairingsStore, err := pairing.NewStore(pairingsStoreFilePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create pairing store")
|
||||
}
|
||||
|
||||
kc := &KeycardContextV2{
|
||||
KeycardContext: KeycardContext{
|
||||
command: make(chan commandType),
|
||||
},
|
||||
logger: zap.L().Named("context"),
|
||||
pairings: pairingsStore,
|
||||
status: NewStatus(),
|
||||
}
|
||||
|
||||
return kc, nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) Start() error {
|
||||
err := kc.establishContext()
|
||||
err = kc.simulateError(err, simulatedNoPCSC)
|
||||
if err != nil {
|
||||
kc.logger.Error("failed to establish context", zap.Error(err))
|
||||
kc.status.State = NoPCSC
|
||||
kc.publishStatus()
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
kc.shutdown = cancel
|
||||
kc.forceScanC = nil
|
||||
|
||||
go kc.cardCommunicationRoutine(ctx)
|
||||
kc.startDetectionLoop(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) establishContext() error {
|
||||
cardCtx, err := scard.EstablishContext()
|
||||
if err != nil {
|
||||
return errors.New(ErrorPCSC)
|
||||
}
|
||||
|
||||
kc.cardCtx = cardCtx
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) cardCommunicationRoutine(ctx context.Context) {
|
||||
// Communication with the keycard must be done in a fixed thread
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
kc.logger.Debug("card communication routine started")
|
||||
|
||||
defer func() {
|
||||
kc.logger.Debug("card communication routine stopped")
|
||||
err := kc.cardCtx.Release()
|
||||
if err != nil {
|
||||
kc.logger.Error("failed to release context", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case cmd := <-kc.command:
|
||||
switch cmd {
|
||||
case Transmit:
|
||||
kc.rpdu, kc.runErr = kc.card.Transmit(kc.apdu)
|
||||
kc.command <- Ack
|
||||
case Close:
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) startDetectionLoop(ctx context.Context) {
|
||||
if kc.cardCtx == nil {
|
||||
panic("card context is nil")
|
||||
}
|
||||
|
||||
logger := kc.logger.Named("detect")
|
||||
|
||||
go func() {
|
||||
logger.Debug("detect started")
|
||||
defer logger.Debug("detect stopped")
|
||||
// This goroutine will be stopped by cardCtx.Cancel()
|
||||
for {
|
||||
ok := kc.detectionRoutine(ctx, logger)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// detectionRoutine is the main routine that monitors the card readers and card changes.
|
||||
// It will be stopped by cardCtx.Cancel() or when the context is done.
|
||||
// Returns false if the monitoring should be stopped by the runner.
|
||||
func (kc *KeycardContextV2) detectionRoutine(ctx context.Context, logger *zap.Logger) bool {
|
||||
// Get current readers list and state
|
||||
readers, err := kc.getCurrentReadersState()
|
||||
if err != nil {
|
||||
logger.Error("failed to get readers state", zap.Error(err))
|
||||
kc.status.Reset(InternalError)
|
||||
kc.publishStatus()
|
||||
return false
|
||||
}
|
||||
|
||||
card, err := kc.connectCard(ctx, readers)
|
||||
if card != nil {
|
||||
err = kc.connectKeycard()
|
||||
if err != nil {
|
||||
logger.Error("failed to connect keycard", zap.Error(err))
|
||||
}
|
||||
go kc.watchActiveReader(ctx, card.readerState)
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error("failed to connect card", zap.Error(err))
|
||||
}
|
||||
|
||||
// Wait for readers changes, including new readers
|
||||
// https://blog.apdu.fr/posts/2024/08/improved-scardgetstatuschange-for-pnpnotification-special-reader/
|
||||
// NOTE: The article states that MacOS is not supported, but works for me on MacOS 15.1.1 (24B91).
|
||||
const pnpNotificationReader = `\\?PnP?\Notification`
|
||||
pnpReader := scard.ReaderState{
|
||||
Reader: pnpNotificationReader,
|
||||
CurrentState: scard.StateUnaware,
|
||||
}
|
||||
rs := append(readers, pnpReader)
|
||||
err = kc.cardCtx.GetStatusChange(rs, infiniteTimeout)
|
||||
if err == scard.ErrCancelled {
|
||||
// Shutdown requested
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error("failed to get status change", zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type connectedCard struct {
|
||||
readerState scard.ReaderState
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) connectCard(ctx context.Context, readers ReadersStates) (*connectedCard, error) {
|
||||
defer kc.publishStatus()
|
||||
|
||||
if readers.Empty() {
|
||||
kc.status.Reset(WaitingForReader)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
kc.forceScanC = make(chan struct{})
|
||||
kc.resetCardConnection()
|
||||
|
||||
readerWithCardIndex, ok := readers.ReaderWithCardIndex()
|
||||
if !ok {
|
||||
kc.logger.Debug("no card found on any readers")
|
||||
kc.status.Reset(WaitingForCard)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
kc.logger.Debug("card found", zap.Int("index", readerWithCardIndex))
|
||||
activeReader := readers[readerWithCardIndex]
|
||||
|
||||
var err error
|
||||
kc.card, err = kc.cardCtx.Connect(activeReader.Reader, scard.ShareExclusive, scard.ProtocolAny)
|
||||
err = kc.simulateError(err, simulatedCardConnectError)
|
||||
if err != nil {
|
||||
kc.status.State = ConnectionError
|
||||
return nil, errors.Wrap(err, "failed to connect to card")
|
||||
}
|
||||
|
||||
kc.c = io.NewNormalChannel(kc)
|
||||
kc.cmdSet = keycard.NewCommandSet(kc.c)
|
||||
|
||||
// Card connected, now check if this is a keycard
|
||||
appInfo, err := kc.selectApplet()
|
||||
err = kc.simulateError(err, simulatedSelectAppletError)
|
||||
if err != nil {
|
||||
kc.status.State = ConnectionError
|
||||
return nil, errors.Wrap(err, "failed to select applet")
|
||||
}
|
||||
|
||||
// Save AppInfo
|
||||
kc.status.AppInfo = appInfo
|
||||
|
||||
if !appInfo.Installed {
|
||||
kc.status.State = NotKeycard
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
kc.status.State = ConnectingCard
|
||||
|
||||
return &connectedCard{
|
||||
readerState: activeReader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) watchActiveReader(ctx context.Context, activeReader scard.ReaderState) {
|
||||
logger := kc.logger.Named("watch")
|
||||
logger.Debug("watch started", zap.String("reader", activeReader.Reader))
|
||||
defer logger.Debug("watch stopped")
|
||||
|
||||
readersStates := ReadersStates{
|
||||
activeReader,
|
||||
}
|
||||
|
||||
for {
|
||||
err := kc.cardCtx.GetStatusChange(readersStates, zeroTimeout)
|
||||
|
||||
if err != nil && err != scard.ErrTimeout {
|
||||
kc.logger.Error("failed to get status change", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
state := readersStates[0].EventState
|
||||
if state&scard.StateUnknown != 0 || state&scard.StateEmpty != 0 {
|
||||
break
|
||||
}
|
||||
|
||||
readersStates.Update()
|
||||
|
||||
// NOTE: Would be better to use `GetStatusChange` with infinite timeout.
|
||||
// This worked perfectly on MacOS, but not on Linux. So we poll the reader state instead.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-time.After(monitoringTick): // Pause for a while to avoid a busy loop
|
||||
case _, ok := <-kc.forceScanC:
|
||||
if ok {
|
||||
kc.startDetectionLoop(ctx)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
kc.startDetectionLoop(ctx)
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) getCurrentReadersState() (ReadersStates, error) {
|
||||
readers, err := kc.cardCtx.ListReaders()
|
||||
err = kc.simulateError(err, simulatedListReadersError)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs := make(ReadersStates, len(readers))
|
||||
for i, name := range readers {
|
||||
rs[i].Reader = name
|
||||
rs[i].CurrentState = scard.StateUnaware
|
||||
}
|
||||
|
||||
if rs.Empty() {
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
err = kc.cardCtx.GetStatusChange(rs, zeroTimeout)
|
||||
err = kc.simulateError(err, simulatedGetStatusChangeError)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs.Update()
|
||||
|
||||
// When removing a reader, a call to `ListReaders` too quick might still return the removed reader.
|
||||
// So we need to filter out the unknown readers.
|
||||
knownReaders := make(ReadersStates, 0, len(rs))
|
||||
for i := range rs {
|
||||
if rs[i].EventState&scard.StateUnknown == 0 {
|
||||
knownReaders.Append(rs[i])
|
||||
}
|
||||
}
|
||||
|
||||
return knownReaders, nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) connectKeycard() error {
|
||||
var err error
|
||||
appInfo := kc.status.AppInfo
|
||||
|
||||
defer kc.publishStatus()
|
||||
|
||||
if !appInfo.Initialized {
|
||||
kc.status.State = EmptyKeycard
|
||||
return nil
|
||||
}
|
||||
|
||||
pair := kc.pairings.Get(appInfo.InstanceUID.String())
|
||||
|
||||
if pair == nil {
|
||||
kc.logger.Debug("pairing not found, pairing now")
|
||||
|
||||
var pairingInfo *types.PairingInfo
|
||||
pairingPassword := DefPairing
|
||||
pairingInfo, err = kc.Pair(pairingPassword)
|
||||
if errors.Is(err, keycard.ErrNoAvailablePairingSlots) {
|
||||
kc.status.State = NoAvailablePairingSlots
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
kc.status.State = PairingError
|
||||
return errors.Wrap(err, "failed to pair keycard")
|
||||
}
|
||||
|
||||
pair = pairing.ToPairInfo(pairingInfo)
|
||||
err = kc.pairings.Store(appInfo.InstanceUID.String(), pair)
|
||||
if err != nil {
|
||||
kc.status.State = InternalError
|
||||
return errors.Wrap(err, "failed to store pairing")
|
||||
}
|
||||
|
||||
// After successful pairing, we should `SelectApplet` again to update the ApplicationInfo
|
||||
appInfo, err = kc.selectApplet()
|
||||
if err != nil {
|
||||
kc.status.State = ConnectionError
|
||||
return errors.Wrap(err, "failed to select applet")
|
||||
}
|
||||
kc.status.AppInfo = appInfo
|
||||
}
|
||||
|
||||
err = kc.OpenSecureChannel(pair.Index, pair.Key)
|
||||
err = kc.simulateError(err, simulatedOpenSecureChannelError)
|
||||
if err != nil {
|
||||
kc.status.State = ConnectionError
|
||||
return errors.Wrap(err, "failed to open secure channel")
|
||||
}
|
||||
|
||||
err = kc.updateApplicationStatus() // Changes status to Ready
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get application status")
|
||||
}
|
||||
|
||||
err = kc.updateMetadata()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get metadata")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) resetCardConnection() {
|
||||
if kc.card != nil {
|
||||
err := kc.card.Disconnect(scard.LeaveCard)
|
||||
|
||||
if err != nil {
|
||||
kc.logger.Error("failed to disconnect card", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
kc.card = nil
|
||||
kc.c = nil
|
||||
kc.cmdSet = nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) forceScan() {
|
||||
kc.forceScanC <- struct{}{}
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) publishStatus() {
|
||||
kc.logger.Info("status changed", zap.Any("status", kc.status))
|
||||
signal.Send("status-changed", kc.status)
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) Stop() {
|
||||
if kc.forceScanC != nil {
|
||||
close(kc.forceScanC)
|
||||
}
|
||||
|
||||
if kc.cardCtx != nil {
|
||||
err := kc.cardCtx.Cancel()
|
||||
if err != nil {
|
||||
kc.logger.Error("failed to cancel context", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
kc.KeycardContext.Stop()
|
||||
|
||||
if kc.shutdown != nil {
|
||||
kc.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) keycardConnected() bool {
|
||||
return kc.cmdSet != nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) keycardInitialized() error {
|
||||
if !kc.keycardConnected() {
|
||||
return errKeycardNotConnected
|
||||
}
|
||||
if kc.status.State == EmptyKeycard {
|
||||
return errKeycardNotInitialized
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) keycardReady() error {
|
||||
if err := kc.keycardInitialized(); err != nil {
|
||||
return err
|
||||
}
|
||||
if kc.status.State != Ready && kc.status.State != Authorized {
|
||||
return errKeycardNotReady
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) keycardAuthorized() error {
|
||||
if err := kc.keycardInitialized(); err != nil {
|
||||
return err
|
||||
}
|
||||
if kc.status.State != Authorized {
|
||||
return errKeycardNotAuthorized
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) checkSCardError(err error, context string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if IsSCardError(err) {
|
||||
kc.logger.Error("command failed, resetting connection",
|
||||
zap.String("context", context),
|
||||
zap.Error(err))
|
||||
kc.resetCardConnection()
|
||||
kc.forceScan()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) selectApplet() (*ApplicationInfoV2, error) {
|
||||
info, err := kc.SelectApplet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ToAppInfoV2(info), err
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) updateApplicationStatus() error {
|
||||
appStatus, err := kc.cmdSet.GetStatusApplication()
|
||||
kc.status.AppStatus = ToAppStatus(appStatus)
|
||||
|
||||
if err != nil {
|
||||
kc.status.State = ConnectionError
|
||||
return err
|
||||
}
|
||||
|
||||
kc.status.State = Ready
|
||||
|
||||
if appStatus != nil {
|
||||
if appStatus.PinRetryCount == 0 {
|
||||
kc.status.State = BlockedPIN
|
||||
}
|
||||
if appStatus.PUKRetryCount == 0 {
|
||||
kc.status.State = BlockedPUK
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) updateMetadata() error {
|
||||
metadata, err := kc.GetMetadata()
|
||||
if err != nil {
|
||||
kc.status.State = ConnectionError
|
||||
return err
|
||||
}
|
||||
|
||||
kc.status.Metadata = metadata
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) GetStatus() Status {
|
||||
return *kc.status
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) Initialize(pin, puk, pairingPassword string) error {
|
||||
if !kc.keycardConnected() {
|
||||
return errKeycardNotConnected
|
||||
}
|
||||
|
||||
secrets := keycard.NewSecrets(pin, puk, pairingPassword)
|
||||
err := kc.cmdSet.Init(secrets)
|
||||
if err != nil {
|
||||
return kc.checkSCardError(err, "Init")
|
||||
}
|
||||
|
||||
// Reset card connection to pair the card and open secure channel
|
||||
kc.resetCardConnection()
|
||||
kc.forceScan()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) onAuthorizeInteractions(authorized bool) {
|
||||
err := kc.updateApplicationStatus()
|
||||
if err != nil {
|
||||
kc.logger.Error("failed to update app status", zap.Error(err))
|
||||
}
|
||||
if kc.status.State == Ready && authorized {
|
||||
kc.status.State = Authorized
|
||||
}
|
||||
kc.publishStatus()
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) VerifyPIN(pin string) (err error) {
|
||||
if err = kc.keycardReady(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
authorized := err == nil
|
||||
kc.onAuthorizeInteractions(authorized)
|
||||
}()
|
||||
|
||||
err = kc.cmdSet.VerifyPIN(pin)
|
||||
return kc.checkSCardError(err, "VerifyPIN")
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) ChangePIN(pin string) error {
|
||||
if err := kc.keycardAuthorized(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
kc.onAuthorizeInteractions(false)
|
||||
}()
|
||||
|
||||
err := kc.cmdSet.ChangePIN(pin)
|
||||
return kc.checkSCardError(err, "ChangePIN")
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) UnblockPIN(puk string, newPIN string) (err error) {
|
||||
if err = kc.keycardInitialized(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if kc.status.State != BlockedPIN {
|
||||
return errKeycardNotBlocked
|
||||
}
|
||||
|
||||
defer func() {
|
||||
authorized := err == nil
|
||||
kc.onAuthorizeInteractions(authorized)
|
||||
}()
|
||||
|
||||
err = kc.cmdSet.UnblockPIN(puk, newPIN)
|
||||
return kc.checkSCardError(err, "UnblockPIN")
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) ChangePUK(puk string) error {
|
||||
if err := kc.keycardAuthorized(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
kc.onAuthorizeInteractions(false)
|
||||
}()
|
||||
|
||||
err := kc.cmdSet.ChangePUK(puk)
|
||||
return kc.checkSCardError(err, "ChangePUK")
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) GenerateMnemonic(mnemonicLength int) ([]int, error) {
|
||||
if err := kc.keycardReady(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexes, err := kc.cmdSet.GenerateMnemonic(mnemonicLength / 3)
|
||||
return indexes, kc.checkSCardError(err, "GenerateMnemonic")
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) LoadMnemonic(mnemonic string, password string) ([]byte, error) {
|
||||
if err := kc.keycardAuthorized(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var keyUID []byte
|
||||
var err error
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
kc.status.AppInfo.KeyUID = keyUID
|
||||
kc.publishStatus()
|
||||
}()
|
||||
|
||||
seed := kc.mnemonicToBinarySeed(mnemonic, password)
|
||||
keyUID, err = kc.loadSeed(seed)
|
||||
return keyUID, kc.checkSCardError(err, "LoadMnemonic")
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) FactoryReset() error {
|
||||
if !kc.keycardConnected() {
|
||||
return errKeycardNotConnected
|
||||
}
|
||||
|
||||
kc.status.Reset(FactoryResetting)
|
||||
kc.publishStatus()
|
||||
|
||||
kc.logger.Debug("factory reset")
|
||||
err := kc.KeycardContext.FactoryReset(true)
|
||||
|
||||
// Reset card connection to read the card data
|
||||
kc.resetCardConnection()
|
||||
kc.forceScan()
|
||||
return err
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) GetMetadata() (*Metadata, error) {
|
||||
if err := kc.keycardReady(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := kc.cmdSet.GetData(keycard.P1StoreDataPublic)
|
||||
if err != nil {
|
||||
return nil, kc.checkSCardError(err, "GetMetadata")
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metadata, err := types.ParseMetadata(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse metadata")
|
||||
}
|
||||
|
||||
return ToMetadata(metadata), nil
|
||||
}
|
||||
|
||||
func (kc KeycardContextV2) parsePaths(paths []string) ([]uint32, error) {
|
||||
parsedPaths := make([]uint32, len(paths))
|
||||
for i, path := range paths {
|
||||
if !strings.HasPrefix(path, WalletRoothPath) {
|
||||
return nil, fmt.Errorf("path '%s' does not start with wallet path '%s'", path, WalletRoothPath)
|
||||
}
|
||||
|
||||
_, components, err := derivationpath.Decode(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse path '%s'", path)
|
||||
}
|
||||
|
||||
// Store only the last part of the component, ignoring the common prefix
|
||||
parsedPaths[i] = components[len(components)-1]
|
||||
}
|
||||
return parsedPaths, nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) StoreMetadata(name string, paths []string) (err error) {
|
||||
if err = kc.keycardAuthorized(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pathComponents, err := kc.parsePaths(paths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metadata, err := types.NewMetadata(name, pathComponents)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create metadata")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = kc.updateMetadata()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
kc.publishStatus()
|
||||
}()
|
||||
|
||||
err = kc.cmdSet.StoreData(keycard.P1StoreDataPublic, metadata.Serialize())
|
||||
return kc.checkSCardError(err, "StoreMetadata")
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) exportedKeyToAddress(key *types.ExportedKey) (string, error) {
|
||||
if key.PubKey() == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
ecdsaPubKey, err := crypto.UnmarshalPubkey(key.PubKey())
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to unmarshal public key")
|
||||
}
|
||||
|
||||
return crypto.PubkeyToAddress(*ecdsaPubKey).Hex(), nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) exportKey(path string, exportOption uint8) (*KeyPair, error) {
|
||||
// 1. As for today, it's pointless to use the 'current path' feature. So we always derive.
|
||||
// 2. We keep this workaround for `makeCurrent` to mitigate a bug in an older version of the Keycard applet
|
||||
// that doesn't correctly export the public key for the master path unless it is also the current path.
|
||||
const derive = true
|
||||
makeCurrent := path == MasterPath
|
||||
|
||||
exportedKey, err := kc.cmdSet.ExportKeyExtended(derive, makeCurrent, exportOption, path)
|
||||
if err != nil {
|
||||
return nil, kc.checkSCardError(err, "ExportKeyExtended")
|
||||
}
|
||||
|
||||
address, err := kc.exportedKeyToAddress(exportedKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to convert key to address")
|
||||
}
|
||||
|
||||
return &KeyPair{
|
||||
Address: address,
|
||||
PublicKey: exportedKey.PubKey(),
|
||||
PrivateKey: exportedKey.PrivKey(),
|
||||
ChainCode: exportedKey.ChainCode(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) ExportLoginKeys() (*LoginKeys, error) {
|
||||
if err := kc.keycardAuthorized(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
keys := &LoginKeys{}
|
||||
|
||||
keys.EncryptionPrivateKey, err = kc.exportKey(EncryptionPath, keycard.P2ExportKeyPrivateAndPublic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys.WhisperPrivateKey, err = kc.exportKey(WhisperPath, keycard.P2ExportKeyPrivateAndPublic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keys, err
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) ExportRecoverKeys() (*RecoverKeys, error) {
|
||||
if err := kc.keycardAuthorized(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loginKeys, err := kc.ExportLoginKeys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys := &RecoverKeys{
|
||||
LoginKeys: *loginKeys,
|
||||
}
|
||||
|
||||
keys.EIP1581key, err = kc.exportKey(Eip1581Path, keycard.P2ExportKeyPublicOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootExportOptions := map[bool]uint8{
|
||||
true: keycard.P2ExportKeyExtendedPublic,
|
||||
false: keycard.P2ExportKeyPublicOnly,
|
||||
}
|
||||
keys.WalletRootKey, err = kc.exportKey(WalletRoothPath, rootExportOptions[kc.status.KeycardSupportsExtendedKeys()])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NOTE: In theory, if P2ExportKeyExtendedPublic is used, then we don't need to export the wallet key separately.
|
||||
keys.WalletKey, err = kc.exportKey(WalletPath, keycard.P2ExportKeyPublicOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys.MasterKey, err = kc.exportKey(MasterPath, keycard.P2ExportKeyPublicOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keys, err
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) SimulateError(err error) error {
|
||||
// Ensure the error is one of the known errors to simulate
|
||||
if err != nil {
|
||||
if simulateErr := GetSimulatedError(err.Error()); simulateErr == nil {
|
||||
return errors.New("unknown error to simulate")
|
||||
}
|
||||
}
|
||||
|
||||
kc.simulatedError = err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kc *KeycardContextV2) simulateError(currentError, errorToSimulate error) error {
|
||||
if !errors.Is(kc.simulatedError, errorToSimulate) {
|
||||
return currentError
|
||||
}
|
||||
switch errorToSimulate {
|
||||
case simulatedCardConnectError:
|
||||
fallthrough
|
||||
case simulatedSelectAppletError:
|
||||
kc.resetCardConnection() // Make it look like we never connected
|
||||
}
|
||||
return errorToSimulate
|
||||
}
|
135
internal/keycard_context_v2_state.go
Normal file
135
internal/keycard_context_v2_state.go
Normal file
@ -0,0 +1,135 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type State string
|
||||
|
||||
const (
|
||||
// UnknownReaderState is the default state when the monitoring was not started.
|
||||
UnknownReaderState State = "unknown"
|
||||
|
||||
// NoPCSC - PCSC library was not found. Can only happen during Start.
|
||||
NoPCSC State = "no-pcsc"
|
||||
|
||||
// InternalError - an internal error occurred.
|
||||
// Should never happen, check logs for more details. Depending on circumstances, can stop the monitoring when occurred.
|
||||
InternalError State = "internal-error"
|
||||
|
||||
// WaitingForReader - no reader was found.
|
||||
WaitingForReader State = "waiting-for-reader"
|
||||
|
||||
// WaitingForCard - no card was found inserted into any of connected readers.
|
||||
WaitingForCard State = "waiting-for-card"
|
||||
|
||||
// ConnectingCard - a card was found inserted into a reader and the connection is being established.
|
||||
// This state is usually very short, as the connection is established quickly.
|
||||
ConnectingCard State = "connecting-card"
|
||||
|
||||
// ConnectionError - an error occurred while connecting or communicating with the card.
|
||||
// In all cases, the monitoring will continue to stay in the watch mode and expect the user to reinsert the card.
|
||||
ConnectionError State = "connection-error"
|
||||
|
||||
// NotKeycard - the card inserted is not a keycard (does not have Keycard applet installed)
|
||||
NotKeycard State = "not-keycard"
|
||||
|
||||
// EmptyKeycard - the keycard is empty, i.e. has not been initialized (PIN/PUK are not set).
|
||||
// Use Initialize command to initialize the keycard.
|
||||
EmptyKeycard State = "empty-keycard"
|
||||
|
||||
// NoAvailablePairingSlots - there are no available pairing slots on the keycard.
|
||||
// Use Unpair command to unpair an existing slot (this command must be executed from the paired devices),
|
||||
// or use FactoryReset command to reset the keycard to factory settings.
|
||||
NoAvailablePairingSlots State = "no-available-pairing-slots"
|
||||
|
||||
// PairingError - an error occurred during the pairing process.
|
||||
// This can be due to a wrong pairing password.
|
||||
PairingError State = "pairing-error"
|
||||
|
||||
// BlockedPIN - the PIN is blocked (remaining attempts == 0).
|
||||
// Use UnblockPIN command to unblock the PIN.
|
||||
BlockedPIN State = "blocked-pin"
|
||||
|
||||
// BlockedPUK - the PUK is blocked (remaining attempts == 0).
|
||||
// The keycard is completely blocked. Use FactoryReset command to reset the keycard to factory settings
|
||||
// and recover the keycard with recovery phrase.
|
||||
BlockedPUK State = "blocked-puk"
|
||||
|
||||
// Ready - the keycard is ready for use.
|
||||
// The keycard is initialized, paired and secure channel is established.
|
||||
// The PIN has not been verified, so only unauthenticated commands can be executed.
|
||||
Ready State = "ready"
|
||||
|
||||
// Authorized - the keycard is authorized (PIN verified).
|
||||
// The keycard is in Ready state and the PIN has been verified, allowing authenticated commands to be executed.
|
||||
Authorized State = "authorized"
|
||||
|
||||
// FactoryResetting - the keycard is undergoing a factory reset.
|
||||
// The keycard is being reset to factory settings. This process can take a few seconds.
|
||||
FactoryResetting State = "factory-resetting"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
State State `json:"state"`
|
||||
AppInfo *ApplicationInfoV2 `json:"keycardInfo"`
|
||||
AppStatus *ApplicationStatus `json:"keycardStatus"`
|
||||
Metadata *Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
func NewStatus() *Status {
|
||||
status := &Status{}
|
||||
status.Reset(UnknownReaderState)
|
||||
return status
|
||||
}
|
||||
|
||||
func (s *Status) Reset(newState State) {
|
||||
s.State = newState
|
||||
s.AppInfo = nil
|
||||
s.AppStatus = nil
|
||||
s.Metadata = nil
|
||||
}
|
||||
|
||||
func (s *Status) KeycardSupportsExtendedKeys() bool {
|
||||
return s.AppInfo != nil && s.AppInfo.versionRaw >= 0x0310
|
||||
}
|
||||
|
||||
var (
|
||||
// simulatedNoPCSC simulates failure in PCSC check.
|
||||
// Call Start to trigger.
|
||||
simulatedNoPCSC = errors.New("simulated-no-pcsc")
|
||||
|
||||
// simulatedListReadersError simulates a failure when trying to list readers.
|
||||
// Connect a reader to trigger. Results in `InternalError` state.
|
||||
simulatedListReadersError = errors.New("simulated-list-readers-error")
|
||||
|
||||
// simulatedGetStatusChangeError simulates a failure when trying to get status change.
|
||||
// Connect a reader to trigger. Results in `InternalError` state.
|
||||
simulatedGetStatusChangeError = errors.New("simulated-get-status-change-error")
|
||||
|
||||
// simulatedNotAKeycard simulates a card connection issue.
|
||||
// Insert a card to trigger. Results in `ConnectionError` state.
|
||||
simulatedCardConnectError = errors.New("simulated-card-connect-error")
|
||||
|
||||
// simulatedSelectAppletError simulates a failure when trying to select the applet.
|
||||
// Insert the keycard to trigger. Results in `ConnectionError` state.
|
||||
simulatedSelectAppletError = errors.New("simulated-select-applet-error")
|
||||
|
||||
// simulatedOpenSecureChannelError happens when trying to open a secure channel with the keycard.
|
||||
// The keycard must be initialized and paired to open a secure channel, so if not initialized,
|
||||
// or in case of a pairing issue, this error will not occur.
|
||||
// Insert an initialized card to trigger. Results in `ConnectionError` state.
|
||||
simulatedOpenSecureChannelError = errors.New("simulated-open-secure-channel-error")
|
||||
)
|
||||
|
||||
func GetSimulatedError(message string) error {
|
||||
errs := map[string]error{
|
||||
simulatedNoPCSC.Error(): simulatedNoPCSC,
|
||||
simulatedListReadersError.Error(): simulatedListReadersError,
|
||||
simulatedGetStatusChangeError.Error(): simulatedGetStatusChangeError,
|
||||
simulatedCardConnectError.Error(): simulatedCardConnectError,
|
||||
simulatedSelectAppletError.Error(): simulatedSelectAppletError,
|
||||
simulatedOpenSecureChannelError.Error(): simulatedOpenSecureChannelError,
|
||||
}
|
||||
return errs[message]
|
||||
}
|
67
internal/readers_states.go
Normal file
67
internal/readers_states.go
Normal file
@ -0,0 +1,67 @@
|
||||
package internal
|
||||
|
||||
import "github.com/ebfe/scard"
|
||||
|
||||
type ReadersStates []scard.ReaderState
|
||||
|
||||
func (rs ReadersStates) Contains(reader string) bool {
|
||||
for _, state := range rs {
|
||||
if state.Reader == reader {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rs ReadersStates) Update() {
|
||||
for i := range rs {
|
||||
rs[i].CurrentState = rs[i].EventState
|
||||
}
|
||||
}
|
||||
|
||||
func (rs ReadersStates) ReaderWithCardIndex() (int, bool) {
|
||||
for i := range rs {
|
||||
if rs[i].EventState&scard.StatePresent == 0 || rs[i].EventState&scard.StateExclusive != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// NOTE: For now we only support one card at a time
|
||||
return i, true
|
||||
}
|
||||
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func (rs *ReadersStates) Append(reader scard.ReaderState) {
|
||||
*rs = append(*rs, reader)
|
||||
}
|
||||
|
||||
func (rs ReadersStates) ReaderHasCard(reader string) bool {
|
||||
for _, state := range rs {
|
||||
if state.Reader == reader && state.EventState&scard.StatePresent != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rs ReadersStates) Empty() bool {
|
||||
return len(rs) == 0
|
||||
}
|
||||
|
||||
func (rs ReadersStates) HasChanges() bool {
|
||||
for _, state := range rs {
|
||||
if state.EventState&scard.StateChanged != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rs ReadersStates) Names() []string {
|
||||
readers := make([]string, 0, len(rs))
|
||||
for _, state := range rs {
|
||||
readers = append(readers, state.Reader)
|
||||
}
|
||||
return readers
|
||||
}
|
@ -20,6 +20,26 @@ type ApplicationInfo struct {
|
||||
KeyUID utils.HexString `json:"keyUID"`
|
||||
}
|
||||
|
||||
// ApplicationInfoV2 is the same as ApplicationInfo but with a string version field.
|
||||
type ApplicationInfoV2 struct {
|
||||
Installed bool `json:"installed"`
|
||||
Initialized bool `json:"initialized"`
|
||||
InstanceUID utils.HexString `json:"instanceUID"`
|
||||
versionRaw int `json:"-"`
|
||||
Version string `json:"version"`
|
||||
AvailableSlots int `json:"availableSlots"`
|
||||
// KeyUID is the sha256 of the master public key on the card.
|
||||
// It's empty if the card doesn't contain any key.
|
||||
KeyUID utils.HexString `json:"keyUID"`
|
||||
}
|
||||
|
||||
type ApplicationStatus struct {
|
||||
RemainingAttemptsPIN int `json:"remainingAttemptsPIN"`
|
||||
RemainingAttemptsPUK int `json:"remainingAttemptsPUK"`
|
||||
KeyInitialized bool `json:"keyInitialized"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type KeyPair struct {
|
||||
Address string `json:"address"`
|
||||
PublicKey utils.HexString `json:"publicKey"`
|
||||
@ -37,3 +57,16 @@ type Metadata struct {
|
||||
Name string `json:"name"`
|
||||
Wallets []Wallet `json:"wallets"`
|
||||
}
|
||||
|
||||
type LoginKeys struct {
|
||||
EncryptionPrivateKey *KeyPair `json:"encryptionPrivateKey"`
|
||||
WhisperPrivateKey *KeyPair `json:"whisperPrivateKey"`
|
||||
}
|
||||
|
||||
type RecoverKeys struct {
|
||||
LoginKeys
|
||||
EIP1581key *KeyPair `json:"eip1581"`
|
||||
WalletRootKey *KeyPair `json:"walletRootKey"`
|
||||
WalletKey *KeyPair `json:"walletKey"`
|
||||
MasterKey *KeyPair `json:"masterKey"`
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ package internal
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/ebfe/scard"
|
||||
keycard "github.com/status-im/keycard-go"
|
||||
"github.com/status-im/keycard-go"
|
||||
"github.com/status-im/keycard-go/derivationpath"
|
||||
ktypes "github.com/status-im/keycard-go/types"
|
||||
)
|
||||
@ -57,6 +58,46 @@ func ToAppInfo(r *ktypes.ApplicationInfo) ApplicationInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func ParseVersion(input []byte) string {
|
||||
if len(input) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(input) != 2 {
|
||||
return "unexpected version format"
|
||||
}
|
||||
|
||||
major := input[0]
|
||||
minor := input[1]
|
||||
return fmt.Sprintf("%d.%d", major, minor)
|
||||
}
|
||||
|
||||
func ToAppInfoV2(r *ktypes.ApplicationInfo) *ApplicationInfoV2 {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &ApplicationInfoV2{
|
||||
Installed: r.Installed,
|
||||
Initialized: r.Initialized,
|
||||
InstanceUID: r.InstanceUID,
|
||||
versionRaw: BytesToInt(r.Version),
|
||||
Version: ParseVersion(r.Version),
|
||||
AvailableSlots: BytesToInt(r.AvailableSlots),
|
||||
KeyUID: r.KeyUID,
|
||||
}
|
||||
}
|
||||
|
||||
func ToAppStatus(r *ktypes.ApplicationStatus) *ApplicationStatus {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &ApplicationStatus{
|
||||
RemainingAttemptsPIN: r.PinRetryCount,
|
||||
RemainingAttemptsPUK: r.PUKRetryCount,
|
||||
KeyInitialized: r.KeyInitialized,
|
||||
Path: r.Path,
|
||||
}
|
||||
}
|
||||
|
||||
func ToSignature(r *ktypes.Signature) *Signature {
|
||||
return &Signature{
|
||||
R: r.R(),
|
||||
|
17
pkg/session/rpc.go
Normal file
17
pkg/session/rpc.go
Normal file
@ -0,0 +1,17 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/gorilla/rpc"
|
||||
"github.com/gorilla/rpc/json"
|
||||
)
|
||||
|
||||
var (
|
||||
globalKeycardService KeycardService
|
||||
)
|
||||
|
||||
func CreateRPCServer() (*rpc.Server, error) {
|
||||
rpcServer := rpc.NewServer()
|
||||
rpcServer.RegisterCodec(json.NewCodec(), "application/json")
|
||||
err := rpcServer.RegisterTCPService(&globalKeycardService, "keycard")
|
||||
return rpcServer, err
|
||||
}
|
307
pkg/session/service.go
Normal file
307
pkg/session/service.go
Normal file
@ -0,0 +1,307 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
goerrors "errors"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/status-im/status-keycard-go/internal"
|
||||
"github.com/status-im/status-keycard-go/pkg/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
errKeycardServiceNotStarted = errors.New("keycard service not started")
|
||||
validate = validator.New()
|
||||
)
|
||||
|
||||
func validateRequest(v interface{}) error {
|
||||
err := validate.Struct(v)
|
||||
if err != nil {
|
||||
errs := err.(validator.ValidationErrors)
|
||||
return goerrors.Join(errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type KeycardService struct {
|
||||
keycardContext *internal.KeycardContextV2
|
||||
simulateError error
|
||||
}
|
||||
|
||||
type StartRequest struct {
|
||||
StorageFilePath string `json:"storageFilePath" validate:"required"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) Start(args *StartRequest, reply *struct{}) error {
|
||||
if s.keycardContext != nil {
|
||||
return errors.New("keycard service already started")
|
||||
}
|
||||
|
||||
var err error
|
||||
s.keycardContext, err = internal.NewKeycardContextV2(args.StorageFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.keycardContext.SimulateError(s.simulateError)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.keycardContext.Start()
|
||||
}
|
||||
|
||||
func (s *KeycardService) Stop(args *struct{}, reply *struct{}) error {
|
||||
if s.keycardContext == nil {
|
||||
return nil
|
||||
}
|
||||
s.keycardContext.Stop()
|
||||
s.keycardContext = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStatus should not be really used, as Status is pushed with `status-changed` signal.
|
||||
// But it's handy to have for debugging purposes.
|
||||
func (s *KeycardService) GetStatus(args *struct{}, reply *internal.Status) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
*reply = s.keycardContext.GetStatus()
|
||||
return nil
|
||||
}
|
||||
|
||||
type InitializeRequest struct {
|
||||
PIN string `json:"pin" validate:"required,len=6"`
|
||||
PUK string `json:"puk" validate:"required,len=12"`
|
||||
PairingPassword string `json:"pairingPassword"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) Initialize(args *InitializeRequest, reply *struct{}) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
err := validateRequest(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if args.PairingPassword == "" {
|
||||
args.PairingPassword = internal.DefPairing
|
||||
}
|
||||
|
||||
err = s.keycardContext.Initialize(args.PIN, args.PUK, args.PairingPassword)
|
||||
return err
|
||||
}
|
||||
|
||||
type AuthorizeRequest struct {
|
||||
PIN string `json:"pin" validate:"required,len=6"`
|
||||
}
|
||||
|
||||
type AuthorizeResponse struct {
|
||||
Authorized bool `json:"authorized"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) Authorize(args *AuthorizeRequest, reply *AuthorizeResponse) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
err := s.keycardContext.VerifyPIN(args.PIN)
|
||||
reply.Authorized = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
type ChangePINRequest struct {
|
||||
NewPIN string `json:"newPin" validate:"required,len=6"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) ChangePIN(args *ChangePINRequest, reply *struct{}) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
err := validateRequest(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.keycardContext.ChangePIN(args.NewPIN)
|
||||
return err
|
||||
}
|
||||
|
||||
type ChangePUKRequest struct {
|
||||
NewPUK string `json:"newPuk" validate:"required,len=12"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) ChangePUK(args *ChangePUKRequest, reply *struct{}) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
err := validateRequest(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.keycardContext.ChangePUK(args.NewPUK)
|
||||
return err
|
||||
}
|
||||
|
||||
type UnblockRequest struct {
|
||||
PUK string `json:"puk" validate:"required,len=12"`
|
||||
NewPIN string `json:"newPin" validate:"required,len=6"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) Unblock(args *UnblockRequest, reply *struct{}) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
err := validateRequest(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.keycardContext.UnblockPIN(args.PUK, args.NewPIN)
|
||||
return err
|
||||
}
|
||||
|
||||
type GenerateMnemonicRequest struct {
|
||||
Length int `json:"length"`
|
||||
}
|
||||
|
||||
type GenerateMnemonicResponse struct {
|
||||
Indexes []int `json:"indexes"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) GenerateMnemonic(args *GenerateMnemonicRequest, reply *GenerateMnemonicResponse) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
indexes, err := s.keycardContext.GenerateMnemonic(args.Length)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply.Indexes = indexes
|
||||
return nil
|
||||
}
|
||||
|
||||
type LoadMnemonicRequest struct {
|
||||
Mnemonic string `json:"mnemonic" validate:"required"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
}
|
||||
|
||||
type LoadMnemonicResponse struct {
|
||||
KeyUID string `json:"keyUID"` // WARNING: Is this what's returned?
|
||||
}
|
||||
|
||||
func (s *KeycardService) LoadMnemonic(args *LoadMnemonicRequest, reply *LoadMnemonicResponse) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
err := validateRequest(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyUID, err := s.keycardContext.LoadMnemonic(args.Mnemonic, args.Passphrase)
|
||||
reply.KeyUID = utils.Btox(keyUID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *KeycardService) FactoryReset(args *struct{}, reply *struct{}) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
err := s.keycardContext.FactoryReset()
|
||||
return err
|
||||
}
|
||||
|
||||
type GetMetadataResponse struct {
|
||||
Metadata *internal.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) GetMetadata(args *struct{}, reply *GetMetadataResponse) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
var err error
|
||||
reply.Metadata, err = s.keycardContext.GetMetadata()
|
||||
return err
|
||||
}
|
||||
|
||||
type StoreMetadataRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Paths []string `json:"paths" validate:""`
|
||||
}
|
||||
|
||||
func (s *KeycardService) StoreMetadata(args *StoreMetadataRequest, reply *struct{}) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
err := validateRequest(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.keycardContext.StoreMetadata(args.Name, args.Paths)
|
||||
}
|
||||
|
||||
type ExportLoginKeysResponse struct {
|
||||
Keys *internal.LoginKeys `json:"keys"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) ExportLoginKeys(args *struct{}, reply *ExportLoginKeysResponse) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
var err error
|
||||
reply.Keys, err = s.keycardContext.ExportLoginKeys()
|
||||
return err
|
||||
}
|
||||
|
||||
type ExportRecoveredKeysResponse struct {
|
||||
Keys *internal.RecoverKeys `json:"keys"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) ExportRecoverKeys(args *struct{}, reply *ExportRecoveredKeysResponse) error {
|
||||
if s.keycardContext == nil {
|
||||
return errKeycardServiceNotStarted
|
||||
}
|
||||
|
||||
var err error
|
||||
reply.Keys, err = s.keycardContext.ExportRecoverKeys()
|
||||
return err
|
||||
}
|
||||
|
||||
type SimulateErrorRequest struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func (s *KeycardService) SimulateError(args *SimulateErrorRequest, reply *struct{}) error {
|
||||
err := validateRequest(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errToSimulate := internal.GetSimulatedError(args.Error)
|
||||
if args.Error != "" && errToSimulate == nil {
|
||||
return errors.New("unknown error to simulate")
|
||||
}
|
||||
|
||||
s.simulateError = errToSimulate
|
||||
|
||||
if s.keycardContext == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.keycardContext.SimulateError(errToSimulate)
|
||||
}
|
@ -40,6 +40,10 @@ func jsonToParams(jsonParams *C.char) (flow.FlowParams, error) {
|
||||
|
||||
//export KeycardInitFlow
|
||||
func KeycardInitFlow(storageDir *C.char) *C.char {
|
||||
if err := checkAPIMutualExclusion(flowAPI); err != nil {
|
||||
return retErr(err)
|
||||
}
|
||||
|
||||
var err error
|
||||
globalFlow, err = flow.NewFlow(C.GoString(storageDir))
|
||||
|
||||
|
85
shared/api_session.go
Normal file
85
shared/api_session.go
Normal file
@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/gorilla/rpc"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/status-im/status-keycard-go/pkg/session"
|
||||
)
|
||||
|
||||
var (
|
||||
globalRPCServer *rpc.Server
|
||||
)
|
||||
|
||||
func marshalError(err error) *C.char {
|
||||
response := struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: "",
|
||||
}
|
||||
if err != nil {
|
||||
response.Error = err.Error()
|
||||
}
|
||||
responseBytes, _ := json.Marshal(response)
|
||||
return C.CString(string(responseBytes))
|
||||
}
|
||||
|
||||
//export KeycardInitializeRPC
|
||||
func KeycardInitializeRPC() *C.char {
|
||||
if err := checkAPIMutualExclusion(sessionAPI); err != nil {
|
||||
return marshalError(err)
|
||||
}
|
||||
|
||||
// TEMP: Replace with logging to a file, take the path as an argument
|
||||
logger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to initialize log: %v\n", err)
|
||||
}
|
||||
zap.ReplaceGlobals(logger)
|
||||
|
||||
rpcServer, err := session.CreateRPCServer()
|
||||
if err != nil {
|
||||
return marshalError(err)
|
||||
}
|
||||
globalRPCServer = rpcServer
|
||||
logger.Info("RPC server initialized")
|
||||
return marshalError(nil)
|
||||
}
|
||||
|
||||
//export KeycardCallRPC
|
||||
func KeycardCallRPC(payload *C.char) *C.char {
|
||||
if globalRPCServer == nil {
|
||||
return marshalError(errors.New("RPC server not initialized"))
|
||||
}
|
||||
|
||||
payloadBytes := []byte(C.GoString(payload))
|
||||
|
||||
// Create a fake HTTP request
|
||||
req := httptest.NewRequest("POST", "/rpc", bytes.NewBuffer(payloadBytes))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Create a fake HTTP response writer
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Call the server's ServeHTTP method
|
||||
globalRPCServer.ServeHTTP(rr, req)
|
||||
|
||||
// Read and return the response body
|
||||
resp := rr.Result()
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return marshalError(errors.Wrap(err, "internal error reading response body"))
|
||||
}
|
||||
|
||||
return C.CString(string(body))
|
||||
}
|
@ -1,3 +1,30 @@
|
||||
package main
|
||||
|
||||
import "errors"
|
||||
|
||||
func main() {}
|
||||
|
||||
type api int
|
||||
|
||||
const (
|
||||
none api = iota
|
||||
flowAPI
|
||||
sessionAPI
|
||||
)
|
||||
|
||||
func checkAPIMutualExclusion(requestedAPI api) error {
|
||||
switch requestedAPI {
|
||||
case flowAPI:
|
||||
if globalRPCServer != nil {
|
||||
return errors.New("not allowed to start flow API when session API is being used")
|
||||
}
|
||||
case sessionAPI:
|
||||
if globalFlow != nil {
|
||||
return errors.New("not allowed to start session API when flow API is being used")
|
||||
}
|
||||
default:
|
||||
panic("Unknown API")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user