Skip to content

Commit

Permalink
feat:support admin password & bump version to 0.4
Browse files Browse the repository at this point in the history
  • Loading branch information
vimiix committed Jul 10, 2024
1 parent 3908180 commit c50bdf3
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ go.work
.idea/
.vscode/
/dist/
.enter.env
1 change: 1 addition & 0 deletions cmd/ssx/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ ssx 100 pwd`,
// no longer needed, hidden them for backwards compatibility
_ = root.Flags().MarkDeprecated("server", "it will remove in the future")
_ = root.Flags().MarkDeprecated("tag", "it will remove in the future")
_ = root.Flags().MarkDeprecated("unsafe", "no longer work after v0.4")

root.CompletionOptions.HiddenDefaultCmd = true
root.SetHelpCommand(&cobra.Command{Hidden: true})
Expand Down
10 changes: 2 additions & 8 deletions docs/zh-cn/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,15 @@ SSX 支持如下环境变量:
|`SSX_DB_PATH`| 用于存储条目的数据库文件 | ~/.ssx.db |
|`SSX_CONNECT_TIMEOUT`| SSH连接超时,单位支持 h/m/s | `10s` |
|`SSX_IMPORT_SSH_CONFIG`| 是否导入用户ssh配置 | |
|`SSX_UNSAFE_MODE`| 密码是否以不安全模式存储,默认会以服务器的设备ID进行加密,如果设置了 `SSX_SECRET_KEY` 则以该值加密| |
|`SSX_SECRET_KEY`| 用于加密条目密码的密钥 | [设备ID](#设备id) |
|`SSX_SECRET_KEY`| [v0.4+ 废弃] 为了兼容旧版本,该参数会等价于 `SSX_DEVICE_ID` | |
|`SSX_DEVICE_ID`| 数据库文件需要绑定的设备ID,可以通过设置相同的该环境变量来实现不同设备共用同一份数据库 | [设备ID](#设备id) |

## 解释

### SSX_IMPORT_SSH_CONFIG

这个环境变量不设置时,ssx 默认是不会读取用户的 `~/.ssh/config` 文件的,ssx 只使用自己存储文件进行检索。如果将这个环境变量设置为非空(任意字符串),ssx 就会在初始化的时候加载用户 ssh 配置文件中存在的服务器条目,但 ssx 仅读取用于检索和登录,并不会将这些条目持久化到 ssx 的存储文件中,所以,如果 `ssx IP` 登录时,这个 `IP``~/.ssh/config` 文件中已经配置过登录验证方式的服务器,ssx 匹配到就直接登录了。但 ssx list 查看时,该服务器会被显示到 `found in ssh config` 的表格中,这个表格中的条目是不具有 ID 属性的。

### SSX_UNSAFE_MODE

默认情况使用设备ID对条目的密码进行加密,所以此时 `SSX_DB_PATH` 所存储的信息无法直接拷贝迁移到其他机器使用,如果设置了 `SSX_UNSAFE_MODE` 则可以直接迁移到其他机器即可使用。

另外如果为了便于迁移,可以通过在多台机器上设置相同的 `SSX_SECRET_KEY` 环境变量,接口共享条目数据库文件。

### 设备ID

- Linux 使用 `/var/lib/dbus/machine-id` ([man](http://man7.org/linux/man-pages/man5/machine-id.5.html))
Expand Down
15 changes: 15 additions & 0 deletions docs/zh-cn/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# 发布日志

## v0.4.0

发布时间:2024年7月10日

**Features:**

- 强制要求给数据库文件设置管理员密码,未设置首次登录会要求补充
- 新增环境变量 `SSX_DEVIVE_ID`, 默认采用设备ID,数据库文件会绑定设备,如果迁移到其他机器需校验管理员密码后更新设备ID

**BREAKING CHANGE:**

- 废弃参数 `--unsafe`
- 废弃环境变量 `SSX_UNSAFE_MODE``SSX_SECRET_KEY`
- 如果旧版本存在 safe 模式的条目,需要在登录时重新输入一次密码

## v0.3.1

发布时间:2024年6月18日
Expand Down
19 changes: 2 additions & 17 deletions internal/encrypt/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,14 @@ import (
// Encrypt Generates the ciphertext for the given string.
// If the encryption fails, the original characters will be returned.
// If the passed string is empty, return empty directly.
func Encrypt(text string, unsafe bool) string {
func Encrypt(text string) string {
if text == "" {
return ""
}

curTime := time.Now().Format("01021504")
salt := md5encode(curTime)
key := salt[:8] + curTime
if !unsafe {
secretKey, err := utils.GetSecretKey()
if err != nil {
lg.Warn("failed to get secret key: %v", err)
}
key += secretKey
}

cipherText, err := aesEncrypt(text, key)
if err != nil {
lg.Debug("failed to encrypt text '%s': %s", utils.MaskString(text), err)
Expand All @@ -46,7 +38,7 @@ func Encrypt(text string, unsafe bool) string {
return base64.StdEncoding.EncodeToString([]byte(salt[:8] + shiftEncode(curTime) + cipherText))
}

func Decrypt(rawCipher string, unsafe bool) string {
func Decrypt(rawCipher string) string {
if rawCipher == "" {
return ""
}
Expand All @@ -59,13 +51,6 @@ func Decrypt(rawCipher string, unsafe bool) string {

key := string(dec[:8]) + shiftDecode(string(dec[8:16]))
text := string(dec[16:])
if !unsafe {
secretKey, err := utils.GetSecretKey()
if err != nil {
lg.Warn("failed to get secret key: %v", err)
}
key += secretKey
}
res, err := aesDecrypt(text, key)
if err != nil {
lg.Debug("failed to decypt cipher '%s': %s", text, err)
Expand Down
4 changes: 2 additions & 2 deletions internal/encrypt/encrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestEncryptDecrypt(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := Decrypt(Encrypt(tt.text, false), false)
actual := Decrypt(Encrypt(tt.text))
assert.Equal(t, tt.text, actual)
})
}
Expand All @@ -35,7 +35,7 @@ func TestDecrypt(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := Decrypt(tt.cipher, true)
actual := Decrypt(tt.cipher)
assert.Equal(t, tt.expect, actual)
})
}
Expand Down
31 changes: 19 additions & 12 deletions internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"archive/tar"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"net/url"
Expand Down Expand Up @@ -98,25 +100,22 @@ func MatchAddress(addr string) (*Address, error) {
return addrObj, nil
}

func to16chars(s string) string {
if len(s) >= 16 {
return s[:16]
}
return s + strings.Repeat("=", 16-len(s))
}

// GetSecretKey get secret key from env, if not set returns machine id
// GetDeviceID get secret key from env, if not set returns machine id
// always returns 16 characters key
func GetSecretKey() (string, error) {
func GetDeviceID() (string, error) {
if os.Getenv(env.SSXDeviceID) != "" {
return os.Getenv(env.SSXDeviceID), nil
}
if os.Getenv(env.SSXSecretKey) != "" {
return to16chars(os.Getenv(env.SSXSecretKey)), nil
lg.Warn("env SSX_SECRET_KEY is deprecated, please use SSX_DEVICE_ID instead")
return os.Getenv(env.SSXSecretKey), nil
}
// ref: https://man7.org/linux/man-pages/man5/machine-id.5.html
machineID, err := machineid.ProtectedID("ssx")
if err != nil {
return "", err
return "", errors.Wrap(err, "failed to get machine id")
}
return to16chars(machineID), nil
return machineID, nil
}

func DownloadFile(ctx context.Context, urlStr string, saveFile string) error {
Expand Down Expand Up @@ -273,3 +272,11 @@ func CopyFile(src, dst string, perm os.FileMode) error {
_, err = io.Copy(tf, sf)
return err
}

// HashWithSHA256 hashes the input string using SHA-256 and returns the hexadecimal representation of the hash
func HashWithSHA256(input string) string {
hash := sha256.New()
hash.Write([]byte(input))
hashedBytes := hash.Sum(nil)
return hex.EncodeToString(hashedBytes)
}
37 changes: 35 additions & 2 deletions internal/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,47 @@ func TestMatchAddress(t *testing.T) {

func TestGetSecretKeyShort(t *testing.T) {
os.Setenv(env.SSXSecretKey, "abc")
res, err := GetSecretKey()
res, err := GetDeviceID()
assert.NoError(t, err)
assert.Equal(t, "abc=============", res)
}

func TestGetSecretKeyLong(t *testing.T) {
os.Setenv(env.SSXSecretKey, "abcdefghijklmnopqrstuvwxyz")
res, err := GetSecretKey()
res, err := GetDeviceID()
assert.NoError(t, err)
assert.Equal(t, "abcdefghijklmnop", res)
}

func TestHashWithSHA256(t *testing.T) {
type args struct {
input string
}
tests := []struct {
name string
args args
want string
}{
{
name: "Hash empty string",
args: args{
input: "",
},
want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
name: "Hash non-empty string",
args: args{
input: "hello world",
},
want: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := HashWithSHA256(tt.args.input); got != tt.want {
t.Errorf("HashWithSHA256() = %v, want %v", got, tt.want)
}
})
}
}
8 changes: 4 additions & 4 deletions ssx/bbolt/bbolt.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ func NewRepo(file string) *Repo {
}

func encodeEntry(e *entry.Entry) ([]byte, error) {
e.Password = encrypt.Encrypt(e.Password, e.IsUnsafe())
e.Passphrase = encrypt.Encrypt(e.Passphrase, e.IsUnsafe())
e.Password = encrypt.Encrypt(e.Password)
e.Passphrase = encrypt.Encrypt(e.Passphrase)
return json.Marshal(e)
}

Expand All @@ -215,7 +215,7 @@ func decodeEntry(bs []byte) (*entry.Entry, error) {
if err := json.Unmarshal(bs, e); err != nil {
return nil, err
}
e.Password = encrypt.Decrypt(e.Password, e.IsUnsafe())
e.Passphrase = encrypt.Decrypt(e.Passphrase, e.IsUnsafe())
e.Password = encrypt.Decrypt(e.Password)
e.Passphrase = encrypt.Decrypt(e.Passphrase)
return e, nil
}
6 changes: 1 addition & 5 deletions ssx/entry/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,13 @@ type Entry struct {
KeyPath string `json:"key_path"`
Passphrase string `json:"passphrase"`
Password string `json:"password"`
SafeMode string `json:"safe_mode"`
Tags []string `json:"tags"`
Source string `json:"source"` // Data source, used to distinguish that it is from ssx stored or local ssh configuration
CreateAt time.Time `json:"create_at"`
UpdateAt time.Time `json:"update_at"`
Proxy *Proxy `json:"proxy"`
}

func (e *Entry) IsUnsafe() bool {
return e.SafeMode == ModeUninit || e.SafeMode == ModeUnsafe
}

func (e *Entry) String() string {
return fmt.Sprintf("%s@%s:%s", e.User, e.Host, e.Port)
}
Expand Down Expand Up @@ -177,6 +172,7 @@ func sshHostKeyCallback() (ssh.HostKeyCallback, error) {
return cb, nil
}

// Tidy performs cleanup and validation on the Entry struct.
func (e *Entry) Tidy() error {
if len(e.User) <= 0 {
e.User = defaultUser
Expand Down
5 changes: 3 additions & 2 deletions ssx/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ const (
SSXDBPath = "SSX_DB_PATH"
SSXConnectTimeout = "SSX_CONNECT_TIMEOUT"
SSXImportSSHConfig = "SSX_IMPORT_SSH_CONFIG" // 设置了该环境变量的话,就会自动将 ~/.ssh/config 中的条目也加载
SSXUnsafeMode = "SSX_UNSAFE_MODE"
SSXSecretKey = "SSX_SECRET_KEY"
SSXUnsafeMode = "SSX_UNSAFE_MODE" // deprecated
SSXSecretKey = "SSX_SECRET_KEY" // deprecated, replaced by SSX_DEVICE_ID
SSXDeviceID = "SSX_DEVICE_ID"
)

func IsUnsafeMode() bool {
Expand Down
Loading

0 comments on commit c50bdf3

Please sign in to comment.