management.mx raw

   1  package main
   2  
   3  import (
   4  	"git.smesh.lol/smesh/web/common/helpers"
   5  	"git.smesh.lol/smesh/web/common/jsbridge/ext"
   6  	"git.smesh.lol/smesh/web/common/jsbridge/schnorr"
   7  	"git.smesh.lol/smesh/web/common/jsbridge/subtle"
   8  )
   9  
  10  // Management API: vault lifecycle, identity CRUD, export/import.
  11  
  12  func mgmtGetVaultStatus() (s string) {
  13  	if !vaultExists {
  14  		return jsonResult(string(append([]byte(nil), '"', 'n', 'o', 'n', 'e', '"')))
  15  	}
  16  	if vaultOpen {
  17  		return jsonResult(string(append([]byte(nil), '"', 'u', 'n', 'l', 'o', 'c', 'k', 'e', 'd', '"')))
  18  	}
  19  	return jsonResult(string(append([]byte(nil), '"', 'l', 'o', 'c', 'k', 'e', 'd', '"')))
  20  }
  21  
  22  func mgmtUnlockVault(paramsJSON string) (s string) {
  23  	kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))
  24  	pw := helpers.JsonGetString(paramsJSON, kPassword)
  25  	if pw == "" {
  26  		return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')))
  27  	}
  28  	if unlockVault(pw) {
  29  		return jsonTrue()
  30  	}
  31  	if lastUnlockErr != "" {
  32  		return jsonErr(lastUnlockErr)
  33  	}
  34  	return jsonFalse()
  35  }
  36  
  37  func mgmtCreateVault(paramsJSON string) (s string) {
  38  	kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))
  39  	pw := helpers.JsonGetString(paramsJSON, kPassword)
  40  	if pw == "" {
  41  		return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')))
  42  	}
  43  	if createVault(pw) {
  44  		return jsonTrue()
  45  	}
  46  	return jsonFalse()
  47  }
  48  
  49  func mgmtLockVault() (s string) {
  50  	lockVault()
  51  	return jsonTrue()
  52  }
  53  
  54  func mgmtListIdentities() (s string) {
  55  	if !vaultOpen {
  56  		return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
  57  	}
  58  	b := append([]byte(nil), '{', '"', 'r', 'e', 's', 'u', 'l', 't', '"', ':', '[')
  59  	for i, id := range identities {
  60  		if i > 0 {
  61  			b = append(b, ',')
  62  		}
  63  		b = append(b, '{', '"', 'p', 'u', 'b', 'k', 'e', 'y', '"', ':')
  64  		b = b | helpers.JsonString(id.Pubkey)
  65  		b = append(b, ',', '"', 'n', 'a', 'm', 'e', '"', ':')
  66  		b = b | helpers.JsonString(id.Name)
  67  		b = append(b, '}')
  68  	}
  69  	return string(append(b, ']', '}'))
  70  }
  71  
  72  func mgmtSwitchIdentity(paramsJSON string) (s string) {
  73  	if !vaultOpen {
  74  		return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
  75  	}
  76  	kPubkey := string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y'))
  77  	pk := helpers.JsonGetString(paramsJSON, kPubkey)
  78  	for i, id := range identities {
  79  		if id.Pubkey == pk {
  80  			activeIdx = i
  81  			cacheSession()
  82  			return jsonTrue()
  83  		}
  84  	}
  85  	return jsonFalse()
  86  }
  87  
  88  func mgmtAddIdentity(paramsJSON string) (s string) {
  89  	if !vaultOpen {
  90  		return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
  91  	}
  92  	kNsec := string(append([]byte(nil), 'n', 's', 'e', 'c'))
  93  	kSeckey := string(append([]byte(nil), 's', 'e', 'c', 'k', 'e', 'y'))
  94  	kName := string(append([]byte(nil), 'n', 'a', 'm', 'e'))
  95  	nsec := helpers.JsonGetString(paramsJSON, kNsec)
  96  	sk := helpers.JsonGetString(paramsJSON, kSeckey)
  97  	name := helpers.JsonGetString(paramsJSON, kName)
  98  	var skBytes []byte
  99  	if nsec != "" {
 100  		// Page-side API passes bech32 nsec.
 101  		raw, ok := decodeNsec(nsec)
 102  		if !ok {
 103  			return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'n', 's', 'e', 'c')))
 104  		}
 105  		skBytes = raw[:]
 106  		sk = helpers.HexEncode(skBytes)
 107  	} else if sk != "" {
 108  		// Backward-compat: hex seckey.
 109  		skBytes = helpers.HexDecode(sk)
 110  		if skBytes == nil {
 111  			return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 's', 'e', 'c', 'k', 'e', 'y')))
 112  		}
 113  	} else {
 114  		// Neither nsec nor seckey provided: generate a fresh random identity.
 115  		var raw [32]byte
 116  		subtle.RandomBytes(raw[:])
 117  		skBytes = raw[:]
 118  		sk = helpers.HexEncode(skBytes)
 119  	}
 120  	pkBytes, ok := schnorr.PubKeyFromSecKey(skBytes)
 121  	if !ok {
 122  		return jsonErr(string(append([]byte(nil), 'k', 'e', 'y', ' ', 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
 123  	}
 124  	pk := helpers.HexEncode(pkBytes)
 125  	for _, id := range identities {
 126  		if id.Pubkey == pk {
 127  			return jsonErr(string(append([]byte(nil), 'i', 'd', 'e', 'n', 't', 'i', 't', 'y', ' ', 'e', 'x', 'i', 's', 't', 's')))
 128  		}
 129  	}
 130  	identities = append(identities, identity{Pubkey: pk, Seckey: sk, Name: name})
 131  	if activeIdx < 0 {
 132  		activeIdx = len(identities) - 1
 133  	}
 134  	saveVault()
 135  	return jsonResult(helpers.JsonString(pk))
 136  }
 137  
 138  // decodeNsec decodes a bech32-encoded nsec1... string into 32 raw bytes.
 139  // Returns ([32]byte{}, false) on invalid format/length/charset.
 140  func decodeNsec(nsec string) ([32]byte, bool) {
 141  	var zero [32]byte
 142  	prefix := string(append([]byte(nil), 'n', 's', 'e', 'c', '1'))
 143  	if len(nsec) < 5 || nsec[:5] != prefix {
 144  		return zero, false
 145  	}
 146  	sep := -1
 147  	for i := len(nsec) - 1; i >= 0; i-- {
 148  		if nsec[i] == '1' {
 149  			sep = i
 150  			break
 151  		}
 152  	}
 153  	if sep < 0 || len(nsec)-sep-1 <= 6 {
 154  		return zero, false
 155  	}
 156  	data := nsec[sep+1 : len(nsec)-6]
 157  	out := []byte{:32}[:0]
 158  	acc := uint32(0)
 159  	bits := uint32(0)
 160  	for i := 0; i < len(data); i++ {
 161  		v, ok := bech32CharVal(data[i])
 162  		if !ok {
 163  			return zero, false
 164  		}
 165  		acc = (acc << 5) | uint32(v)
 166  		bits += 5
 167  		for bits >= 8 {
 168  			bits -= 8
 169  			out = append(out, byte(acc>>bits))
 170  		}
 171  	}
 172  	if len(out) != 32 {
 173  		return zero, false
 174  	}
 175  	var result [32]byte
 176  	for i := 0; i < 32; i++ {
 177  		result[i] = out[i]
 178  	}
 179  	return result, true
 180  }
 181  
 182  func mgmtRemoveIdentity(paramsJSON string) (s string) {
 183  	if !vaultOpen {
 184  		return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
 185  	}
 186  	kPubkey := string(append([]byte(nil), 'p', 'u', 'b', 'k', 'e', 'y'))
 187  	pk := helpers.JsonGetString(paramsJSON, kPubkey)
 188  	for i, id := range identities {
 189  		if id.Pubkey == pk {
 190  			identities = identities[:i] | identities[i+1:]
 191  			if activeIdx >= len(identities) {
 192  				activeIdx = len(identities) - 1
 193  			}
 194  			saveVault()
 195  			return jsonTrue()
 196  		}
 197  	}
 198  	return jsonFalse()
 199  }
 200  
 201  func mgmtNsecLogin(paramsJSON string) (s string) {
 202  	kNsec := string(append([]byte(nil), 'n', 's', 'e', 'c'))
 203  	nsec := helpers.JsonGetString(paramsJSON, kNsec)
 204  	if nsec == "" {
 205  		return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'n', 's', 'e', 'c')))
 206  	}
 207  	raw, ok := decodeNsec(nsec)
 208  	if !ok {
 209  		return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'n', 's', 'e', 'c')))
 210  	}
 211  	skBytes := raw[:]
 212  	sk := helpers.HexEncode(skBytes)
 213  	pkBytes, ok := schnorr.PubKeyFromSecKey(skBytes)
 214  	if !ok {
 215  		return jsonErr(string(append([]byte(nil), 'k', 'e', 'y', ' ', 'd', 'e', 'r', 'i', 'v', 'a', 't', 'i', 'o', 'n', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
 216  	}
 217  	pk := helpers.HexEncode(pkBytes)
 218  	identities = []identity{{Pubkey: pk, Seckey: sk, Name: ""}}
 219  	activeIdx = 0
 220  	vaultOpen = true
 221  	vaultExists = true
 222  	vaultVersion = 0
 223  	_ = ext.StorageSet
 224  	return jsonTrue()
 225  }
 226  
 227  // bech32CharVal returns the 5-bit value for a bech32 character.
 228  // Charset: "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
 229  func bech32CharVal(c byte) (byte, bool) {
 230  	switch c {
 231  	case 'q':
 232  		return 0, true
 233  	case 'p':
 234  		return 1, true
 235  	case 'z':
 236  		return 2, true
 237  	case 'r':
 238  		return 3, true
 239  	case 'y':
 240  		return 4, true
 241  	case '9':
 242  		return 5, true
 243  	case 'x':
 244  		return 6, true
 245  	case '8':
 246  		return 7, true
 247  	case 'g':
 248  		return 8, true
 249  	case 'f':
 250  		return 9, true
 251  	case '2':
 252  		return 10, true
 253  	case 't':
 254  		return 11, true
 255  	case 'v':
 256  		return 12, true
 257  	case 'd':
 258  		return 13, true
 259  	case 'w':
 260  		return 14, true
 261  	case '0':
 262  		return 15, true
 263  	case 's':
 264  		return 16, true
 265  	case '3':
 266  		return 17, true
 267  	case 'j':
 268  		return 18, true
 269  	case 'n':
 270  		return 19, true
 271  	case '5':
 272  		return 20, true
 273  	case '4':
 274  		return 21, true
 275  	case 'k':
 276  		return 22, true
 277  	case 'h':
 278  		return 23, true
 279  	case 'c':
 280  		return 24, true
 281  	case 'e':
 282  		return 25, true
 283  	case '6':
 284  		return 26, true
 285  	case 'm':
 286  		return 27, true
 287  	case 'u':
 288  		return 28, true
 289  	case 'a':
 290  		return 29, true
 291  	case '7':
 292  		return 30, true
 293  	case 'l':
 294  		return 31, true
 295  	}
 296  	return 0, false
 297  }
 298  
 299  func mgmtExportVault(paramsJSON string) (s string) {
 300  	if !vaultOpen {
 301  		return jsonErr(string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', ' ', 'l', 'o', 'c', 'k', 'e', 'd')))
 302  	}
 303  	kPassword := string(append([]byte(nil), 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'))
 304  	pw := helpers.JsonGetString(paramsJSON, kPassword)
 305  	if pw == "" {
 306  		return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd')))
 307  	}
 308  	salt := []byte{:32}
 309  	subtle.RandomBytes(salt)
 310  	hash := passwordHash(pw)
 311  	if hash == "" {
 312  		return jsonErr(string(append([]byte(nil), 'h', 'a', 's', 'h', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
 313  	}
 314  	key := subtle.Argon2idDeriveKey(pw, salt, argon2T, argon2MNew, argon2P, argon2DKLen)
 315  	if len(key) == 0 {
 316  		return jsonErr(string(append([]byte(nil), 'k', 'e', 'y', ' ', 'f', 'a', 'i', 'l', 'e', 'd')))
 317  	}
 318  	b := []byte{:512}[:0]
 319  	b = append(b, '{', '"', 'v', 'e', 'r', 's', 'i', 'o', 'n', '"', ':', '3')
 320  	b = append(b, ',', '"', 'a', 'r', 'g', 'o', 'n', '2', 'M', '"', ':')
 321  	b = b | helpers.Itoa(int64(argon2MNew))
 322  	b = append(b, ',', '"', 's', 'a', 'l', 't', '"', ':')
 323  	b = b | helpers.JsonString(helpers.Base64Encode(salt))
 324  	b = append(b, ',', '"', 'v', 'a', 'u', 'l', 't', 'H', 'a', 's', 'h', '"', ':')
 325  	b = b | helpers.JsonString(hash)
 326  	b = append(b, ',', '"', 'i', 'd', 'e', 'n', 't', 'i', 't', 'i', 'e', 's', '"', ':', '[')
 327  	for i, id := range identities {
 328  		if i > 0 {
 329  			b = append(b, ',')
 330  		}
 331  		encSk, ok := encryptFieldWith(id.Seckey, key)
 332  		if !ok {
 333  			encSk = ""
 334  		}
 335  		b = append(b, '{', '"', 'p', 'u', 'b', 'k', 'e', 'y', '"', ':')
 336  		b = b | helpers.JsonString(id.Pubkey)
 337  		b = append(b, ',', '"', 's', 'e', 'c', 'k', 'e', 'y', '"', ':')
 338  		b = b | helpers.JsonString(encSk)
 339  		b = append(b, ',', '"', 'n', 'a', 'm', 'e', '"', ':')
 340  		b = b | helpers.JsonString(id.Name)
 341  		b = append(b, '}')
 342  	}
 343  	b = append(b, ']', '}')
 344  	return jsonResult(string(b))
 345  }
 346  
 347  func mgmtImportVault(paramsJSON string) (s string) {
 348  	kData := string(append([]byte(nil), 'd', 'a', 't', 'a'))
 349  	data := helpers.JsonGetString(paramsJSON, kData)
 350  	if data == "" {
 351  		return jsonErr(string(append([]byte(nil), 'm', 'i', 's', 's', 'i', 'n', 'g', ' ', 'd', 'a', 't', 'a')))
 352  	}
 353  	kVersion := string(append([]byte(nil), 'v', 'e', 'r', 's', 'i', 'o', 'n'))
 354  	kVaultHash := string(append([]byte(nil), 'v', 'a', 'u', 'l', 't', 'H', 'a', 's', 'h'))
 355  	ver := helpers.JsonGetValue(data, kVersion)
 356  	vh := helpers.JsonGetString(data, kVaultHash)
 357  	if ver == "" || vh == "" {
 358  		return jsonErr(string(append([]byte(nil), 'i', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'v', 'a', 'u', 'l', 't', ' ', 'f', 'o', 'r', 'm', 'a', 't')))
 359  	}
 360  	vaultRawCache = data
 361  	vaultExists = true
 362  	vaultOpen = false
 363  	kVault := string(append([]byte(nil), 's', 'm', 'e', 's', 'h', '-', 'v', 'a', 'u', 'l', 't'))
 364  	ext.StorageSet(kVault, data)
 365  	return jsonTrue()
 366  }
 367  
 368  func mgmtResetExtension() (s string) {
 369  	ext.StorageRemove(vaultStorageKey)
 370  	ext.StorageRemove(string(append([]byte(nil), 's', 'm', 'e', 's', 'h', '-', 'p', 'e', 'r', 'm', 'i', 's', 's', 'i', 'o', 'n', 's')))
 371  	lockVault()
 372  	vaultExists = false
 373  	vaultRawCache = ""
 374  	return jsonTrue()
 375  }
 376