diff --git a/go.mod b/go.mod index 7545152..7fd8eb1 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/1password/onepassword-sdk-go -go 1.22.0 - -toolchain go1.22.5 +go 1.24.0 require ( github.com/extism/go-sdk v1.7.0 @@ -18,6 +16,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect + golang.org/x/sys v0.37.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 69904ca..dde5c6f 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZB github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= diff --git a/internal/shared_lib_core.go b/internal/shared_lib_core.go index c2eeeb0..9b36d2b 100644 --- a/internal/shared_lib_core.go +++ b/internal/shared_lib_core.go @@ -3,85 +3,26 @@ package internal import ( "context" "encoding/json" - "errors" "fmt" "log" "os" "path" "runtime" - "unsafe" ) -/* -#cgo LDFLAGS: -ldl - -#include -#include -#include - -// Function pointer types matching Rust exports -typedef int32_t (*send_message_t)( - const uint8_t* msg_ptr, - size_t msg_len, - uint8_t** out_buf, - size_t* out_len, - size_t* out_cap -); - -typedef void (*free_message_t)( - uint8_t* buf, - size_t len, - size_t cap -); - -// Trampoline for calling `send_message`, as Go cannot call function pointers directly. -static inline int32_t call_send_message( - send_message_t fn, - const uint8_t* msg_ptr, - size_t msg_len, - uint8_t** out_buf, - size_t* out_len, - size_t* out_cap -) { - return fn(msg_ptr, msg_len, out_buf, out_len, out_cap); -} - -// Trampoline for calling `free_message`, as Go cannot call function pointers directly. -static inline void call_free_message( - free_message_t fn, - uint8_t* buf, - size_t len, - size_t cap -) { - fn(buf, len, cap); -} - -// dlopen wrapper -static void* open_library(const char* path) { - return dlopen(path, RTLD_NOW); -} - -// dlsym wrapper -static void* load_symbol(void* handle, const char* name) { - return dlsym(handle, name); -} - -// dlclose wrapper -static int close_library(void* handle) { - return dlclose(handle); +// Request/Response mirror your Unix file (kept identical) +type Request struct { + Kind string `json:"kind"` + AccountName string `json:"account_name"` + Payload []byte `json:"payload"` } -*/ -import "C" - -type SharedLibCore struct { - accountName string - handle unsafe.Pointer - sendMessage C.send_message_t - freeResponse C.free_message_t +type Response struct { + Success bool `json:"success"` + Payload []byte `json:"payload"` } -var coreLib *SharedLibCore +func (r Response) Error() string { return string(r.Payload) } // find1PasswordLibPath returns the path to the 1Password shared library // (libop_sdk_ipc_client.dylib/.so/.dll) depending on OS. @@ -107,6 +48,14 @@ func find1PasswordLibPath() (string, error) { "/snap/bin/1password/libop_sdk_ipc_client.so", } + case "windows": + locations = []string{ + path.Join(home, `AppData\Local\1Password\op_sdk_ipc_client.dll`), + `C:\Program Files\1Password\app\8\op_sdk_ipc_client.dll`, + `C:\Program Files (x86)\1Password\app\8\op_sdk_ipc_client.dll`, + path.Join(home, `AppData\Local\1Password\app\8\op_sdk_ipc_client.dll`), + } + default: return "", fmt.Errorf("unsupported OS: %s", runtime.GOOS) } @@ -121,11 +70,11 @@ func find1PasswordLibPath() (string, error) { func GetSharedLibCore(accountName string) (*CoreWrapper, error) { if coreLib == nil { - path, err := find1PasswordLibPath() + libPath, err := find1PasswordLibPath() if err != nil { return nil, err } - coreLib, err = loadCore(path) + coreLib, err = loadCore(libPath) if err != nil { return nil, err } @@ -137,40 +86,6 @@ func GetSharedLibCore(accountName string) (*CoreWrapper, error) { return &coreWrapper, nil } -func loadCore(path string) (*SharedLibCore, error) { - cPath := C.CString(path) - defer C.free(unsafe.Pointer(cPath)) - - handle := C.open_library(cPath) - if handle == nil { - return nil, errors.New("failed to open library") - } - - symbol := C.CString("op_sdk_ipc_send_message") - defer C.free(unsafe.Pointer(symbol)) - - fnSend := C.load_symbol(handle, symbol) - if fnSend == nil { - C.close_library(handle) - return nil, errors.New("failed to load send_message") - } - - symbolFree := C.CString("op_sdk_ipc_free_response") - defer C.free(unsafe.Pointer(symbolFree)) - - fnFree := C.load_symbol(handle, symbolFree) - if fnFree == nil { - C.close_library(handle) - return nil, errors.New("failed to load free_message") - } - - return &SharedLibCore{ - handle: handle, - sendMessage: (C.send_message_t)(fnSend), - freeResponse: (C.free_message_t)(fnFree), - }, nil -} - // InitClient creates a client instance in the current core module and returns its unique ID. func (slc *SharedLibCore) InitClient(ctx context.Context, config []byte) ([]byte, error) { const kind = "init_client" @@ -192,6 +107,7 @@ func (slc *SharedLibCore) InitClient(ctx context.Context, config []byte) ([]byte return res, nil } +// Invoke performs an SDK operation. func (slc *SharedLibCore) Invoke(ctx context.Context, invokeConfig []byte) ([]byte, error) { const kind = "invoke" request := Request{ @@ -220,57 +136,3 @@ func (slc *SharedLibCore) ReleaseClient(clientID []byte) { log.Println("failed to release client") } } - -func (slc *SharedLibCore) callSharedLibrary(input []byte) ([]byte, error) { - if len(input) == 0 { - return nil, errors.New("internal: empty input") - } - - var outBuf *C.uint8_t - var outLen C.size_t - var outCap C.size_t - - retCode := C.call_send_message( - slc.sendMessage, - (*C.uint8_t)(unsafe.Pointer(&input[0])), - C.size_t(len(input)), - &outBuf, - &outLen, - &outCap, - ) - - if retCode != 0 { - return nil, fmt.Errorf("failed to send message to Desktop App. Please make sure the integrations is enabled or otherwise contact 1Password support. Return code: %d", int(retCode)) - } - - resp := C.GoBytes(unsafe.Pointer(outBuf), C.int(outLen)) - // Call trampoline with the function pointer - C.call_free_message(slc.freeResponse, outBuf, outLen, outCap) - - var response Response - err := json.Unmarshal(resp, &response) - if err != nil { - return nil, err - } - - if response.Success { - return response.Payload, nil - } else { - return nil, response - } -} - -type Request struct { - Kind string `json:"kind"` - AccountName string `json:"account_name"` - Payload []byte `json:"payload"` -} - -type Response struct { - Success bool `json:"success"` - Payload []byte `json:"payload"` -} - -func (r Response) Error() string { - return string(r.Payload) -} diff --git a/internal/shared_lib_core_unix.go b/internal/shared_lib_core_unix.go new file mode 100644 index 0000000..9f10400 --- /dev/null +++ b/internal/shared_lib_core_unix.go @@ -0,0 +1,154 @@ +//go:build darwin || linux + +package internal + +import ( + "encoding/json" + "errors" + "fmt" + "unsafe" +) + +/* +#cgo LDFLAGS: -ldl + +#include +#include +#include + +// Function pointer types matching Rust exports +typedef int32_t (*send_message_t)( + const uint8_t* msg_ptr, + size_t msg_len, + uint8_t** out_buf, + size_t* out_len, + size_t* out_cap +); + +typedef void (*free_message_t)( + uint8_t* buf, + size_t len, + size_t cap +); + +// Trampoline for calling `send_message`, as Go cannot call function pointers directly. +static inline int32_t call_send_message( + send_message_t fn, + const uint8_t* msg_ptr, + size_t msg_len, + uint8_t** out_buf, + size_t* out_len, + size_t* out_cap +) { + return fn(msg_ptr, msg_len, out_buf, out_len, out_cap); +} + +// Trampoline for calling `free_message`, as Go cannot call function pointers directly. +static inline void call_free_message( + free_message_t fn, + uint8_t* buf, + size_t len, + size_t cap +) { + fn(buf, len, cap); +} + +// dlopen wrapper +static void* open_library(const char* path) { + return dlopen(path, RTLD_NOW); +} + +// dlsym wrapper +static void* load_symbol(void* handle, const char* name) { + return dlsym(handle, name); +} + +// dlclose wrapper +static int close_library(void* handle) { + return dlclose(handle); +} + +*/ +import "C" + +type SharedLibCore struct { + accountName string + handle unsafe.Pointer + sendMessage C.send_message_t + freeResponse C.free_message_t +} + +var coreLib *SharedLibCore + +func loadCore(path string) (*SharedLibCore, error) { + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) + + handle := C.open_library(cPath) + if handle == nil { + return nil, errors.New("failed to open library") + } + + symbol := C.CString("op_sdk_ipc_send_message") + defer C.free(unsafe.Pointer(symbol)) + + fnSend := C.load_symbol(handle, symbol) + if fnSend == nil { + C.close_library(handle) + return nil, errors.New("failed to load send_message") + } + + symbolFree := C.CString("op_sdk_ipc_free_response") + defer C.free(unsafe.Pointer(symbolFree)) + + fnFree := C.load_symbol(handle, symbolFree) + if fnFree == nil { + C.close_library(handle) + return nil, errors.New("failed to load free_message") + } + + return &SharedLibCore{ + handle: handle, + sendMessage: (C.send_message_t)(fnSend), + freeResponse: (C.free_message_t)(fnFree), + }, nil +} + +func (slc *SharedLibCore) callSharedLibrary(input []byte) ([]byte, error) { + if len(input) == 0 { + return nil, errors.New("internal: empty input") + } + + var outBuf *C.uint8_t + var outLen C.size_t + var outCap C.size_t + + retCode := C.call_send_message( + slc.sendMessage, + (*C.uint8_t)(unsafe.Pointer(&input[0])), + C.size_t(len(input)), + &outBuf, + &outLen, + &outCap, + ) + + if retCode != 0 { + return nil, fmt.Errorf("failed to send message to Desktop App. Please make sure the integrations is enabled or otherwise contact 1Password support. Return code: %d", int(retCode)) + } + + resp := C.GoBytes(unsafe.Pointer(outBuf), C.int(outLen)) + // Call trampoline with the function pointer + C.call_free_message(slc.freeResponse, outBuf, outLen, outCap) + + var response Response + err := json.Unmarshal(resp, &response) + if err != nil { + return nil, err + } + + if response.Success { + return response.Payload, nil + } else { + return nil, response + } +} diff --git a/internal/shared_lib_core_windows.go b/internal/shared_lib_core_windows.go new file mode 100644 index 0000000..941dd84 --- /dev/null +++ b/internal/shared_lib_core_windows.go @@ -0,0 +1,94 @@ +//go:build windows + +package internal + +import ( + "encoding/json" + "errors" + "fmt" + "unsafe" + + "golang.org/x/sys/windows" +) + +type SharedLibCore struct { + accountName string + dll *windows.DLL + procSend *windows.Proc + procFree *windows.Proc +} + +var coreLib *SharedLibCore + +func loadCore(path string) (*SharedLibCore, error) { + dll, err := windows.LoadDLL(path) // absolute path avoids search path surprises + if err != nil { + return nil, err + } + send, err := dll.FindProc("op_sdk_ipc_send_message") + if err != nil { + dll.Release() + return nil, errors.New("failed to load send_message") + } + free, err := dll.FindProc("op_sdk_ipc_free_response") + if err != nil { + dll.Release() + return nil, errors.New("failed to load free_message") + } + return &SharedLibCore{ + dll: dll, + procSend: send, + procFree: free, + }, nil +} + +func (slc *SharedLibCore) callSharedLibrary(input []byte) ([]byte, error) { + if len(input) == 0 { + return nil, errors.New("internal: empty input") + } + + // Signature we’re calling (from Rust exports): + // int32_t op_sdk_ipc_send_message(const uint8_t* msg_ptr, size_t msg_len, + // uint8_t** out_buf, size_t* out_len, size_t* out_cap); + var outBuf *byte + var outLen uintptr + var outCap uintptr + + r1, _, callErr := slc.procSend.Call( + uintptr(unsafe.Pointer(&input[0])), + uintptr(len(input)), + uintptr(unsafe.Pointer(&outBuf)), + uintptr(unsafe.Pointer(&outLen)), + uintptr(unsafe.Pointer(&outCap)), + ) + // syscall layer error + if callErr != nil && callErr != windows.ERROR_SUCCESS { + return nil, callErr + } + // library-level return code + if int32(r1) != 0 { + return nil, fmt.Errorf("failed to send message to Desktop App. Return code: %d", int32(r1)) + } + + // Copy response out of the DLL buffer, then free via exported function + resp := unsafe.Slice(outBuf, outLen) + out := make([]byte, outLen) + copy(out, resp) + + // void op_sdk_ipc_free_response(uint8_t* buf, size_t len, size_t cap); + _, _, _ = slc.procFree.Call( + uintptr(unsafe.Pointer(outBuf)), + outLen, + outCap, + ) + + // Match Unix: decode envelope and return payload or error + var response Response + if err := json.Unmarshal(out, &response); err != nil { + return nil, err + } + if response.Success { + return response.Payload, nil + } + return nil, response +}