From d0ad0d9ff51afaf67cef16cbf31e45aa80f29d46 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:18 +0530 Subject: [PATCH 01/33] Update docs/getting-started/features.md --- docs/getting-started/features.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/features.md b/docs/getting-started/features.md index 8596938..1fbe39e 100644 --- a/docs/getting-started/features.md +++ b/docs/getting-started/features.md @@ -186,6 +186,33 @@ A Windows-only autofill daemon that works **system-wide without a browser extens --- +## :material-face-recognition: Face ID Unlock (Optional) + +Face ID provides biometric unlock using local face recognition. It is **optional** and only available when APM is built with the `faceid` build tag because it depends on native OpenCV and dlib libraries. + +```bash +# Standard build (no Face ID) +go build -o pm.exe + +# Face ID build +go build -tags faceid -o pm.exe +``` + +Once built with `faceid`: + +```bash +pm faceid enroll +pm faceid status +pm faceid test +pm faceid remove +``` + +Notes: +- Models are downloaded automatically to your user config directory under `apm/faceid/models`. +- Enrollment metadata is stored next to the vault at `faceid/enrollment.json`. + +--- + ## :material-shield-check: Health, Trust & Audit ### Vault Health Dashboard @@ -280,4 +307,4 @@ For organizations, the **Team Edition** (`pm-team`) adds: - **[Vault Management Guide](../guides/vault-management.md)** — Deep dive into day-to-day usage - **[CLI Reference](../reference/cli.md)** — Every command documented -- **[Architecture](../concepts/architecture.md)** — How it all fits together \ No newline at end of file +- **[Architecture](../concepts/architecture.md)** — How it all fits together From 827f5027d9ed4e4841a1e1afdeb173ba59ca9b7e Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:18 +0530 Subject: [PATCH 02/33] Update docs/index.md --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 07ed12c..c9e1ffd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,6 +11,7 @@ - **Multi-Cloud Sync** — Native support for Google Drive, GitHub, and Dropbox. Your vault is uploaded as an encrypted blob; providers never see plaintext. - **AI-Agent Integration** — Built-in MCP (Model Context Protocol) server lets AI assistants like Claude, Cursor, and Windsurf read and manage vault entries with permission-scoped, token-based access. - **Windows Autofill** — A local daemon that detects credential forms and injects keystrokes via hotkey — no browser extension required. +- **Face ID Unlock (Optional)** — Biometric unlock powered by local face recognition. Available when built with the `faceid` build tag. - **Plugin Ecosystem** — Manifest-based plugins with 100+ granular permissions, a marketplace, and hook-based lifecycle integration. - **Team Edition** — Multi-user credential sharing with RBAC, departments, and approval workflows. From ec64a3c8b06db69b411574c631575e5dbd433c8b Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:18 +0530 Subject: [PATCH 03/33] Update go.mod --- go.mod | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index c3d19d7..79efb2f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/aaravmaloo/apm -go 1.25 +go 1.25.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 @@ -15,23 +15,25 @@ require ( github.com/hnakamur/w32uiautomation v0.0.0-20210808143226-23a1f2281c99 github.com/modelcontextprotocol/go-sdk v1.2.0 github.com/spf13/cobra v1.10.2 - golang.org/x/crypto v0.47.0 - golang.org/x/term v0.39.0 + golang.org/x/crypto v0.48.0 + golang.org/x/term v0.40.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e github.com/fatih/color v1.18.0 github.com/fsnotify/fsnotify v1.9.0 github.com/pquerna/otp v1.5.0 - golang.org/x/oauth2 v0.34.0 - golang.org/x/sys v0.40.0 - google.golang.org/api v0.263.0 + gocv.io/x/gocv v0.43.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sys v0.41.0 + google.golang.org/api v0.271.0 ) require ( - cloud.google.com/go/auth v0.18.1 // indirect + cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -57,8 +59,8 @@ require ( github.com/google/jsonschema-go v0.3.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect - github.com/googleapis/gax-go/v2 v2.16.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect @@ -81,10 +83,10 @@ require ( go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/text v0.33.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.78.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) From eb18c01bc1f961b6f09729c635c42cf303f1166c Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:18 +0530 Subject: [PATCH 04/33] Update go.sum --- go.sum | 68 +++++++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/go.sum b/go.sum index cdbcdbf..a89e1dd 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= -cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -41,6 +41,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e h1:lqIUFzxaqyYqUn4MhzAvSAh4wIte/iLNcIEWxpT/qbc= +github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -182,12 +184,12 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= -github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= -github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -289,14 +291,16 @@ go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4Etq go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gocv.io/x/gocv v0.43.0 h1:PFNpRUcV8fgBRDbVHHN+4BDZjjPnVveo5N/+e15BTuA= +gocv.io/x/gocv v0.43.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -358,16 +362,16 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -377,8 +381,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -412,12 +416,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -425,8 +429,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -471,8 +475,8 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -495,8 +499,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.263.0 h1:UFs7qn8gInIdtk1ZA6eXRXp5JDAnS4x9VRsRVCeKdbk= -google.golang.org/api v0.263.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8= +google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= +google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -532,12 +536,12 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= -google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -550,8 +554,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From eed09ee2f8a6a95c7a0a1544166dd6b853c6676e Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:19 +0530 Subject: [PATCH 05/33] Update main.go --- main.go | 69 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/main.go b/main.go index 236cd16..c49ddf8 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ import ( src "github.com/aaravmaloo/apm/src" "github.com/aaravmaloo/apm/src/autofill" "github.com/aaravmaloo/apm/src/autofillcmd" + "github.com/aaravmaloo/apm/src/faceid" "github.com/aaravmaloo/apm/src/plugins" "github.com/aaravmaloo/apm/src/tui" @@ -2326,8 +2327,8 @@ func main() { } defer term.Restore(int(os.Stdin.Fd()), oldState) - fmt.Print("\x1b[?25l") // Hide cursor - defer fmt.Print("\x1b[?25h") // Show cursor + fmt.Print("\x1b[?25l") + defer fmt.Print("\x1b[?25h") perms := append([]string{}, plugin.Definition.Permissions...) sort.Strings(perms) @@ -3305,7 +3306,6 @@ func main() { counts[ns]++ } - // Ensure default is always in the list for display purposes spacesToList := vault.Spaces hasDefault := false for _, s := range spacesToList { @@ -3315,7 +3315,7 @@ func main() { } } if !hasDefault { - // Prepend default + spacesToList = append([]string{"default"}, spacesToList...) } @@ -3395,7 +3395,13 @@ func main() { }, } - rootCmd.AddCommand(addCmd, getCmd, genCmd, modeCmd, sessionCmd, cinfoCmd, auditCmd, trustCmd, totpCmd, importCmd, exportCmd, infoCmd, cloudCmd, healthCmd, policyCmd, spaceCmd, pluginsCmd, setupCmd, unlockCmd, lockCmd, profileCmd, compromiseCmd, authCmd, vocabCmd, loadedCmd) + faceidCmd := faceid.BuildFaceIDCmd(vaultPath, readPassword, func() string { + if cfgDir, err := os.UserConfigDir(); err == nil { + return filepath.Join(cfgDir, "apm", "faceid", "models") + } + return filepath.Join(filepath.Dir(vaultPath), "faceid", "models") + }) + rootCmd.AddCommand(addCmd, getCmd, genCmd, modeCmd, sessionCmd, cinfoCmd, auditCmd, trustCmd, totpCmd, importCmd, exportCmd, infoCmd, cloudCmd, healthCmd, policyCmd, spaceCmd, pluginsCmd, setupCmd, unlockCmd, lockCmd, profileCmd, compromiseCmd, authCmd, vocabCmd, loadedCmd, faceidCmd) autofillCmd, _ := autofillcmd.NewAutofillAndVaultCommands(autofillcmd.Options{ VaultPath: &vaultPath, ReadPassword: readPassword, @@ -3617,7 +3623,6 @@ func main() { } rootCmd.AddCommand(tuiCmd) - // Register plugin commands as top-level commands (e.g. "pm hello"). registerDynamicPluginCommands := func() { existingNames := make(map[string]struct{}) for _, registered := range rootCmd.Commands() { @@ -4190,9 +4195,9 @@ func captureNoteContent(vault *src.Vault, title, initial string) (string, error) if n == 1 { ch := key[0] switch ch { - case 19: // Ctrl+S + case 19: return string(buffer), nil - case 27: // Esc + case 27: return "", fmt.Errorf("note edit cancelled") case 127, 8: deleteBeforeCursor() @@ -4248,9 +4253,9 @@ func captureNoteContent(vault *src.Vault, title, initial string) (string, error) cursor-- } case strings.Contains(seq, "\x1b[A"): - // Cursor up reserved for future line navigation. + case strings.Contains(seq, "\x1b[B"): - // Cursor down reserved for future line navigation. + } } } @@ -4487,12 +4492,45 @@ func src_unlockVault() (string, *src.Vault, bool, error) { return pass, src.GetDecoyVault(), true, nil } - fmt.Printf("Master Password (attempt %d/3): ", i+1) - pass, err := readPassword() - if err != nil { - return "", nil, false, err + var pass string + var err error + + vaultDir := filepath.Dir(vaultPath) + enrollment, _ := faceid.LoadEnrollment(vaultDir) + + if enrollment != nil { + fmt.Printf("Master Password (attempt %d/3): \n", i+1) + modelsDir := filepath.Join(vaultDir, "faceid", "models") + + profile, _, err := src.GetVaultParams(data) + profileName := "standard" + if err == nil { + profileName = profile.Name + } + + method, p, err := faceid.RaceUnlock(enrollment, modelsDir, profileName) + if err == nil && p != "" { + pass = p + if method == "face" { + fmt.Println(" Unlocked via Face ID") + } + } else { + + fmt.Printf(" Fallback to password: ") + pass, err = readPassword() + if err != nil { + return "", nil, false, err + } + fmt.Println() + } + } else { + fmt.Printf("Master Password (attempt %d/3): ", i+1) + pass, err = readPassword() + if err != nil { + return "", nil, false, err + } + fmt.Println() } - fmt.Println() vault, err := src.DecryptVault(data, pass, costMultiplier) if err == nil { @@ -4518,7 +4556,6 @@ func src_unlockVault() (string, *src.Vault, bool, error) { src.LogAccess("FAIL") src.TrackFailure() - // Attempt to get recovery info to find alert email if vault is locked if rec, err := src.GetVaultRecoveryInfo(data); err == nil && rec.AlertsEnabled && rec.AlertEmail != "" { tempVault := &src.Vault{ AlertEmail: rec.AlertEmail, From 3549b3009dd3923dd4b809720d94d6b5ea53ddc8 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:19 +0530 Subject: [PATCH 06/33] Update mkdocs.yml --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 8331479..44ddcb4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -77,6 +77,7 @@ plugins: nav: - Home: index.md + - Contributing: contributing.md - Getting Started: - getting-started/index.md - Installation: getting-started/installation.md @@ -89,6 +90,7 @@ nav: - Cloud synchronization: guides/cloud-sync.md - Using .apmignore: guides/apmignore.md - Autofill on Windows: autofill_windows.md + - Face ID: guides/faceid.md - Generating TOTP codes: guides/totp.md - Managing sessions: guides/sessions.md - Using plugins: guides/plugins.md From 148c7312a4bc904641eb40e051c60ca1d324c621 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:19 +0530 Subject: [PATCH 07/33] Update obfuscation/harden_obf.go --- obfuscation/harden_obf.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/obfuscation/harden_obf.go b/obfuscation/harden_obf.go index 1d54a91..d026fb1 100644 --- a/obfuscation/harden_obf.go +++ b/obfuscation/harden_obf.go @@ -10,10 +10,7 @@ var key = []byte("AaravMalooAPMSecureCloudSync2025!") // 33 bytes func harden(data []byte) []byte { res := make([]byte, len(data)) for i, v := range data { - // 1. XOR with index - // 2. XOR with key - // 3. Rotate Left 3 - // 4. Add 13 + val := (v ^ byte(i) ^ key[i%len(key)]) res[i] = bits.RotateLeft8(val, 3) + 13 } @@ -29,12 +26,11 @@ func decodeOld(data []byte) []byte { } func main() { - // src/cloud.go values + creds := decodeOld([]byte{209, 136, 195, 196, 217, 222, 203, 198, 198, 207, 206, 136, 144, 209, 136, 201, 198, 195, 207, 196, 222, 245, 195, 206, 136, 144, 136, 159, 146, 158, 158, 153, 147, 146, 155, 152, 152, 155, 156, 135, 216, 159, 156, 199, 222, 205, 193, 218, 220, 198, 217, 206, 194, 154, 152, 197, 201, 195, 222, 146, 153, 220, 207, 157, 156, 199, 206, 206, 204, 220, 223, 158, 132, 203, 218, 218, 217, 132, 205, 197, 197, 205, 198, 207, 223, 217, 207, 216, 201, 197, 196, 222, 207, 196, 222, 132, 201, 197, 199, 136, 134, 136, 201, 198, 195, 207, 196, 222, 245, 217, 207, 201, 216, 207, 222, 136, 144, 136, 237, 229, 233, 249, 250, 242, 135, 252, 154, 196, 158, 230, 227, 253, 147, 239, 196, 147, 153, 240, 205, 230, 154, 227, 218, 254, 195, 250, 201, 242, 211, 195, 159, 236, 217, 136, 134, 136, 216, 207, 206, 195, 216, 207, 201, 222, 245, 223, 216, 195, 217, 136, 144, 241, 136, 194, 222, 222, 218, 144, 133, 133, 198, 197, 201, 203, 198, 194, 197, 217, 222, 136, 247, 134, 136, 203, 223, 222, 194, 245, 223, 216, 195, 136, 144, 136, 194, 222, 222, 218, 217, 144, 133, 133, 203, 201, 201, 197, 223, 196, 222, 217, 132, 205, 197, 197, 205, 198, 207, 132, 201, 197, 199, 133, 197, 133, 197, 203, 223, 222, 194, 152, 133, 203, 223, 222, 194, 136, 134, 136, 222, 197, 193, 207, 196, 245, 223, 216, 195, 136, 144, 136, 194, 222, 222, 218, 217, 144, 133, 133, 197, 203, 223, 222, 194, 152, 132, 205, 197, 197, 205, 198, 207, 203, 218, 195, 217, 132, 201, 197, 199, 133, 222, 197, 193, 207, 196, 136, 215, 215}) tokens := decodeOld([]byte{209, 136, 216, 207, 204, 216, 207, 217, 194, 245, 222, 197, 193, 207, 196, 136, 144, 136, 155, 133, 133, 154, 205, 197, 231, 227, 236, 135, 200, 218, 158, 227, 226, 203, 233, 205, 243, 227, 235, 248, 235, 235, 237, 232, 235, 249, 228, 221, 236, 135, 230, 147, 227, 216, 239, 155, 230, 198, 242, 226, 220, 218, 228, 158, 200, 193, 235, 146, 203, 155, 157, 254, 135, 228, 193, 230, 221, 252, 210, 152, 233, 155, 156, 153, 219, 231, 153, 203, 154, 250, 229, 223, 228, 254, 225, 224, 222, 197, 211, 226, 228, 194, 248, 252, 193, 240, 154, 233, 230, 251, 155, 253, 227, 226, 135, 254, 210, 224, 201, 208, 217, 136, 134, 136, 222, 193, 207, 196, 245, 222, 211, 218, 207, 136, 144, 136, 232, 207, 203, 216, 207, 216, 136, 215}) dbToken := decodeOld([]byte{217, 198, 132, 223, 132, 235, 237, 248, 195, 207, 210, 233, 204, 197, 250, 197, 196, 220, 232, 206, 218, 222, 159, 220, 211, 236, 194, 251, 157, 156, 228, 239, 147, 255, 226, 228, 217, 195, 205, 211, 135, 222, 208, 152, 199, 198, 245, 201, 232, 204, 253, 221, 208, 152, 199, 226, 200, 221, 240, 192, 250, 219, 243, 220, 152, 135, 228, 223, 135, 229, 250, 238, 200, 250, 205, 198, 233, 222, 254, 250, 237, 206, 156, 243, 235, 199, 249, 252, 221, 217, 254, 205, 224, 194, 231, 159, 240, 253, 157, 199, 192, 232, 194, 211, 155, 201, 158, 223, 198, 242, 220, 250, 238, 217, 250, 193, 254, 157, 217, 153, 192, 197, 237, 154, 157, 239, 242, 238, 198, 198, 198, 229, 207, 216, 219, 208, 210, 218, 206, 248, 232, 155, 235, 250, 196, 249, 204, 242, 201, 200, 159, 195, 201, 245, 251, 206, 242, 248, 159, 249, 248, 208, 217, 228, 154, 195, 230, 243, 248, 239, 225, 192, 245, 239, 205, 251, 223, 217, 216, 147, 232, 153, 252, 135, 210, 250, 225, 235, 232, 147, 253, 251, 208, 252, 211, 223, 208, 245, 147, 253, 245, 238, 232, 206, 204, 200, 249, 226, 233, 195, 206, 199, 199, 226, 238, 221, 135, 146, 154, 200, 225, 193, 195, 204, 197, 219, 218, 222, 159, 211, 199, 155, 152, 227, 222, 193, 197, 217, 235, 219, 220, 233, 233, 155, 243, 158, 197, 146, 227, 226, 207, 210, 227, 230, 153, 198, 156, 220, 219, 225, 229, 199, 218, 217, 157, 201, 195, 222, 154, 211, 228, 223, 208, 240, 232, 242, 192, 198, 229, 242, 245, 220, 227, 233, 152, 156, 232, 205, 216, 251, 252, 154, 218, 195, 152, 152, 197, 250, 249, 220, 217, 231, 231, 253, 230, 192, 227, 225, 222, 204, 154, 205, 153, 254, 243, 193, 207, 253, 225, 205, 253, 231, 225, 201, 218, 255, 210, 226, 238, 201, 204, 196, 200, 222, 254, 198, 218, 251, 226, 147, 238, 232, 203, 211, 222, 192, 203, 222, 236, 230, 204, 216, 236, 231, 225, 250, 248, 219, 211, 232, 237, 220, 210, 238, 199, 231, 193, 225, 203, 250, 230, 158, 203, 203, 255, 196, 152, 156, 219, 225, 203, 217, 220, 211, 159, 200, 237, 193, 226, 242, 207, 233, 204, 204, 249, 198, 198, 252, 243, 201, 196, 147, 135, 229, 228, 239, 207, 193, 146, 195, 152, 152, 218, 216, 203, 199, 238, 235, 252, 251, 207, 227, 135, 196, 157, 135, 239, 159, 235, 251, 205, 229, 224, 196, 204, 228, 156, 155, 232, 135, 152, 236, 205, 205, 255, 232, 197, 225, 235, 230, 198, 156, 219, 219, 252, 229, 223, 135, 222, 208, 238, 249, 255, 236, 207, 158, 208, 228, 146, 147, 210, 249, 157, 237, 193, 204, 231, 208, 204, 237, 229, 240, 231, 245, 221, 146, 211, 227, 152, 225, 197, 255, 216, 232, 220, 210, 245, 206, 237, 255, 242, 226, 155, 255, 211, 228, 242, 195, 250, 231, 242, 193, 159, 152, 207, 211, 210, 254, 203, 220, 207, 217, 197, 218, 221, 238, 154, 157, 229, 158, 219, 248, 155, 153, 252, 248, 250, 255, 194, 193, 252, 252, 153, 206, 198, 222, 206, 205, 153, 240, 251, 210, 152, 206, 196, 206, 229, 210, 135, 210, 255, 205, 198, 243, 207, 200, 251, 235, 198, 158, 197, 224, 192, 238, 254, 200, 200, 216, 203, 224, 243, 235, 226, 243, 146, 229, 240, 236, 225, 154, 219, 193, 147, 237, 224, 228, 218, 223, 249, 245, 197, 218, 235, 255, 216, 147, 219, 203, 238, 245, 255, 225, 200, 199, 251, 147, 223, 147, 235, 157, 243, 225, 251, 221, 255, 200, 254, 220, 250, 158, 156, 233, 159, 154, 220, 224, 251, 255, 253, 230, 238, 157, 195, 194, 253, 135, 239, 251, 196, 152, 255, 232, 228, 220, 152, 239, 254, 152, 207, 240, 254, 135, 206, 255, 147, 251, 248, 205, 199, 193, 157, 211, 147, 225, 218, 146, 251, 194, 249, 220, 232, 243, 251, 205, 233, 230, 222, 157, 227, 243, 240, 218, 254, 235, 226, 235, 211, 147, 226, 196, 157, 249, 206, 159, 235, 245, 219, 192, 197, 232, 216, 226, 216, 207, 255, 152, 194, 207, 152, 199, 229, 199, 157, 233, 231, 197, 204, 159, 255, 197, 233, 222, 210, 135, 232, 221, 235, 156, 210, 158, 254, 222, 228, 197, 225, 135, 154, 232, 255, 159, 147, 228, 192, 229, 196, 199, 200, 250, 252, 235, 232, 227, 211, 218, 135, 210, 216, 203, 203, 216, 147, 200, 248, 220, 230, 207, 245, 221, 159, 199, 135, 152, 248, 233, 159, 221, 231, 230, 253, 236, 255, 228, 237, 201, 242, 194, 227, 250, 255, 154, 206, 236, 147, 196, 240, 242, 238, 210, 219, 231, 203, 219, 208, 233, 253, 204, 216, 203, 228, 251, 236, 253, 203, 196, 232, 243, 203, 250, 229, 232, 235, 230, 230, 198, 240, 194, 193, 219, 240, 147, 205, 159, 158, 203, 153, 245, 147, 219, 192, 248, 237, 232, 135, 152, 192, 135, 249, 222, 236, 153, 211, 238, 203, 216, 200, 200, 154, 204, 152, 216, 154, 157, 224, 197, 195, 243, 239, 220, 224, 229, 194, 254, 219, 208, 210, 218, 206, 221, 237, 238, 223, 194, 238, 228, 253, 158, 152, 239, 248, 228, 159, 242, 239, 251, 198, 231, 199, 158, 203, 153, 245, 245, 239, 206, 156, 203, 153, 199, 159, 206, 201, 243, 211, 217, 156, 198, 146, 240, 153, 221, 147, 154, 222, 206, 146, 155, 232, 194, 227, 240, 220, 252, 250, 207, 238, 248, 240, 211, 239, 218, 230, 222, 232, 252, 227, 243, 200, 224, 218, 229, 203, 245, 253, 195, 207, 226, 220, 218, 240, 254, 225, 232, 255, 195, 207, 152, 195, 253, 232, 135, 251, 206, 135, 147, 211, 201, 207, 198, 195, 231, 242, 156, 192, 227, 228, 238, 226, 238, 220, 159, 135, 233, 193, 197, 228, 221, 224, 155, 243, 255, 210, 158, 230, 207, 254, 235, 205, 227, 225, 218, 249, 255, 211, 197, 233, 225, 135, 205, 226, 221, 206, 210, 226, 220, 225, 237, 147, 249, 219, 240, 230, 255, 147, 237, 240, 155, 147, 207, 253, 154, 235, 158, 210, 218, 235, 242, 147, 211, 198, 207, 248, 240, 203, 158, 227, 217, 152, 248, 146, 238, 146, 233, 193, 233, 203, 219, 196, 135, 216, 243, 237, 201, 201, 154, 192, 217, 250, 228, 233, 208, 159, 223, 158, 192, 254, 157, 233, 192, 233, 227, 208, 229, 197, 231, 251, 248, 231, 207, 236, 245, 228, 199, 231, 221, 254, 222, 217, 196, 232, 228, 198, 242, 156, 250, 199, 225, 245, 211, 230, 229, 155, 248, 221, 228, 219, 198, 205, 158, 152, 201, 198, 224, 199, 239, 253, 147, 216, 199, 233, 153, 196, 233, 147, 253, 239, 147, 233, 225, 205, 210, 251, 232, 196, 157, 205, 146, 232, 240, 210, 157, 248, 245, 237, 227, 198, 238, 233, 242, 248, 217, 227, 223, 153, 226, 199, 147, 255, 156, 230, 135, 225, 222, 152, 240, 216, 216, 155, 157, 153, 204, 203, 152, 248, 155, 199, 203, 147, 228, 227, 154, 207, 159, 154, 235, 197, 248, 237, 220, 250, 253, 147, 210, 198, 153, 224, 206, 216, 232, 233, 146, 229, 152, 193, 242, 154, 155, 243, 249, 194, 229, 201, 205, 222, 211, 203, 253, 239, 254, 218, 197, 235, 154, 135, 240, 224, 226, 224, 229, 147, 154, 230, 243, 239, 220, 200, 233, 220, 157, 223, 200, 157, 237, 157, 232, 232, 231, 243, 227, 207, 249, 210, 227, 237, 217, 218, 245, 200, 199, 232, 248, 146, 155, 222, 195, 242, 197, 221, 249, 233, 194, 228, 204, 229, 248, 206, 197, 236, 248, 253, 227, 251, 192, 230, 216, 249, 204, 218, 216, 219, 227, 208, 135, 222, 245, 219, 217, 227, 232, 248, 206, 252, 253, 226, 152, 242, 208, 229, 242, 205, 192, 208, 250, 240, 240, 194, 253, 135, 231, 233, 228, 135, 237, 204, 248, 158, 158, 225, 195, 197, 240, 204, 237, 224, 237, 147, 230, 197, 231, 205, 154, 211, 250, 233, 207, 248, 211, 235, 203, 159, 240, 203, 192, 236, 194, 248, 159, 222, 201, 207, 204, 157, 155, 204, 146, 135, 158, 223, 254, 239, 216, 211, 224, 249, 226, 218, 233, 222, 225, 236, 207, 206, 223, 231, 238, 251, 227, 221, 156, 135, 230, 251, 152, 220, 229, 226, 155, 152, 220, 252, 216, 197, 251, 205, 154, 230, 153, 147, 193, 203, 230, 251, 217, 210, 152, 216, 152, 240, 153, 210, 147, 235, 222, 155, 243, 201, 207, 224, 210, 254, 198, 198, 221, 239, 159, 193, 158, 227, 230, 194, 197, 156, 248, 243, 238, 200, 198, 199, 153, 224, 205}) - // main.go values host := decodeOld([]byte{0xd9, 0xc7, 0xde, 0xda, 0x84, 0xcd, 0xc7, 0xcb, 0xc3, 0xc6, 0x84, 0xc9, 0xc5, 0xc7}) user := decodeOld([]byte{0xcb, 0xcb, 0xd8, 0xcb, 0xdc, 0xc7, 0xcb, 0xc6, 0xc5, 0xc5, 0x9a, 0x9c, 0xea, 0xcd, 0xc7, 0xcb, 0xc3, 0xc6, 0x84, 0xc9, 0xc5, 0xc7}) pass := decodeOld([]byte{0xc3, 0xd8, 0xd3, 0xd8, 0x8a, 0xc5, 0xc7, 0xc6, 0xdc, 0x8a, 0xc2, 0xde, 0xdb, 0xcc, 0x8a, 0xda, 0xd8, 0xc2, 0xdb}) From c86ceda0e15f91a0cceab6a6d8665f048d1e9e7b Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:19 +0530 Subject: [PATCH 08/33] Update src/apmignore.go --- src/apmignore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apmignore.go b/src/apmignore.go index e994924..7a851b6 100644 --- a/src/apmignore.go +++ b/src/apmignore.go @@ -115,7 +115,7 @@ func ParseIgnoreConfig(content string) (IgnoreConfig, error) { } cfg.Misc[strings.ToLower(key)] = value default: - // Unknown section is ignored for forward compatibility. + } } if err := scanner.Err(); err != nil { From fceba847778e3822ff71c70a97c8097f57800255 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:19 +0530 Subject: [PATCH 09/33] Update src/auth_logic.go --- src/auth_logic.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/auth_logic.go b/src/auth_logic.go index 6cf79c2..f80aa6c 100644 --- a/src/auth_logic.go +++ b/src/auth_logic.go @@ -21,7 +21,6 @@ func AttemptUnlockWithSession(vaultPath string) (*UnlockResult, error) { return nil, fmt.Errorf("vault not found") } - // 1. Check Ephemeral Session if ephID := strings.TrimSpace(os.Getenv("APM_EPHEMERAL_ID")); ephID != "" { eph, err := ValidateEphemeralSession(ephID, os.Getpid(), strings.TrimSpace(os.Getenv("APM_EPHEMERAL_AGENT"))) if err == nil { @@ -40,7 +39,6 @@ func AttemptUnlockWithSession(vaultPath string) (*UnlockResult, error) { } } - // 2. Check Standard Session if session, err := GetSession(); err == nil { data, err := LoadVault(vaultPath) if err == nil { @@ -87,7 +85,7 @@ func UnlockWithPassword(vaultPath, password string) (*UnlockResult, error) { } if localFailures >= 6 { - // Return decoy vault if too many failures + TrackFailure() CreateSession(password, 1*time.Hour, true, 15*time.Minute) return &UnlockResult{ @@ -101,7 +99,7 @@ func UnlockWithPassword(vaultPath, password string) (*UnlockResult, error) { vault, err := DecryptVault(data, password, 1) if err != nil { TrackFailure() - // Try to send alert if possible + if rec, err := GetVaultRecoveryInfo(data); err == nil && rec.AlertsEnabled && rec.AlertEmail != "" { tempVault := &Vault{ AlertEmail: rec.AlertEmail, @@ -113,7 +111,6 @@ func UnlockWithPassword(vaultPath, password string) (*UnlockResult, error) { return nil, err } - // Success ClearFailures() vault.FailedAttempts = 0 vault.EmergencyMode = false From 70199a85d87d04a39d47c59cf1c445764fba706a Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:19 +0530 Subject: [PATCH 10/33] Update src/autofill/daemon.go --- src/autofill/daemon.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/autofill/daemon.go b/src/autofill/daemon.go index 2cbc748..9551118 100644 --- a/src/autofill/daemon.go +++ b/src/autofill/daemon.go @@ -116,7 +116,7 @@ func Run(opts RunOptions) error { } if err := daemon.systemEngine.Start(daemon.hotkey, daemon.handleHotkey); err != nil { - // Keep daemon operational even if global hotkey setup fails. + daemon.systemEngine = newSystemEngine() } @@ -400,8 +400,6 @@ func (d *Daemon) handleHotkey(ctx WindowContext) { return } - // Retry once with a fresh window snapshot. Some web OTP fields update focus - // asynchronously, so a second capture improves first-try reliability. freshCtx, err := captureActiveWindowContext() if err != nil { return @@ -418,8 +416,7 @@ func (d *Daemon) tryHandleHotkeyContext(requestContext RequestContext) bool { resp, statusCode := d.resolveFill(req) if statusCode == http.StatusOK && resp.Status == ResponseStatusMultiple && len(resp.Candidates) > 0 { - // Prefer the top ranked candidate for hotkey flows to avoid requiring - // manual re-trigger when several close matches exist. + req.SelectionID = resp.Candidates[0].ProfileID resp, statusCode = d.resolveFill(req) } @@ -574,7 +571,6 @@ func contextSuggestsCredentialEntry(ctx RequestContext) bool { focusedName := strings.ToLower(strings.TrimSpace(ctx.FocusedName)) signals := strings.TrimSpace(windowTitle + " " + focusedName) - // Suppress hints in common non-auth surfaces even if an account match exists. if containsAny(signals, "inbox", "mailbox", "compose", "message list", "thread list", "chat", "channel", "timeline", "feed") && !containsAny(signals, "login", "log in", "sign in", "signin", "password", "otp", "2fa", "verification", "auth", "security code", "authenticator") { return false From 5e55babfc60e99ea6d186d0c49fc4b892420743c Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:20 +0530 Subject: [PATCH 11/33] Update src/autofill/matching.go --- src/autofill/matching.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/autofill/matching.go b/src/autofill/matching.go index 13c8c55..27817ba 100644 --- a/src/autofill/matching.go +++ b/src/autofill/matching.go @@ -130,8 +130,6 @@ func scoreSystemProfile(profile Profile, ctx RequestContext) (int, bool) { score += 150 } - // If a profile has no explicit system match hints, keep it out of system - // autofill to avoid accidental cross-app typing. if len(profile.ProcessNames) == 0 && len(profile.WindowContains) == 0 { return 0, false } From 1547248af6a844172216ee30e5c3adea78481849 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:20 +0530 Subject: [PATCH 12/33] Update src/autofill/system_engine_windows.go --- src/autofill/system_engine_windows.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/autofill/system_engine_windows.go b/src/autofill/system_engine_windows.go index b3033cc..53e73b2 100644 --- a/src/autofill/system_engine_windows.go +++ b/src/autofill/system_engine_windows.go @@ -136,8 +136,7 @@ func (w *windowsSystemEngine) pollHotkey() { } func (w *windowsSystemEngine) dispatchHotkey() { - // Wait for the hotkey combo to be released before handling callback so - // autofill typing doesn't run while Ctrl/Shift are still physically held. + deadline := time.Now().Add(500 * time.Millisecond) for time.Now().Before(deadline) { select { From 311a70fa206ffc23e7306ec5eafa28ed4d2bdfbd Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:20 +0530 Subject: [PATCH 13/33] Update src/autofill/system_intelligent.go --- src/autofill/system_intelligent.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/autofill/system_intelligent.go b/src/autofill/system_intelligent.go index 7d068f9..ae80b8a 100644 --- a/src/autofill/system_intelligent.go +++ b/src/autofill/system_intelligent.go @@ -152,8 +152,7 @@ func buildIntelligentCandidates(vault *src.Vault, ctx RequestContext) []intellig continue } if typedEmail != "" { - // In typed-email mode we only autofill credentials tied to that exact - // username and skip standalone TOTP-only guesses. + continue } From 3b380a51231b18cbcd3f5abe3ed62367bb7c966d Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:20 +0530 Subject: [PATCH 14/33] Update src/autofill/uia_windows.go --- src/autofill/uia_windows.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/autofill/uia_windows.go b/src/autofill/uia_windows.go index 6e9981e..ee0e7d1 100644 --- a/src/autofill/uia_windows.go +++ b/src/autofill/uia_windows.go @@ -1,5 +1,3 @@ -//go:build windows - package autofill import ( From af79bc2ea6c9c958e5f38d5c9fb9022bd7276330 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:20 +0530 Subject: [PATCH 15/33] Update src/plugins/engine.go --- src/plugins/engine.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/plugins/engine.go b/src/plugins/engine.go index 121bd39..773c03a 100644 --- a/src/plugins/engine.go +++ b/src/plugins/engine.go @@ -95,7 +95,7 @@ func (se *StepExecutor) ExecuteStep(step CommandStep, permissions []string) erro if val == "" { for _, t := range se.Vault.TOTPEntries { if t.Account == key { - val = t.Secret // In a real app we might want to generate the code here + val = t.Secret break } } @@ -284,8 +284,7 @@ func (se *StepExecutor) ExecuteStep(step CommandStep, permissions []string) erro return fmt.Errorf("permission denied: cloud.sync") } fmt.Println("Triggering cloud sync...") - // In a real app we would call the sync logic here. - // For now we just simulate. + return nil case "v:lock": @@ -298,7 +297,7 @@ func (se *StepExecutor) ExecuteStep(step CommandStep, permissions []string) erro } format := se.getArg(step.Args, 0) path := se.getArg(step.Args, 1) - // Dummy implementation for export + fmt.Printf("Exporting vault to %s in %s format...\n", path, format) return nil From 125682f88fda0580eeaac85622f975d84f8fcce4 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:20 +0530 Subject: [PATCH 16/33] Update src/plugins/manifest.go --- src/plugins/manifest.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/plugins/manifest.go b/src/plugins/manifest.go index 9d23ac8..287fc42 100644 --- a/src/plugins/manifest.go +++ b/src/plugins/manifest.go @@ -29,14 +29,13 @@ func (m *Manifest) Validate() error { } allowedPermissions := map[string]bool{ - // Base + "vault.read": true, "vault.write": true, "file.storage": true, "crypto.use": true, "network.outbound": true, - // Vault "vault.delete": true, "vault.import": true, "vault.export": true, @@ -47,7 +46,6 @@ func (m *Manifest) Validate() error { "vault.unlock": true, "vault.sync": true, - // Vault Items "vault.item.create": true, "vault.item.read": true, "vault.item.update": true, @@ -56,7 +54,6 @@ func (m *Manifest) Validate() error { "vault.item.copy": true, "vault.item.share": true, - // Vault Fields "vault.item.field.password.read": true, "vault.item.field.password.write": true, "vault.item.field.username.read": true, @@ -74,7 +71,6 @@ func (m *Manifest) Validate() error { "vault.item.field.custom.read": true, "vault.item.field.custom.write": true, - // Network "network.http": true, "network.https": true, "network.ftp": true, @@ -91,7 +87,6 @@ func (m *Manifest) Validate() error { "network.api.rest": true, "network.api.grpc": true, - // System "system.read": true, "system.write": true, "system.exec": true, @@ -113,7 +108,6 @@ func (m *Manifest) Validate() error { "system.bluetooth": true, "system.wifi": true, - // Crypto "crypto.hash": true, "crypto.random": true, "crypto.encrypt": true, @@ -127,7 +121,6 @@ func (m *Manifest) Validate() error { "crypto.cert.generate": true, "crypto.cert.validate": true, - // Plugin Management "plugin.list": true, "plugin.install": true, "plugin.uninstall": true, @@ -136,7 +129,6 @@ func (m *Manifest) Validate() error { "plugin.config.write": true, "plugin.reload": true, - // UI "ui.prompt": true, "ui.alert": true, "ui.confirm": true, @@ -151,7 +143,6 @@ func (m *Manifest) Validate() error { "ui.theme.set": true, "ui.font.set": true, - // User "user.read": true, "user.write": true, "user.auth": true, @@ -161,7 +152,6 @@ func (m *Manifest) Validate() error { "user.profile.write": true, "user.biometric": true, - // Audit "audit.read": true, "audit.write": true, "audit.log.read": true, @@ -169,24 +159,20 @@ func (m *Manifest) Validate() error { "audit.alert.read": true, "audit.report": true, - // Database (Internal) "db.read": true, "db.write": true, "db.query": true, "db.schema.read": true, "db.schema.write": true, - // AI / ML "ai.model.load": true, "ai.predict": true, "ai.train": true, - // IoT / Hardware "iot.scan": true, "iot.connect": true, "iot.control": true, - // Cloud "cloud.sync": true, "cloud.backup": true, "cloud.restore": true, @@ -198,7 +184,7 @@ func (m *Manifest) Validate() error { if allowedPermissions[p] { continue } - // Allow wildcards (e.g. "vault.*") + if len(p) > 2 && p[len(p)-2:] == ".*" { continue } From 273c3ae386bf1b8bf069e981686d8b190371b640 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:20 +0530 Subject: [PATCH 17/33] Update src/security.go --- src/security.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/security.go b/src/security.go index 50d0328..6b766d8 100644 --- a/src/security.go +++ b/src/security.go @@ -61,7 +61,6 @@ func SendAlert(vault *Vault, requiredLevel int, eventType, details string) { return } - // Default to level 1 for safety if not set vLevel := vault.SecurityLevel if vLevel < 1 { vLevel = 1 From e50b6e52234395734860168e315a819f6addf7bf Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:21 +0530 Subject: [PATCH 18/33] Update src/tui/details.go --- src/tui/details.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/details.go b/src/tui/details.go index b830006..0a3cb95 100644 --- a/src/tui/details.go +++ b/src/tui/details.go @@ -23,7 +23,7 @@ func RenderDetails(res src.SearchResult) string { case "TOTP": e := res.Data.(src.TOTPEntry) b.WriteString(fmt.Sprintf("Account: %s\n", e.Account)) - // We could generate code here if we want real-time updates + case "Token": e := res.Data.(src.TokenEntry) b.WriteString(fmt.Sprintf("Name: %s\n", e.Name)) From 6b24ea5bd1156421448c3893254170fb01a414c4 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:21 +0530 Subject: [PATCH 19/33] Update src/tui/health.go --- src/tui/health.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/health.go b/src/tui/health.go index 7f6f7eb..2dfed2e 100644 --- a/src/tui/health.go +++ b/src/tui/health.go @@ -36,7 +36,7 @@ func RenderAudit(vault *src.Vault) string { if len(vault.History) == 0 { s += "No audit logs found.\n" } else { - // Show last 15 logs + start := len(vault.History) - 15 if start < 0 { start = 0 From ea9de9ebe3fa507f741c3b90091b9f13a5b0bd7a Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:21 +0530 Subject: [PATCH 20/33] Update src/vault.go --- src/vault.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vault.go b/src/vault.go index c70496a..24dfe61 100644 --- a/src/vault.go +++ b/src/vault.go @@ -350,7 +350,7 @@ type Vault struct { Spaces []string `json:"spaces"` ActivePolicy Policy `json:"active_policy,omitempty"` PluginPermissionOverrides map[string]map[string]bool `json:"plugin_permission_overrides,omitempty"` - NeedsRepair bool `json:"-"` // Internal flag for silent self-healing + NeedsRepair bool `json:"-"` CurrentProfileParams *CryptoProfile `json:"-"` RecoveryEmail string `json:"recovery_email,omitempty"` @@ -576,9 +576,7 @@ func decryptNewVault(data []byte, masterPassword string, costMultiplier int) (*V if offset+rLen <= len(data) { var rec RecoveryData if err := json.Unmarshal(data[offset:offset+rLen], &rec); err == nil { - // Pre-populate some fields from header if needed - // These might be overwritten by encrypted payload later, - // but it's good for consistency and potentially pre-unlock checks + } } offset += rLen From 5c2ab4f32e90867f4b9affa39988128d35a741b1 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:21 +0530 Subject: [PATCH 21/33] Add CONTRIBUTING.md --- CONTRIBUTING.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1b7c891 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing to APM + +Thanks for contributing. APM is a security-sensitive CLI, so we prioritize small, well-tested changes. + +## Build +There is a faceid tag, for people contributing to ```faceid``` feature. It takes quite a while to setup, since it requires OpenCV and GoCV; and would certainly be a time-waster for people contributing to not faceid realted features. + +```bash +# Standard build +go build -o pm.exe + +# Face ID build (requires native OpenCV + dlib) +go build -tags faceid -o pm.exe +``` + +Face ID is behind a build tag because it depends on native OpenCV/dlib libraries that are not required for the core CLI. + +## AI-Generated Code +While contributing to APM, I would strongly advise writing code by yourself and using AI to refactor it. However, AI written code is also NOT discouraged, but it will take longer than usual to review the AI generated code. \ No newline at end of file From 38fd3f01d5045a1125bf9ccd2aab3c38ea1f4f16 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:21 +0530 Subject: [PATCH 22/33] Add docs/contributing.md --- docs/contributing.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/contributing.md diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..71e048c --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,25 @@ +# Contributing + +Thanks for contributing to APM. This page covers the fastest path to a clean build and a smooth review. + +## Build + +```bash +# Standard build +go build -o pm.exe + +# Face ID build (requires native OpenCV + dlib) +go build -tags faceid -o pm.exe +``` + +Face ID is behind a build tag because it depends on native OpenCV/dlib libraries that are not required for the core CLI. + +## Pull Requests + +- Keep changes focused and scoped. +- Include a short description of what changed and why. +- Mention any manual test steps you ran. + +## AI-Generated Code + +AI-generated changes take longer to review. If possible, write the code yourself and use AI only as a collaborator or reviewer while building APM. From 3d10b0ffcf26d59d836674b6a566f705a56b10d8 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:21 +0530 Subject: [PATCH 23/33] Add docs/guides/faceid.md --- docs/guides/faceid.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/guides/faceid.md diff --git a/docs/guides/faceid.md b/docs/guides/faceid.md new file mode 100644 index 0000000..1a86d23 --- /dev/null +++ b/docs/guides/faceid.md @@ -0,0 +1,32 @@ +# Face ID (Optional) + +Face ID enables biometric unlock using local face recognition. It is **optional** and only available when APM is built with the `faceid` build tag because it depends on native OpenCV and dlib libraries. + +## Build + +```bash +# Standard build (no Face ID) +go build -o pm.exe + +# Face ID build +go build -tags faceid -o pm.exe +``` + +## Commands + +```bash +pm faceid enroll +pm faceid status +pm faceid test +pm faceid remove +``` + +## Storage + +- **Models** are downloaded automatically to your user config directory under `apm/faceid/models`. +- **Enrollment** metadata is stored next to the vault at `faceid/enrollment.json`. + +## Notes + +- Face ID uses the default camera (index `0`). +- If Face ID fails, you can always unlock with your master password. From da9099a6322623bdcc334709725c200cd72649af Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:22 +0530 Subject: [PATCH 24/33] Add src/faceid/animation.go --- src/faceid/animation.go | 125 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/faceid/animation.go diff --git a/src/faceid/animation.go b/src/faceid/animation.go new file mode 100644 index 0000000..b1f4f04 --- /dev/null +++ b/src/faceid/animation.go @@ -0,0 +1,125 @@ +//go:build faceid + +package faceid + +import ( + "fmt" + "time" +) + +const ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" + colorDimGray = "\033[90m" + colorBoldGreen = "\033[1;32m" + colorBoldRed = "\033[1;31m" + cursorUp = "\033[%dA" + clearLine = "\033[2K" +) + +func PlaySuccessAnimation() { + frame1 := fmt.Sprintf( + "%s┌─────────────────┐%s\n"+ + "%s│ │%s\n"+ + "%s│ %s[ FACE ]%s │%s\n"+ + "%s│ │%s\n"+ + "%s└─────────────────┘%s\n", + colorDimGray, colorReset, + colorDimGray, colorReset, + colorDimGray, colorDimGray, colorDimGray, colorReset, + colorDimGray, colorReset, + colorDimGray, colorReset, + ) + fmt.Print(frame1) + time.Sleep(100 * time.Millisecond) + + clearAnimationArea(5) + frame2 := fmt.Sprintf( + "%s┌─────────────────┐%s\n"+ + "%s│ │%s\n"+ + "%s│ %s[ ···SCAN···]%s │%s\n"+ + "%s│ │%s\n"+ + "%s└─────────────────┘%s\n", + colorDimGray, colorReset, + colorDimGray, colorReset, + colorDimGray, colorYellow, colorDimGray, colorReset, + colorDimGray, colorReset, + colorDimGray, colorReset, + ) + fmt.Print(frame2) + time.Sleep(200 * time.Millisecond) + + clearAnimationArea(5) + frame3 := fmt.Sprintf( + "%s┌─────────────────┐%s\n"+ + "%s│ │%s\n"+ + "%s│ %s[ ✓ FACE ]%s │%s\n"+ + "%s│ │%s\n"+ + "%s└─────────────────┘%s\n", + colorDimGray, colorReset, + colorDimGray, colorReset, + colorDimGray, colorGreen, colorDimGray, colorReset, + colorDimGray, colorReset, + colorDimGray, colorReset, + ) + fmt.Print(frame3) + time.Sleep(200 * time.Millisecond) + + clearAnimationArea(5) + fmt.Printf( + "%s┌─────────────────┐%s\n"+ + "%s│ │%s\n"+ + "%s│ ✓ Unlocked │%s\n"+ + "%s│ │%s\n"+ + "%s└─────────────────┘%s\n"+ + " %sVault unlocked via Face ID%s\n", + colorDimGray, colorReset, + colorDimGray, colorReset, + colorBoldGreen, colorReset, + colorDimGray, colorReset, + colorDimGray, colorReset, + colorBoldGreen, colorReset, + ) +} + +func PlayScanningState() { + fmt.Printf( + "%s┌─────────────────┐%s\n"+ + "%s│ │%s\n"+ + "%s│ %s[ ···SCAN···]%s │%s\n"+ + "%s│ │%s\n"+ + "%s└─────────────────┘%s\n", + colorDimGray, colorReset, + colorDimGray, colorReset, + colorDimGray, colorYellow, colorDimGray, colorReset, + colorDimGray, colorReset, + colorDimGray, colorReset, + ) +} + +func PlayFailureAnimation() { + clearAnimationArea(5) + fmt.Printf( + "%s┌─────────────────┐%s\n"+ + "%s│ │%s\n"+ + "%s│ ✗ No Match │%s\n"+ + "%s│ │%s\n"+ + "%s└─────────────────┘%s\n", + colorDimGray, colorReset, + colorDimGray, colorReset, + colorBoldRed, colorReset, + colorDimGray, colorReset, + colorDimGray, colorReset, + ) + time.Sleep(500 * time.Millisecond) + clearAnimationArea(5) +} + +func clearAnimationArea(lines int) { + for i := 0; i < lines; i++ { + fmt.Printf(fmt.Sprintf(cursorUp, 1)) + fmt.Print(clearLine) + } +} From 317700f81d39e2677cea65f1ce71795c003f3a27 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:22 +0530 Subject: [PATCH 25/33] Add src/faceid/camera.go --- src/faceid/camera.go | 66 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/faceid/camera.go diff --git a/src/faceid/camera.go b/src/faceid/camera.go new file mode 100644 index 0000000..9c8787e --- /dev/null +++ b/src/faceid/camera.go @@ -0,0 +1,66 @@ +//go:build faceid + +package faceid + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "gocv.io/x/gocv" +) + +var ErrNoCamera = fmt.Errorf("no camera found") + +func captureFrames(n int, intervalMs int) ([]string, error) { + webcam, err := gocv.OpenVideoCapture(0) + if err != nil { + return nil, ErrNoCamera + } + defer webcam.Close() + + if !webcam.IsOpened() { + return nil, ErrNoCamera + } + + img := gocv.NewMat() + defer img.Close() + + var paths []string + tmpDir := os.TempDir() + + for i := 0; i < n; i++ { + if ok := webcam.Read(&img); !ok || img.Empty() { + if i == 0 { + cleanupFrames(paths) + return nil, ErrNoCamera + } + continue + } + + ts := time.Now().UnixMilli() + framePath := filepath.Join(tmpDir, fmt.Sprintf("apm_frame_%d_%d.jpg", ts, i)) + + if ok := gocv.IMWrite(framePath, img); !ok { + continue + } + paths = append(paths, framePath) + + if i < n-1 { + time.Sleep(time.Duration(intervalMs) * time.Millisecond) + } + } + + if len(paths) == 0 { + return nil, fmt.Errorf("failed to capture any frames from camera") + } + + return paths, nil +} + +func cleanupFrames(paths []string) { + for _, p := range paths { + os.Remove(p) + } +} From b318db6191f82e9dd149e9274456651a201f5636 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:22 +0530 Subject: [PATCH 26/33] Add src/faceid/enrollment.go --- src/faceid/enrollment.go | 172 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/faceid/enrollment.go diff --git a/src/faceid/enrollment.go b/src/faceid/enrollment.go new file mode 100644 index 0000000..a51ba6d --- /dev/null +++ b/src/faceid/enrollment.go @@ -0,0 +1,172 @@ +//go:build faceid + +package faceid + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "math" + "os" + "path/filepath" + "time" +) + +type FaceIDEnrollment struct { + Embedding []float32 `json:"embedding"` + EncryptedMasterPass []byte `json:"encrypted_master"` + EnrolledAt time.Time `json:"enrolled_at"` + DeviceName string `json:"device_name"` + ModelVersion string `json:"model_version"` +} + +func EnrollFace(masterPassword string, vaultDir string, modelsDir string, profile string) (*FaceIDEnrollment, error) { + if modelsDir == "" { + modelsDir = filepath.Join(vaultDir, "faceid", "models") + } + if err := EnsureModels(modelsDir); err != nil { + return nil, err + } + + rec, err := NewRecognizer(modelsDir) + if err != nil { + return nil, err + } + defer rec.Close() + + fmt.Println(" Capturing face frames...") + embedding, err := rec.Enroll(10) + if err != nil { + return nil, fmt.Errorf("enrollment failed: %w", err) + } + + encryptedPass, err := encryptWithEmbedding(embedding, []byte(masterPassword)) + if err != nil { + return nil, fmt.Errorf("failed to encrypt master password: %w", err) + } + + hostname, _ := os.Hostname() + + enrollment := &FaceIDEnrollment{ + Embedding: embedding, + EncryptedMasterPass: encryptedPass, + EnrolledAt: time.Now(), + DeviceName: hostname, + ModelVersion: "dlib_resnet_v1", + } + + enrollmentPath := filepath.Join(vaultDir, "faceid", "enrollment.json") + if err := os.MkdirAll(filepath.Dir(enrollmentPath), 0700); err != nil { + return nil, fmt.Errorf("failed to create enrollment directory: %w", err) + } + data, err := json.MarshalIndent(enrollment, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to encode enrollment: %w", err) + } + + if err := os.WriteFile(enrollmentPath, data, 0600); err != nil { + return nil, fmt.Errorf("failed to write enrollment: %w", err) + } + + return enrollment, nil +} + +func LoadEnrollment(vaultDir string) (*FaceIDEnrollment, error) { + enrollmentPath := filepath.Join(vaultDir, "faceid", "enrollment.json") + data, err := os.ReadFile(enrollmentPath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + var enrollment FaceIDEnrollment + if err := json.Unmarshal(data, &enrollment); err != nil { + return nil, fmt.Errorf("failed to parse enrollment: %w", err) + } + + return &enrollment, nil +} + +func RemoveEnrollment(vaultDir string) error { + enrollmentPath := filepath.Join(vaultDir, "faceid", "enrollment.json") + return os.Remove(enrollmentPath) +} + +func DecryptMasterPassword(enrollment *FaceIDEnrollment, verifiedEmbedding []float32) (string, error) { + key := embeddingToKey(enrollment.Embedding) + defer wipeBytes(key) + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonceSize := gcm.NonceSize() + if len(enrollment.EncryptedMasterPass) < nonceSize { + return "", fmt.Errorf("encrypted master password data is corrupted") + } + + nonce := enrollment.EncryptedMasterPass[:nonceSize] + ciphertext := enrollment.EncryptedMasterPass[nonceSize:] + + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", fmt.Errorf("failed to decrypt master password from face enrollment: %w", err) + } + + return string(plaintext), nil +} + +func encryptWithEmbedding(embedding []float32, plaintext []byte) ([]byte, error) { + key := embeddingToKey(embedding) + defer wipeBytes(key) + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + ciphertext := gcm.Seal(nil, nonce, plaintext, nil) + result := append(nonce, ciphertext...) + return result, nil +} + +func embeddingToKey(embedding []float32) []byte { + buf := make([]byte, len(embedding)*4) + for i, v := range embedding { + bits := math.Float32bits(v) + binary.LittleEndian.PutUint32(buf[i*4:], bits) + } + hash := sha256.Sum256(buf) + key := make([]byte, 32) + copy(key, hash[:]) + return key +} + +func wipeBytes(b []byte) { + for i := range b { + b[i] = 0 + } +} From 17130371bb6aa61fbb089166a03b63073918353f Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:22 +0530 Subject: [PATCH 27/33] Add src/faceid/faceid_cmd.go --- src/faceid/faceid_cmd.go | 214 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/faceid/faceid_cmd.go diff --git a/src/faceid/faceid_cmd.go b/src/faceid/faceid_cmd.go new file mode 100644 index 0000000..7a82d8b --- /dev/null +++ b/src/faceid/faceid_cmd.go @@ -0,0 +1,214 @@ +//go:build faceid + +package faceid + +import ( + "fmt" + "path/filepath" + + src "github.com/aaravmaloo/apm/src" + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +func BuildFaceIDCmd(vaultPath string, readPasswordFunc func() (string, error), getModelsDir func() string) *cobra.Command { + faceidCmd := &cobra.Command{ + Use: "faceid", + Short: "Manage Face ID authentication", + } + + enrollCmd := &cobra.Command{ + Use: "enroll", + Short: "Enroll your face for unlocking", + Run: func(cmd *cobra.Command, args []string) { + vaultDir := filepath.Dir(vaultPath) + modelsDir := getModelsDir() + + data, err := src.LoadVault(vaultPath) + if err != nil { + color.Red("Failed to load vault: %v", err) + return + } + + fmt.Print("Master Password: ") + mp, err := readPasswordFunc() + if err != nil { + return + } + fmt.Println() + + vault, err := src.DecryptVault(data, mp, 1) + if err != nil { + color.Red("Invalid master password") + return + } + + enrollment, err := LoadEnrollment(vaultDir) + if err != nil { + color.Red("Failed to load enrollment: %v", err) + return + } + if enrollment != nil { + color.Yellow("A face is already enrolled. Enrolling again will overwrite it.") + } + + _, err = EnrollFace(mp, vaultDir, modelsDir, vault.Profile) + if err != nil { + color.Red("Enrollment failed: %v", err) + return + } + + color.Green("Face ID enrolled successfully!") + }, + } + + testCmd := &cobra.Command{ + Use: "test", + Short: "Test face recognition without unlocking", + Run: func(cmd *cobra.Command, args []string) { + vaultDir := filepath.Dir(vaultPath) + modelsDir := getModelsDir() + + data, err := src.LoadVault(vaultPath) + if err != nil { + color.Red("Failed to load vault: %v", err) + return + } + + fmt.Print("Master Password: ") + mp, err := readPasswordFunc() + if err != nil { + return + } + fmt.Println() + + vault, err := src.DecryptVault(data, mp, 1) + if err != nil { + color.Red("Invalid master password") + return + } + + enrollment, err := LoadEnrollment(vaultDir) + if err != nil { + color.Red("Failed to load enrollment: %v", err) + return + } + if enrollment == nil { + color.Red("No face enrolled. Run 'pm faceid enroll' first.") + return + } + + if err := EnsureModels(modelsDir); err != nil { + color.Red("Face ID models download failed: %v", err) + return + } + + rec, err := NewRecognizer(modelsDir) + if err != nil { + color.Red("Failed to init recognizer: %v", err) + return + } + defer rec.Close() + + fmt.Println("Capturing face...") + matched, conf, err := rec.Verify(enrollment.Embedding, vault.Profile) + if err != nil { + color.Red("Verification failed: %v", err) + return + } + + if matched { + color.Green("PASS - Match confirmed (score: %.3f)", conf) + } else { + color.Red("FAIL - No match (best score: %.3f)", conf) + } + }, + } + + removeCmd := &cobra.Command{ + Use: "remove", + Short: "Remove Face ID enrollment", + Run: func(cmd *cobra.Command, args []string) { + vaultDir := filepath.Dir(vaultPath) + data, err := src.LoadVault(vaultPath) + if err != nil { + color.Red("Failed to load vault: %v", err) + return + } + + fmt.Print("Master Password: ") + mp, err := readPasswordFunc() + if err != nil { + return + } + fmt.Println() + + _, err = src.DecryptVault(data, mp, 1) + if err != nil { + color.Red("Invalid master password") + return + } + + enrollment, err := LoadEnrollment(vaultDir) + if err != nil { + color.Red("Failed to load enrollment: %v", err) + return + } + if enrollment == nil { + color.Yellow("No face enrolled.") + return + } + + err = RemoveEnrollment(vaultDir) + if err != nil { + color.Red("Failed to remove enrollment: %v", err) + return + } + + color.Green("Face ID enrollment removed.") + }, + } + + statusCmd := &cobra.Command{ + Use: "status", + Short: "Show Face ID enrollment status", + Run: func(cmd *cobra.Command, args []string) { + vaultDir := filepath.Dir(vaultPath) + data, err := src.LoadVault(vaultPath) + if err != nil { + color.Red("Failed to load vault: %v", err) + return + } + + fmt.Print("Master Password: ") + mp, err := readPasswordFunc() + if err != nil { + return + } + fmt.Println() + + _, err = src.DecryptVault(data, mp, 1) + if err != nil { + color.Red("Invalid master password") + return + } + + enrollment, err := LoadEnrollment(vaultDir) + if err != nil { + color.Red("Failed to load enrollment: %v", err) + return + } + if enrollment == nil { + fmt.Println("Face ID: Not Enrolled") + } else { + fmt.Println("Face ID: Enrolled") + fmt.Printf(" Enrolled At: %s\n", enrollment.EnrolledAt.Format("Jan 02, 2006 15:04:05")) + fmt.Printf(" Device Name: %s\n", enrollment.DeviceName) + fmt.Printf(" Model Version: %s\n", enrollment.ModelVersion) + } + }, + } + + faceidCmd.AddCommand(enrollCmd, testCmd, removeCmd, statusCmd) + return faceidCmd +} From 4474e1b0d300657a6889a1177ad505cf0a7114a1 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:22 +0530 Subject: [PATCH 28/33] Add src/faceid/input_stub.go --- src/faceid/input_stub.go | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/faceid/input_stub.go diff --git a/src/faceid/input_stub.go b/src/faceid/input_stub.go new file mode 100644 index 0000000..02378bf --- /dev/null +++ b/src/faceid/input_stub.go @@ -0,0 +1,7 @@ +//go:build faceid && !windows + +package faceid + +func inputAvailable() bool { + return false +} From f14ab7ec2c5e9fea10b66c34ae270e11c2c88c0b Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:23 +0530 Subject: [PATCH 29/33] Add src/faceid/input_windows.go --- src/faceid/input_windows.go | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/faceid/input_windows.go diff --git a/src/faceid/input_windows.go b/src/faceid/input_windows.go new file mode 100644 index 0000000..83658d6 --- /dev/null +++ b/src/faceid/input_windows.go @@ -0,0 +1,75 @@ +//go:build faceid && windows + +package faceid + +import ( + "os" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +const keyEvent = 0x0001 + +type inputRecord struct { + EventType uint16 + _ uint16 + Event [16]byte +} + +type keyEventRecord struct { + KeyDown int32 + RepeatCount uint16 + VirtualKeyCode uint16 + VirtualScanCode uint16 + UnicodeChar uint16 + ControlKeyState uint32 +} + +var ( + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + procPeekConsoleInput = modkernel32.NewProc("PeekConsoleInputW") +) + +func peekConsoleInput(h windows.Handle, records *inputRecord, length uint32, read *uint32) error { + r1, _, e1 := procPeekConsoleInput.Call( + uintptr(h), + uintptr(unsafe.Pointer(records)), + uintptr(length), + uintptr(unsafe.Pointer(read)), + ) + if r1 == 0 { + if e1 != nil && e1 != syscall.Errno(0) { + return e1 + } + return syscall.EINVAL + } + return nil +} + +func inputAvailable() bool { + h := windows.Handle(os.Stdin.Fd()) + var n uint32 + if err := windows.GetNumberOfConsoleInputEvents(h, &n); err != nil || n == 0 { + return false + } + if n > 64 { + n = 64 + } + records := make([]inputRecord, n) + var read uint32 + if err := peekConsoleInput(h, &records[0], uint32(len(records)), &read); err != nil { + return false + } + for i := 0; i < int(read); i++ { + if records[i].EventType != keyEvent { + continue + } + kev := (*keyEventRecord)(unsafe.Pointer(&records[i].Event[0])) + if kev.KeyDown != 0 { + return true + } + } + return false +} From fa7da634e09f641ef9fef5b0e0a22e3a1d64b4d2 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:23 +0530 Subject: [PATCH 30/33] Add src/faceid/models.go --- src/faceid/models.go | 92 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/faceid/models.go diff --git a/src/faceid/models.go b/src/faceid/models.go new file mode 100644 index 0000000..04f9a60 --- /dev/null +++ b/src/faceid/models.go @@ -0,0 +1,92 @@ +//go:build faceid + +package faceid + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" +) + +type modelSpec struct { + Name string + URL string +} + +var requiredModels = []modelSpec{ + { + Name: "shape_predictor_5_face_landmarks.dat", + URL: "https://github.com/Kagami/go-face-testdata/raw/master/models/shape_predictor_5_face_landmarks.dat", + }, + { + Name: "dlib_face_recognition_resnet_model_v1.dat", + URL: "https://github.com/Kagami/go-face-testdata/raw/master/models/dlib_face_recognition_resnet_model_v1.dat", + }, + { + Name: "mmod_human_face_detector.dat", + URL: "https://github.com/Kagami/go-face-testdata/raw/master/models/mmod_human_face_detector.dat", + }, +} + +func EnsureModels(modelsDir string) error { + if modelsDir == "" { + return fmt.Errorf("models directory not set") + } + if err := os.MkdirAll(modelsDir, 0700); err != nil { + return fmt.Errorf("failed to create models dir: %w", err) + } + + for _, m := range requiredModels { + dst := filepath.Join(modelsDir, m.Name) + if fi, err := os.Stat(dst); err == nil && fi.Size() > 0 { + continue + } + if err := downloadFile(dst, m.URL); err != nil { + return fmt.Errorf("failed to download %s: %w", m.Name, err) + } + } + + return nil +} + +func downloadFile(dstPath, url string) error { + tmpPath := dstPath + ".tmp" + _ = os.Remove(tmpPath) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "apm-faceid/1.0") + + client := &http.Client{Timeout: 10 * time.Minute} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("unexpected HTTP status %s", resp.Status) + } + + out, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + if _, err := io.Copy(out, resp.Body); err != nil { + _ = out.Close() + return err + } + if err := out.Close(); err != nil { + return err + } + + if err := os.Rename(tmpPath, dstPath); err != nil { + return err + } + return nil +} From 0a1a871493f814b2cbace100445182d6c963d517 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:23 +0530 Subject: [PATCH 31/33] Add src/faceid/recognizer.go --- src/faceid/recognizer.go | 334 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 src/faceid/recognizer.go diff --git a/src/faceid/recognizer.go b/src/faceid/recognizer.go new file mode 100644 index 0000000..82301c8 --- /dev/null +++ b/src/faceid/recognizer.go @@ -0,0 +1,334 @@ +//go:build faceid + +package faceid + +import ( + "context" + "fmt" + "image" + "image/color" + "math" + "os" + "os/signal" + "time" + + "github.com/Kagami/go-face" + "gocv.io/x/gocv" +) + +var ThresholdByProfile = map[string]float32{ + "standard": 0.45, + "hardened": 0.38, + "paranoid": 0.32, + "legacy": 0.45, +} + +const ( + DefaultThreshold = 0.45 + brightenAlpha = 1.35 + brightenBeta = 45.0 + enrollMinClean = 5 +) + +type Recognizer struct { + rec *face.Recognizer + modelsDir string +} + +func NewRecognizer(modelsDir string) (*Recognizer, error) { + rec, err := face.NewRecognizer(modelsDir) + if err != nil { + return nil, fmt.Errorf("failed to init face recognizer (models dir: %s): %w", modelsDir, err) + } + return &Recognizer{rec: rec, modelsDir: modelsDir}, nil +} + +func (r *Recognizer) Close() { + if r.rec != nil { + r.rec.Close() + } +} + +func (r *Recognizer) Enroll(numFrames int) ([]float32, error) { + if numFrames < 5 { + numFrames = 10 + } + + embeddings, err := r.enrollWithPreview(numFrames) + if err != nil { + return nil, err + } + + if len(embeddings) < enrollMinClean { + return nil, fmt.Errorf("insufficient clean frames: got %d, need at least %d", len(embeddings), enrollMinClean) + } + + avg := make([]float32, 128) + for _, emb := range embeddings { + for i := 0; i < 128; i++ { + avg[i] += float32(emb[i]) + } + } + count := float32(len(embeddings)) + for i := range avg { + avg[i] /= count + } + + return avg, nil +} + +func (r *Recognizer) Verify(stored []float32, securityProfile string) (bool, float32, error) { + return r.VerifyWithContext(context.Background(), stored, securityProfile, 6) +} + +func (r *Recognizer) VerifyWithContext(ctx context.Context, stored []float32, securityProfile string, maxFrames int) (bool, float32, error) { + threshold := DefaultThreshold + if t, ok := ThresholdByProfile[securityProfile]; ok { + threshold = float64(t) + } + + frameCh := make(chan []byte, 2) + errCh := make(chan error, 1) + + go streamFrames(ctx, frameCh, errCh) + + var bestConfidence float32 + processed := 0 + + for processed < maxFrames { + select { + case <-ctx.Done(): + return false, bestConfidence, fmt.Errorf("verification cancelled") + case err := <-errCh: + if err != nil { + return false, bestConfidence, err + } + case imgBytes, ok := <-frameCh: + if !ok { + return false, bestConfidence, ErrNoCamera + } + processed++ + + faces, err := r.rec.Recognize(imgBytes) + if err != nil || len(faces) != 1 { + continue + } + + dist := cosineDistance(stored, descriptorToFloat32Slice(faces[0].Descriptor)) + conf := 1.0 - dist + + if float64(dist) < threshold { + return true, conf, nil + } + + if conf > bestConfidence { + bestConfidence = conf + } + } + } + + return false, bestConfidence, nil +} + +func (r *Recognizer) enrollWithPreview(numFrames int) ([]face.Descriptor, error) { + webcam, err := gocv.OpenVideoCapture(0) + if err != nil { + return nil, ErrNoCamera + } + defer webcam.Close() + + if !webcam.IsOpened() { + return nil, ErrNoCamera + } + + img := gocv.NewMat() + defer img.Close() + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + displayCh := make(chan gocv.Mat, 1) + doneCh := make(chan struct{}) + go func() { + defer close(doneCh) + win := gocv.NewWindow("APM Face ID Enrollment (Ctrl+C to cancel)") + defer win.Close() + for { + select { + case <-ctx.Done(): + return + case frame, ok := <-displayCh: + if !ok { + return + } + if frame.Empty() { + frame.Close() + continue + } + win.IMShow(frame) + win.WaitKey(1) + frame.Close() + } + } + }() + + var embeddings []face.Descriptor + + for len(embeddings) < numFrames { + select { + case <-ctx.Done(): + close(displayCh) + <-doneCh + return nil, fmt.Errorf("enrollment cancelled") + default: + } + + if ok := webcam.Read(&img); !ok || img.Empty() { + continue + } + + bright := brightenMat(img) + buf, err := gocv.IMEncode(gocv.JPEGFileExt, bright) + if err != nil { + bright.Close() + continue + } + imgBytes := append([]byte(nil), buf.GetBytes()...) + buf.Close() + + faces, err := r.rec.Recognize(imgBytes) + if err == nil { + for _, f := range faces { + gocv.Rectangle(&bright, f.Rectangle, color.RGBA{0, 255, 0, 0}, 2) + } + } + + status := fmt.Sprintf("Frames: %d/%d", len(embeddings), numFrames) + if err == nil && len(faces) == 1 { + embeddings = append(embeddings, faces[0].Descriptor) + status = fmt.Sprintf("Captured %d/%d", len(embeddings), numFrames) + } else if err == nil && len(faces) > 1 { + status = "Multiple faces detected" + } else if err == nil && len(faces) == 0 { + status = "No face detected" + } + + gocv.PutText( + &bright, + status, + image.Pt(10, 30), + gocv.FontHersheySimplex, + 0.8, + color.RGBA{255, 255, 255, 0}, + 2, + ) + + if !trySendFrame(displayCh, bright) { + bright.Close() + } + } + + close(displayCh) + <-doneCh + return embeddings, nil +} + +func streamFrames(ctx context.Context, out chan<- []byte, errCh chan<- error) { + webcam, err := gocv.OpenVideoCapture(0) + if err != nil { + errCh <- ErrNoCamera + close(out) + return + } + defer webcam.Close() + + if !webcam.IsOpened() { + errCh <- ErrNoCamera + close(out) + return + } + + img := gocv.NewMat() + defer img.Close() + + for { + select { + case <-ctx.Done(): + close(out) + return + default: + } + + if ok := webcam.Read(&img); !ok || img.Empty() { + time.Sleep(20 * time.Millisecond) + continue + } + + bright := brightenMat(img) + buf, err := gocv.IMEncode(gocv.JPEGFileExt, bright) + bright.Close() + if err != nil { + continue + } + imgBytes := append([]byte(nil), buf.GetBytes()...) + buf.Close() + + select { + case out <- imgBytes: + default: + } + + time.Sleep(10 * time.Millisecond) + } +} + +func brightenMat(src gocv.Mat) gocv.Mat { + dst := gocv.NewMat() + gocv.ConvertScaleAbs(src, &dst, brightenAlpha, brightenBeta) + return dst +} + +func trySendFrame(ch chan<- gocv.Mat, frame gocv.Mat) bool { + select { + case ch <- frame: + return true + default: + return false + } +} + +func cosineDistance(a, b []float32) float32 { + if len(a) != len(b) || len(a) == 0 { + return 1.0 + } + + var dotProduct, normA, normB float64 + for i := range a { + dotProduct += float64(a[i]) * float64(b[i]) + normA += float64(a[i]) * float64(a[i]) + normB += float64(b[i]) * float64(b[i]) + } + + if normA == 0 || normB == 0 { + return 1.0 + } + + similarity := dotProduct / (math.Sqrt(normA) * math.Sqrt(normB)) + return float32(1.0 - similarity) +} + +func float32SliceToDescriptor(s []float32) face.Descriptor { + var d face.Descriptor + for i := 0; i < 128 && i < len(s); i++ { + d[i] = s[i] + } + return d +} + +func descriptorToFloat32Slice(d face.Descriptor) []float32 { + s := make([]float32, 128) + for i := 0; i < 128; i++ { + s[i] = float32(d[i]) + } + return s +} From 99d2652e6522129b034889906ca1a08017246755 Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:23 +0530 Subject: [PATCH 32/33] Add src/faceid/stubs.go --- src/faceid/stubs.go | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/faceid/stubs.go diff --git a/src/faceid/stubs.go b/src/faceid/stubs.go new file mode 100644 index 0000000..882f126 --- /dev/null +++ b/src/faceid/stubs.go @@ -0,0 +1,51 @@ +//go:build !faceid + +package faceid + +import ( + "errors" + "time" + + "github.com/spf13/cobra" +) + +var ErrNotCompiled = errors.New("apm was compiled without Face ID support") + +type FaceIDEnrollment struct { + Embedding []float32 `json:"embedding"` + EncryptedMasterPass []byte `json:"encrypted_master"` + EnrolledAt time.Time `json:"enrolled_at"` + DeviceName string `json:"device_name"` + ModelVersion string `json:"model_version"` +} + +func LoadEnrollment(vaultDir string) (*FaceIDEnrollment, error) { + return nil, nil +} + +func RemoveEnrollment(vaultDir string) error { + return nil +} + +func EnrollFace(masterPassword string, vaultDir string, modelsDir string, profile string) (*FaceIDEnrollment, error) { + return nil, ErrNotCompiled +} + +func EnsureModels(modelsDir string) error { + return ErrNotCompiled +} + +func RaceUnlock(enrollment *FaceIDEnrollment, modelsDir string, securityProfile string) (string, string, error) { + return "", "", ErrNotCompiled +} + +func BuildFaceIDCmd(vaultPath string, readPasswordFunc func() (string, error), getModelsDir func() string) *cobra.Command { + cmd := &cobra.Command{ + Use: "faceid", + Short: "Manage Face ID authentication (Not compiled)", + RunE: func(cmd *cobra.Command, args []string) error { + return ErrNotCompiled + }, + } + return cmd +} From 5c1bd6e0c6fad3410730c7e11ac9710269f2a00e Mon Sep 17 00:00:00 2001 From: Aarav Maloo Date: Sat, 14 Mar 2026 16:07:23 +0530 Subject: [PATCH 33/33] Add src/faceid/unlock.go --- src/faceid/unlock.go | 99 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/faceid/unlock.go diff --git a/src/faceid/unlock.go b/src/faceid/unlock.go new file mode 100644 index 0000000..ae75af1 --- /dev/null +++ b/src/faceid/unlock.go @@ -0,0 +1,99 @@ +//go:build faceid + +package faceid + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "sync/atomic" + "time" + + "golang.org/x/term" +) + +var ErrModelsNotFound = fmt.Errorf("face recognition models not found") +var ErrEnrollmentCorrupt = fmt.Errorf("face enrollment data is corrupted") + +func RaceUnlock(enrollment *FaceIDEnrollment, modelsDir string, securityProfile string) (string, string, error) { + if enrollment == nil { + return "", "", fmt.Errorf("no face enrollment provided") + } + + if len(enrollment.Embedding) == 0 || len(enrollment.EncryptedMasterPass) == 0 { + fmt.Printf(" %s⚠ Face enrollment data appears corrupted, using password only%s\n", colorYellow, colorReset) + return passwordOnlyUnlock() + } + + if _, err := os.Stat(filepath.Join(modelsDir, "shape_predictor_5_face_landmarks.dat")); os.IsNotExist(err) { + if err := EnsureModels(modelsDir); err != nil { + fmt.Printf(" %sℹ Face ID models missing and auto-download failed: %v%s\n", colorDimGray, err, colorReset) + fmt.Printf(" %sℹ Run 'pm faceid enroll' to set up Face ID%s\n", colorDimGray, colorReset) + return passwordOnlyUnlock() + } + } + + fmt.Println("Looking for face... (press any key to type password)") + + rec, err := NewRecognizer(modelsDir) + if err != nil { + return passwordOnlyUnlock() + } + defer rec.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wantPassword int32 + go func() { + ticker := time.NewTicker(20 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if inputAvailable() { + atomic.StoreInt32(&wantPassword, 1) + cancel() + return + } + } + } + }() + + matched, _, err := rec.VerifyWithContext(ctx, enrollment.Embedding, securityProfile, 6) + if atomic.LoadInt32(&wantPassword) == 1 { + return passwordOnlyUnlock() + } + if err != nil || !matched { + return passwordOnlyUnlock() + } + + password, err := DecryptMasterPassword(enrollment, enrollment.Embedding) + if err != nil { + return passwordOnlyUnlock() + } + + return "face", password, nil +} + +func passwordOnlyUnlock() (string, string, error) { + fmt.Print("Master password: ") + if term.IsTerminal(int(os.Stdin.Fd())) { + bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", "", err + } + fmt.Println() + return "password", strings.TrimSpace(string(bytePassword)), nil + } + buf := make([]byte, 1024) + n, err := os.Stdin.Read(buf) + if err != nil { + return "", "", err + } + return "password", strings.TrimSpace(string(buf[:n])), nil +}