diff --git a/.vscode/launch.json b/.vscode/launch.json index d19a737..27adb24 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -36,6 +36,15 @@ "mode": "debug", "program": "${workspaceFolder}/sample/simple", "cwd": "${workspaceFolder}/sample/simple/build" + }, + { + "name": "Launch Account Manager", + "type": "go", + "request": "launch", + "mode": "debug", + "cwd": "${workspaceFolder}/internal/builds/linux/account_manager", + "program": "${workspaceFolder}/internal/cwd/account_manager", + "buildFlags": "-tags=IDE" } ] } \ No newline at end of file diff --git a/foundation/app/rpc/responseUtils.go b/foundation/app/rpc/responseUtils.go index 7b2b54f..7bf17aa 100644 --- a/foundation/app/rpc/responseUtils.go +++ b/foundation/app/rpc/responseUtils.go @@ -23,7 +23,8 @@ import ( const SUCCESS_CODE = 200 const SYSTEMERR_CODE = 400 // theres something wrong with the system const INVALID_CODE = 401 -const NOT_IMPLEMENTED = 300 // code was not implemented +const NOT_IMPLEMENTED = 300 // code was not implemented +const INVALID_PARAMETER_PROVIDED = 402 // parameters was invalid // return json string for response func CreateResponse(code int16, msg string) string { diff --git a/foundation/event/data/actionGroup.go b/foundation/event/data/actionGroup.go index 70a401e..6faa0f0 100755 --- a/foundation/event/data/actionGroup.go +++ b/foundation/event/data/actionGroup.go @@ -16,8 +16,8 @@ import ( ) type ActionGroup struct { - Activity *Action `json:"activity"` - Service *Action `json:"service"` + Activity *Action `json:"activity,omitempty"` + Service *Action `json:"service,omitempty"` //Broadcasts []Action } diff --git a/internal/builds/linux/account_manager/info.json b/internal/builds/linux/account_manager/info.json new file mode 100644 index 0000000..11bf9e7 --- /dev/null +++ b/internal/builds/linux/account_manager/info.json @@ -0,0 +1,19 @@ +{ + "name": "Account Manager", + "description": "Service that manage Elabox account and account related authentications.", + "packageId": "ela.account", + "build": 1, + "version": "0.1.0", + "program": "ela.account", + "programArgs": null, + "activityGroup": { + "customLink": "", + "customPort": 0, + "activities": null + }, + "permissions": [], + "exportService": true, + "location": "system", + "nodejs": false, + "packagerVersion": "0.1.0" +} \ No newline at end of file diff --git a/internal/builds/linux/account_manager/packager.json b/internal/builds/linux/account_manager/packager.json new file mode 100644 index 0000000..ed3f936 --- /dev/null +++ b/internal/builds/linux/account_manager/packager.json @@ -0,0 +1,8 @@ +{ + "cwd": "", + "config": "info.json", + "binDir": "", + "bin": "./bin/ela.account", + "packages": [], + "customInstaller": "" +} \ No newline at end of file diff --git a/internal/builds/linux/system/packager.json b/internal/builds/linux/system/packager.json index 988d4e7..8097a45 100644 --- a/internal/builds/linux/system/packager.json +++ b/internal/builds/linux/system/packager.json @@ -8,6 +8,7 @@ "../../../../../elabox-logs/build/ela.logs.box", "../../../../../elabox-rewards/build/ela.rewards.box", "../packageinstaller/ela.installer.box", + "../account_manager/ela.account.box", "../companion/ela.companion.box", "../carrier/ela.carrier.box", "../../../../../elabox-dapp-store/build/ela.store/ela.store.box", diff --git a/internal/cwd/account_manager/account.go b/internal/cwd/account_manager/account.go new file mode 100644 index 0000000..34270e5 --- /dev/null +++ b/internal/cwd/account_manager/account.go @@ -0,0 +1,12 @@ +package main + +type Account struct { + Address string `json:"address"` +} + +func RetrievePrimaryAccountDetails() map[string]interface{} { + result := make(map[string]interface{}) + wallet, _ := LoadWalletAddr() + result["wallet"] = wallet + return result +} diff --git a/internal/cwd/account_manager/auth.go b/internal/cwd/account_manager/auth.go new file mode 100644 index 0000000..f2f6790 --- /dev/null +++ b/internal/cwd/account_manager/auth.go @@ -0,0 +1,91 @@ +package main + +import ( + "crypto/sha256" + "errors" + "os" + "os/exec" + "strings" + + "github.com/cansulting/elabox-system-tools/foundation/perm" + "github.com/cansulting/elabox-system-tools/foundation/system" +) + +const SHADOW_FILE = "/etc/shadow" + +// check authorization for specific DID, return true if DID is authorized +func AuthenticateDid(did string) bool { + deviceSerial := system.GetDeviceInfo().Serial + hash := sha256.Sum256([]byte(did + deviceSerial)) + // step: load the currently saved did hash + savedHash, err := os.ReadFile(DID_HASH_PATH) + if err != nil { + return false + } + if string(hash[:]) != string(savedHash) { + return false + } + return true +} + +func IsDidSetup() bool { + if _, err := os.Stat(DID_HASH_PATH); err != nil { + return false + } + return true +} + +// use to authenticate specific password +func AuthenticateSystemAccount(username string, password string) (error, bool) { + contents, err := os.ReadFile(SHADOW_FILE) + if err != nil { + return err, false + } + hashContent := Grep(username, string(contents)) + // unable to find specific user account + if hashContent == "" { + return nil, false + } + creds := strings.Split(hashContent, "$") + salt := creds[2] + savedHash := strings.Split(hashContent, ":")[1] + encryptType := creds[1] + // generate hash + cmd := exec.Command( + "/usr/bin/openssl", + "passwd", "-"+encryptType, + "-salt", salt, + password, + ) + hash, err := cmd.CombinedOutput() + if err != nil { + return err, false + } + // password is correct + strHash := string(hash) + strHash = strings.TrimRight(strHash, "\n") + if savedHash == strHash { + return nil, true + } + return nil, false +} + +// set the current device did +func SetDeviceDid(presentation map[string]interface{}) error { + // step: validate presentation + if presentation["holder"] == nil { + return errors.New("no holder provider in presentation") + } + // step: create hash + did := presentation["holder"].(string) + deviceSerial := system.GetDeviceInfo().Serial + hash := sha256.Sum256([]byte(did + deviceSerial)) + // step: save to file + if err := os.MkdirAll(DID_DATA_DIR, perm.PUBLIC_WRITE); err != nil { + return err + } + if err := os.WriteFile(DID_HASH_PATH, hash[:], perm.PUBLIC_VIEW); err != nil { + return err + } + return nil +} diff --git a/internal/cwd/account_manager/constants.go b/internal/cwd/account_manager/constants.go new file mode 100644 index 0000000..b671b5a --- /dev/null +++ b/internal/cwd/account_manager/constants.go @@ -0,0 +1,16 @@ +package main + +import "github.com/cansulting/elabox-system-tools/foundation/app" + +// actions +const AC_AUTH_DID = "account.actions.AUTH_DID" // use to authenticaticate specific did +const AC_SETUP_CHECK = "account.actions.DID_SETUP_CHECK" // use to check if theres an existing did setup +const AC_SETUP_DID = "account.actions.DID_SETUP" // use to setup did + +const PACKAGE_ID = "ela.account" +const HOME_DIR = "/home/elabox" +const DID_DATA_DIR = HOME_DIR + "/data/" + PACKAGE_ID +const DID_HASH_PATH = DID_DATA_DIR + "/did.dat" +const KEYSTORE_PATH = "/home/elabox/documents/ela.mainchain/keystore.dat" + +var Controller *app.Controller diff --git a/internal/cwd/account_manager/main.go b/internal/cwd/account_manager/main.go new file mode 100644 index 0000000..314ee1b --- /dev/null +++ b/internal/cwd/account_manager/main.go @@ -0,0 +1,12 @@ +package main + +import "github.com/cansulting/elabox-system-tools/foundation/app" + +func main() { + con, err := app.NewController(nil, &MyService{}) + if err != nil { + panic(err) + } + Controller = con + app.RunApp(con) +} diff --git a/internal/cwd/account_manager/main_test.go b/internal/cwd/account_manager/main_test.go new file mode 100644 index 0000000..3acea6a --- /dev/null +++ b/internal/cwd/account_manager/main_test.go @@ -0,0 +1,31 @@ +package main + +import "testing" + +const SAMPLE_DID = "HELLO_DID_USER" +const SAMPLE_USERNAME = "elabox" +const SAMPLE_PASSWORD = "elabox" + +// use to test device did +func Test_SetDeviceDid(t *testing.T) { + presentation := make(map[string]interface{}) + presentation["holder"] = SAMPLE_DID + if err := SetDeviceDid(presentation); err != nil { + t.Error(err) + } +} + +func Test_AuthenticationDid(t *testing.T) { + if !AuthenticateDid(SAMPLE_DID) { + t.Error("failed to authenticate did") + } +} + +func Test_AuthenticateSystemAccount(t *testing.T) { + err, success := AuthenticateSystemAccount(SAMPLE_USERNAME, SAMPLE_PASSWORD) + if err != nil { + t.Error(err) + return + } + t.Log("Authenticate", success) +} diff --git a/internal/cwd/account_manager/myservice.go b/internal/cwd/account_manager/myservice.go new file mode 100644 index 0000000..18aa4e7 --- /dev/null +++ b/internal/cwd/account_manager/myservice.go @@ -0,0 +1,87 @@ +package main + +import ( + "github.com/cansulting/elabox-system-tools/foundation/app/rpc" + "github.com/cansulting/elabox-system-tools/foundation/event/data" + "github.com/cansulting/elabox-system-tools/foundation/event/protocol" +) + +type MyService struct { +} + +func (instance *MyService) IsRunning() bool { + return true +} + +func (instance *MyService) OnStart() error { + Controller.RPC.OnRecieved(AC_AUTH_DID, instance.onAuthDidAction) + Controller.RPC.OnRecieved(AC_SETUP_CHECK, instance.onCheckSetup) + Controller.RPC.OnRecieved(AC_SETUP_DID, instance.onSetupDid) + return nil +} + +func (instance *MyService) OnEnd() error { + return nil +} + +// authorize user given the did presentation, upon success returns JWT token +func (instance *MyService) onAuthDidAction(client protocol.ClientInterface, action data.Action) string { + presentation, err := action.DataToMap() + // step: validate presentation + if err != nil { + return rpc.CreateResponse(rpc.INVALID_CODE, "invalid did presentation provided, "+err.Error()) + } + if presentation["holder"] == nil { + return rpc.CreateResponse(rpc.INVALID_CODE, "no holder was provided") + } + // step: compare with the existing hash + did := presentation["holder"].(string) + if AuthenticateDid(did) { + acc := RetrievePrimaryAccountDetails() + return rpc.CreateJsonResponse(rpc.SUCCESS_CODE, acc) + } else { + return rpc.CreateResponse(rpc.INVALID_CODE, "incorrect did credentials") + } +} + +// use to check whether the elabox was setup already. return "setup" if already setup +func (instance *MyService) onCheckSetup(client protocol.ClientInterface, action data.Action) string { + if IsDidSetup() { + return rpc.CreateSuccessResponse("setup") + } else { + return rpc.CreateSuccessResponse("not_setup") + } +} + +// use to setup did, requires did presentation, username and password +func (instance *MyService) onSetupDid(client protocol.ClientInterface, action data.Action) string { + acData, err := action.DataToMap() + // step: validate inputs + if err != nil { + return rpc.CreateResponse(rpc.INVALID_PARAMETER_PROVIDED, "invalid parameters provided, "+err.Error()) + } + if acData["presentation"] == nil { + return rpc.CreateResponse(rpc.INVALID_PARAMETER_PROVIDED, "no presentation provided") + } + + // step: authenticate for existing did setup + if IsDidSetup() { + if acData["username"] == nil || acData["password"] == nil { + return rpc.CreateResponse(rpc.INVALID_PARAMETER_PROVIDED, "username and password is required") + } + pass := acData["password"].(string) + username := acData["username"].(string) + err, success := AuthenticateSystemAccount(username, pass) + if err != nil { + return rpc.CreateResponse(rpc.SYSTEMERR_CODE, err.Error()) + } + if !success { + return rpc.CreateResponse(rpc.INVALID_PARAMETER_PROVIDED, "password or username is incorrect") + } + } + presentation := acData["presentation"].(map[string]interface{}) + if err := SetDeviceDid(presentation); err != nil { + return rpc.CreateResponse(rpc.SYSTEMERR_CODE, "failed to setup did, "+err.Error()) + } + return rpc.CreateSuccessResponse("success") +} diff --git a/internal/cwd/account_manager/utils.go b/internal/cwd/account_manager/utils.go new file mode 100644 index 0000000..fa6ceb6 --- /dev/null +++ b/internal/cwd/account_manager/utils.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/json" + "os" + "strings" +) + +type Address struct { + Address string `json:"Address"` + ProgramHash string `json:"ProgramHash"` +} + +type KeyStore struct { + Account []Address `json:"Account"` +} + +func Grep(keyword string, src string) string { + splits := strings.Split(src, "\n") + keywordbt := []byte(keyword) + for _, line := range splits { + if len(line) >= len(keywordbt) { + found := true + for i, keywordC := range keywordbt { + if keywordC != line[i] { + found = false + break + } + } + if found { + return line + } + } + } + // nothing was found + return "" +} + +// use to retrieve wallet address from keystore path +func LoadWalletAddr() (string, error) { + dat, err := os.ReadFile(KEYSTORE_PATH) + if err != nil { + return "", err + } + + var keyStore KeyStore + _ = json.Unmarshal(dat, &keyStore) + return keyStore.Account[0].Address, nil +} diff --git a/internal/scripts/build.sh b/internal/scripts/build.sh index 7f4e201..fd0ab6b 100755 --- a/internal/scripts/build.sh +++ b/internal/scripts/build.sh @@ -117,6 +117,13 @@ eval "$gobuild" -o $buildpath/$system_name/bin ../cwd/$system_name programName=$(jq ".program" $buildpath/$system_name/info.json | sed 's/\"//g') mv $buildpath/$system_name/bin/$system_name $buildpath/$system_name/bin/$programName +# build account manager +echo "Building Account Manager" +mkdir -p $buildpath/account_manager/bin +eval "$gobuild" -o $buildpath/account_manager/bin ../cwd/account_manager +programName=$(jq ".program" $buildpath/account_manager/info.json | sed 's/\"//g') +mv $buildpath/account_manager/bin/account_manager $buildpath/account_manager/bin/$programName + # build reward if exists if [ -d "$ELA_REWARDS" ]; then wd=$PWD @@ -280,6 +287,7 @@ packager $buildpath/esc/packager.json packager $buildpath/carrier/packager.json packager $buildpath/mainchain/packager.json packager $buildpath/$packageinstaller/packager.json +packager $buildpath/account_manager/packager.json packager $buildpath/feeds/packager.json packager $buildpath/$system_name/packager.json