management.mx raw

   1  package main
   2  
   3  import (
   4  	"smesh.lol/web/common/helpers"
   5  	"smesh.lol/web/common/jsbridge/ext"
   6  	"smesh.lol/web/common/jsbridge/schnorr"
   7  	"smesh.lol/web/common/jsbridge/subtle"
   8  )
   9  
  10  // Management API handlers (sm3sh modal → extension).
  11  
  12  func mgmtGetVaultStatus() string {
  13  	if !vaultExists {
  14  		return "{\"result\":\"none\"}"
  15  	}
  16  	if vaultOpen {
  17  		return "{\"result\":\"unlocked\"}"
  18  	}
  19  	return "{\"result\":\"locked\"}"
  20  }
  21  
  22  func mgmtUnlockVaultAsync(paramsJSON string, respond func(string)) {
  23  	pw := helpers.JsonGetString(paramsJSON, "password")
  24  	if pw == "" {
  25  		respond("{\"error\":\"missing password\"}")
  26  		return
  27  	}
  28  	unlockVault(pw, func(ok bool) {
  29  		if ok {
  30  			respond("{\"result\":true}")
  31  		} else {
  32  			respond("{\"result\":false}")
  33  		}
  34  	})
  35  }
  36  
  37  func mgmtLockVault() string {
  38  	lockVault()
  39  	return "{\"result\":true}"
  40  }
  41  
  42  func mgmtCreateVaultAsync(paramsJSON string, respond func(string)) {
  43  	pw := helpers.JsonGetString(paramsJSON, "password")
  44  	if pw == "" {
  45  		respond("{\"error\":\"missing password\"}")
  46  		return
  47  	}
  48  	createVault(pw, func(ok bool) {
  49  		if ok {
  50  			respond("{\"result\":true}")
  51  		} else {
  52  			respond("{\"result\":false}")
  53  		}
  54  	})
  55  }
  56  
  57  func mgmtListIdentities() string {
  58  	if !vaultOpen {
  59  		return "{\"error\":\"vault locked\"}"
  60  	}
  61  	s := "["
  62  	for i, id := range identities {
  63  		if i > 0 {
  64  			s += ","
  65  		}
  66  		active := "false"
  67  		if i == activeIdx {
  68  			active = "true"
  69  		}
  70  		s += "{\"pubkey\":" + helpers.JsonString(id.Pubkey) +
  71  			",\"name\":" + helpers.JsonString(id.Name) +
  72  			",\"active\":" + active + "}"
  73  	}
  74  	return "{\"result\":" + s + "]}"
  75  }
  76  
  77  func mgmtSwitchIdentity(paramsJSON string) string {
  78  	pk := helpers.JsonGetString(paramsJSON, "pubkey")
  79  	for i, id := range identities {
  80  		if id.Pubkey == pk {
  81  			activeIdx = i
  82  			saveVault(nil)
  83  			return "{\"result\":true}"
  84  		}
  85  	}
  86  	return "{\"result\":false}"
  87  }
  88  
  89  // mgmtNsecLogin sets up an in-memory identity from an nsec without vault crypto.
  90  // Used in dev mode where crypto.subtle may be unavailable (LAN over HTTP).
  91  func mgmtNsecLogin(paramsJSON string) string {
  92  	nsec := helpers.JsonGetString(paramsJSON, "nsec")
  93  	if nsec == "" {
  94  		return "{\"error\":\"missing nsec\"}"
  95  	}
  96  	skBytes := helpers.DecodeNsec(nsec)
  97  	if skBytes == nil {
  98  		return "{\"error\":\"invalid nsec\"}"
  99  	}
 100  	pk, ok := schnorr.PubKeyFromSecKey(skBytes)
 101  	if !ok {
 102  		return "{\"error\":\"invalid key\"}"
 103  	}
 104  	pkHex := helpers.HexEncode(pk)
 105  	skHex := helpers.HexEncode(skBytes)
 106  
 107  	// Set up in-memory vault state without encryption.
 108  	vaultOpen = true
 109  	vaultExists = true
 110  	identities = []identity{{Pubkey: pkHex, Seckey: skHex}}
 111  	activeIdx = 0
 112  	return "{\"result\":true,\"pubkey\":" + helpers.JsonString(pkHex) + "}"
 113  }
 114  
 115  func mgmtAddIdentity(paramsJSON string) string {
 116  	if !vaultOpen {
 117  		return "{\"error\":\"vault locked\"}"
 118  	}
 119  	nsec := helpers.JsonGetString(paramsJSON, "nsec")
 120  	if nsec == "" {
 121  		return "{\"error\":\"missing nsec\"}"
 122  	}
 123  	skBytes := helpers.DecodeNsec(nsec)
 124  	if skBytes == nil {
 125  		return "{\"error\":\"invalid nsec\"}"
 126  	}
 127  	pk, ok := schnorr.PubKeyFromSecKey(skBytes)
 128  	if !ok {
 129  		return "{\"error\":\"invalid key\"}"
 130  	}
 131  	pkHex := helpers.HexEncode(pk)
 132  	skHex := helpers.HexEncode(skBytes)
 133  
 134  	// Check for duplicate.
 135  	for _, id := range identities {
 136  		if id.Pubkey == pkHex {
 137  			return "{\"error\":\"already exists\"}"
 138  		}
 139  	}
 140  
 141  	identities = append(identities, identity{Pubkey: pkHex, Seckey: skHex})
 142  	if activeIdx < 0 {
 143  		activeIdx = 0
 144  	}
 145  	saveVault(nil)
 146  	return "{\"result\":true,\"pubkey\":" + helpers.JsonString(pkHex) + "}"
 147  }
 148  
 149  func mgmtRemoveIdentity(paramsJSON string) string {
 150  	if !vaultOpen {
 151  		return "{\"error\":\"vault locked\"}"
 152  	}
 153  	pk := helpers.JsonGetString(paramsJSON, "pubkey")
 154  	for i, id := range identities {
 155  		if id.Pubkey == pk {
 156  			identities = append(identities[:i], identities[i+1:]...)
 157  			if activeIdx >= len(identities) {
 158  				activeIdx = len(identities) - 1
 159  			}
 160  			saveVault(nil)
 161  			return "{\"result\":true}"
 162  		}
 163  	}
 164  	return "{\"result\":false}"
 165  }
 166  
 167  func mgmtGetPermissions() string {
 168  	return getPermissionsJSON()
 169  }
 170  
 171  func mgmtSetPermission(paramsJSON string) string {
 172  	host := helpers.JsonGetString(paramsJSON, "host")
 173  	method := helpers.JsonGetString(paramsJSON, "method")
 174  	policy := helpers.JsonGetString(paramsJSON, "policy")
 175  	setPermission(host, method, policy)
 176  	return "{\"result\":true}"
 177  }
 178  
 179  func mgmtExportVaultAsync(paramsJSON string, respond func(string)) {
 180  	if !vaultOpen {
 181  		respond("{\"error\":\"vault locked\"}")
 182  		return
 183  	}
 184  	pw := helpers.JsonGetString(paramsJSON, "password")
 185  	if pw == "" {
 186  		respond("{\"error\":\"missing password\"}")
 187  		return
 188  	}
 189  
 190  	// Re-encrypt vault with export password: fresh salt, IV, Argon2id key.
 191  	salt := []byte{:32}
 192  	iv := []byte{:12}
 193  	subtle.RandomBytes(salt)
 194  	subtle.RandomBytes(iv)
 195  
 196  	passwordHash(pw, func(hash string) {
 197  		if hash == "" {
 198  			respond("{\"error\":\"hash failed\"}")
 199  			return
 200  		}
 201  		subtle.Argon2idDeriveKey(pw, salt, argon2T, argon2M, argon2P, argon2DKLen, func(key []byte) {
 202  			if len(key) == 0 {
 203  				respond("{\"error\":\"key derivation failed\"}")
 204  				return
 205  			}
 206  			// Encrypt identities with the export key.
 207  			encryptAllIdentities(key, iv, func(idJSON string) {
 208  				// Encrypt HD fields if present.
 209  				exportHDFields(key, iv, func(hdExtra string) {
 210  					s := "{\"version\":2" +
 211  						",\"iv\":" + helpers.JsonString(helpers.Base64Encode(iv)) +
 212  						",\"salt\":" + helpers.JsonString(helpers.Base64Encode(salt)) +
 213  						",\"vaultHash\":" + helpers.JsonString(hash) +
 214  						",\"identities\":" + idJSON +
 215  						",\"permissions\":[]" +
 216  						",\"relays\":[]" +
 217  						",\"selectedIdentityId\":null" +
 218  						hdExtra + "}"
 219  					respond("{\"result\":" + s + "}")
 220  				})
 221  			})
 222  		})
 223  	})
 224  }
 225  
 226  func mgmtImportVaultAsync(paramsJSON string, respond func(string)) {
 227  	data := helpers.JsonGetString(paramsJSON, "data")
 228  	if data == "" {
 229  		respond("{\"error\":\"missing data\"}")
 230  		return
 231  	}
 232  	// Validate: must have version and vaultHash at minimum.
 233  	ver := helpers.JsonGetValue(data, "version")
 234  	vh := helpers.JsonGetString(data, "vaultHash")
 235  	if ver == "" || vh == "" {
 236  		respond("{\"error\":\"invalid vault format\"}")
 237  		return
 238  	}
 239  	// Store as-is; user must unlock with the original password.
 240  	vaultRawCache = data
 241  	vaultExists = true
 242  	vaultOpen = false
 243  	ext.StorageSet(vaultStorageKey, data)
 244  	respond("{\"result\":true}")
 245  }
 246  
 247  func mgmtPromptResponse(paramsJSON string) string {
 248  	// TODO: wire prompt response to pending sign request
 249  	return "{\"result\":true}"
 250  }
 251  
 252  // --- HD keychain management ---
 253  
 254  func mgmtGenerateMnemonic() string {
 255  	m := generateMnemonic()
 256  	return "{\"result\":" + helpers.JsonString(m) + "}"
 257  }
 258  
 259  func mgmtValidateMnemonic(paramsJSON string) string {
 260  	m := helpers.JsonGetString(paramsJSON, "mnemonic")
 261  	if validateMnemonic(m) {
 262  		return "{\"result\":true}"
 263  	}
 264  	return "{\"result\":false}"
 265  }
 266  
 267  func mgmtCreateHDVaultAsync(paramsJSON string, respond func(string)) {
 268  	pw := helpers.JsonGetString(paramsJSON, "password")
 269  	name := helpers.JsonGetString(paramsJSON, "name")
 270  	if pw == "" {
 271  		respond("{\"error\":\"missing password\"}")
 272  		return
 273  	}
 274  	if name == "" {
 275  		name = "Identity 0"
 276  	}
 277  	hdCreateVault(pw, name, func(mnemonic string) {
 278  		if mnemonic == "" {
 279  			respond("{\"result\":false}")
 280  		} else {
 281  			respond("{\"result\":" + helpers.JsonString(mnemonic) + "}")
 282  		}
 283  	})
 284  }
 285  
 286  func mgmtRestoreHDVaultAsync(paramsJSON string, respond func(string)) {
 287  	pw := helpers.JsonGetString(paramsJSON, "password")
 288  	mnemonic := helpers.JsonGetString(paramsJSON, "mnemonic")
 289  	name := helpers.JsonGetString(paramsJSON, "name")
 290  	if pw == "" || mnemonic == "" {
 291  		respond("{\"error\":\"missing password or mnemonic\"}")
 292  		return
 293  	}
 294  	if name == "" {
 295  		name = "Identity 0"
 296  	}
 297  	hdRestoreVault(pw, mnemonic, name, func(ok bool) {
 298  		if ok {
 299  			respond("{\"result\":true}")
 300  		} else {
 301  			respond("{\"result\":false}")
 302  		}
 303  	})
 304  }
 305  
 306  func mgmtDeriveIdentityAsync(paramsJSON string, respond func(string)) {
 307  	if !vaultOpen {
 308  		respond("{\"error\":\"vault locked\"}")
 309  		return
 310  	}
 311  	if hdMnemonic == "" {
 312  		respond("{\"error\":\"not an HD vault\"}")
 313  		return
 314  	}
 315  	name := helpers.JsonGetString(paramsJSON, "name")
 316  	if name == "" {
 317  		name = "Identity " + itoa(hdNextAccount)
 318  	}
 319  	hdDeriveNext(name, func(pubkey string) {
 320  		if pubkey == "" {
 321  			respond("{\"result\":false}")
 322  		} else {
 323  			respond("{\"result\":" + helpers.JsonString(pubkey) + "}")
 324  		}
 325  	})
 326  }
 327  
 328  func mgmtGetMnemonic() string {
 329  	if !vaultOpen {
 330  		return "{\"error\":\"vault locked\"}"
 331  	}
 332  	if hdMnemonic == "" {
 333  		return "{\"result\":null}"
 334  	}
 335  	return "{\"result\":" + helpers.JsonString(hdMnemonic) + "}"
 336  }
 337  
 338  func mgmtProbeAccountAsync(paramsJSON string, respond func(string)) {
 339  	if !vaultOpen || hdMnemonic == "" {
 340  		respond("{\"result\":\"\"}")
 341  		return
 342  	}
 343  	idxStr := helpers.JsonGetValue(paramsJSON, "index")
 344  	idx := parseSimpleInt(idxStr)
 345  	probeHDAccount(idx, func(pk string) {
 346  		respond("{\"result\":" + helpers.JsonString(pk) + "}")
 347  	})
 348  }
 349  
 350  func mgmtIsHD() string {
 351  	if !vaultOpen {
 352  		return "{\"error\":\"vault locked\"}"
 353  	}
 354  	if hdMnemonic != "" {
 355  		return "{\"result\":true}"
 356  	}
 357  	return "{\"result\":false}"
 358  }
 359  
 360  func mgmtResetExtension() string {
 361  	lockVault()
 362  	vaultExists = false
 363  	vaultRawCache = ""
 364  	ext.StorageRemove(vaultStorageKey)
 365  	return "{\"result\":true}"
 366  }
 367