├── .babelrc ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql.yml ├── .gitignore ├── .node-version ├── .npmignore ├── LICENSE.md ├── README.md ├── binding.gyp ├── docker ├── arm64-cross-compile │ └── Dockerfile ├── armv7l-cross-compile │ └── Dockerfile └── i386 │ └── Dockerfile ├── keytar.d.ts ├── lib └── keytar.js ├── package-lock.json ├── package.json ├── script ├── cibuild ├── download-node-lib-win-arm64.ps1 └── upload.js ├── spec └── keytar-spec.js └── src ├── async.cc ├── async.h ├── credentials.h ├── keytar.h ├── keytar_mac.cc ├── keytar_posix.cc ├── keytar_win.cc └── main.cc /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": "inline", 3 | "plugins": ["transform-async-to-generator"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: 7 | - v*.*.* 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build: 13 | name: ${{ matrix.friendlyName }} 14 | env: 15 | CC: "clang" 16 | CXX: "clang++" 17 | npm_config_clang: "1" 18 | 19 | strategy: 20 | matrix: 21 | node-version: [15.x] 22 | os: [ubuntu-20.04, windows-2019, macos-latest] 23 | include: 24 | - os: ubuntu-20.04 25 | friendlyName: Ubuntu 26 | - os: windows-2019 27 | friendlyName: Windows 28 | - os: macos-latest 29 | friendlyName: macOS 30 | 31 | runs-on: ${{ matrix.os }} 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Use Node.js ${{ matrix.node-version }} 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | - run: | 40 | sudo apt-get install gnome-keyring \ 41 | libsecret-1-dev \ 42 | dbus-x11 \ 43 | python3-dev 44 | if: ${{ matrix.os == 'ubuntu-20.04' }} 45 | name: Install additional dependencies 46 | 47 | # This step can be removed as soon as official Windows arm64 builds are published: 48 | # https://github.com/nodejs/build/issues/2450#issuecomment-705853342 49 | - run: | 50 | $NodeVersion = (node --version) -replace '^.' 51 | $NodeFallbackVersion = "15.8.0" 52 | & .\script\download-node-lib-win-arm64.ps1 $NodeVersion $NodeFallbackVersion 53 | if: ${{ matrix.os == 'windows-2019' }} 54 | name: Install Windows arm64 node.lib 55 | 56 | - run: npm install 57 | name: Setup environment 58 | 59 | - run: npm run build 60 | name: Build native module from source 61 | 62 | - run: | 63 | echo "Install keyring..." 64 | pip3 install --upgrade pip 65 | pip3 install keyring 66 | echo "Prepare D-Bus session..." 67 | eval $(dbus-launch --sh-syntax); 68 | eval $(echo 'somecredstorepass' | gnome-keyring-daemon --unlock) 69 | echo "Create a test key using script..." 70 | python -c "import keyring;keyring.set_password('system', 'login', 'pwd');" 71 | npm test 72 | if: ${{ matrix.os == 'ubuntu-20.04' }} 73 | name: Run tests (Linux) 74 | 75 | - run: npm test 76 | if: ${{ matrix.os != 'ubuntu-20.04' }} 77 | name: Run tests (Windows/macOS) 78 | 79 | - run: npm run prebuild-napi-x64 80 | name: Prebuild (x64) 81 | 82 | - run: npm run prebuild-napi-arm64 83 | name: Prebuild (arm64) 84 | if: ${{ matrix.os != 'ubuntu-20.04' }} 85 | 86 | - run: npm run prebuild-napi-ia32 87 | if: ${{ matrix.os == 'windows-2019' }} 88 | name: Prebuild (Windows x86) 89 | 90 | - run: | 91 | mkdir -p prebuilds && chmod 777 prebuilds 92 | docker build -t node-keytar/i386 docker/i386 93 | docker run --rm -v ${PWD}:/project node-keytar/i386 /bin/bash -c "cd /project && npm run prebuild-napi-ia32 && rm -rf build" 94 | docker build -t node-keytar/arm64-cross-compile docker/arm64-cross-compile 95 | docker run --rm -v ${PWD}:/project node-keytar/arm64-cross-compile /bin/bash -c "cd /project && npm run prebuild-napi-arm64 && rm -rf build" 96 | docker build -t node-keytar/armv7l-cross-compile docker/armv7l-cross-compile 97 | docker run --rm -v ${PWD}:/project node-keytar/armv7l-cross-compile /bin/bash -c "cd /project && npm run prebuild-napi-armv7l" 98 | if: ${{ matrix.os == 'ubuntu-20.04' }} 99 | name: Prebuild (Linux x86 + ARM64 + ARMV7L) 100 | 101 | - run: | 102 | ls prebuilds/ 103 | name: List prebuilds 104 | 105 | - name: Upload prebuilds to GitHub 106 | run: npm run upload 107 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 108 | env: 109 | GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 110 | 111 | alpine-build: 112 | runs-on: ubuntu-latest 113 | container: node:15-alpine3.12 114 | steps: 115 | - uses: actions/checkout@v2 116 | - name: install additional dependencies 117 | run: | 118 | apk add g++ make python2 libsecret-dev 119 | 120 | - run: npm install 121 | name: Setup environment 122 | 123 | - run: | 124 | npm run prebuild-napi-x64 125 | npm run prebuild-napi-arm64 126 | name: Prebuild 127 | 128 | - run: | 129 | ls prebuilds/ 130 | name: List prebuilds 131 | 132 | - name: Upload prebuilds to GitHub 133 | run: npm run upload 134 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 135 | env: 136 | GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 137 | 138 | # Separate step for publishing to NPM so we're sure that generating + uploading prebuilds worked on all platforms 139 | npm-publish: 140 | needs: [build, alpine-build] 141 | name: Publish to NPM 142 | runs-on: ubuntu-20.04 143 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 144 | 145 | steps: 146 | - uses: actions/checkout@v2 147 | - name: Use Node.js 15 148 | uses: actions/setup-node@v1 149 | with: 150 | node-version: 15.x 151 | registry-url: 'https://registry.npmjs.org' 152 | 153 | - run: sudo apt-get install libsecret-1-dev 154 | name: Install additional dependencies 155 | 156 | - run: npm install 157 | name: Setup environment 158 | 159 | - run: npm publish --access public 160 | name: Upload to NPM 161 | env: 162 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 163 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "Code Scanning" 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | CodeQL: 7 | 8 | name: ${{ matrix.friendlyName }} 9 | 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest, windows-2019, macos-latest] 15 | include: 16 | - os: ubuntu-latest 17 | friendlyName: Ubuntu 18 | - os: windows-2019 19 | friendlyName: Windows 20 | - os: macos-latest 21 | friendlyName: macOS 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v2 25 | 26 | # Initializes the CodeQL tools for scanning. 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v1 29 | with: 30 | languages: javascript, cpp 31 | 32 | # Setup dependencies (and build native modules from source) 33 | - name: Install dependencies 34 | run: | 35 | npm install 36 | npm run build 37 | 38 | # Run code analysis 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v1 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /prebuilds 3 | /build 4 | *.log 5 | *~ 6 | .tags 7 | iojs-*/ 8 | 8.*/ 9 | 6.*/ 10 | .idea/ 11 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 14.15.1 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /build 2 | /spec 3 | *.log 4 | *~ 5 | .node-version 6 | .npmignore 7 | .tags 8 | .travis.yml 9 | appveyor.yml 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keytar - Node module to manage system keychain 2 | 3 | [![Travis Build Status](https://travis-ci.org/atom/node-keytar.svg?branch=master)](https://travis-ci.org/atom/node-keytar) 4 | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/atom/node-keytar?svg=true)](https://ci.appveyor.com/project/Atom/node-keytar) 5 | [![Dependency Status](https://david-dm.org/atom/node-keytar.svg)](https://david-dm.org/atom/node-keytar) 6 | 7 | A native Node module to get, add, replace, and delete passwords in system's keychain. On macOS the passwords are managed by the Keychain, on Linux they are managed by the Secret Service API/libsecret, and on Windows they are managed by Credential Vault. 8 | 9 | ## Installing 10 | 11 | ```sh 12 | npm install keytar 13 | ``` 14 | 15 | ### On Linux 16 | 17 | Currently this library uses `libsecret` so you may need to install it before running `npm install`. 18 | 19 | Depending on your distribution, you will need to run the following command: 20 | 21 | * Debian/Ubuntu: `sudo apt-get install libsecret-1-dev` 22 | * Red Hat-based: `sudo yum install libsecret-devel` 23 | * Arch Linux: `sudo pacman -S libsecret` 24 | 25 | ## Building 26 | 27 | * Clone the repository 28 | * Run `npm install` 29 | * Run `npm test` to run the tests 30 | 31 | ## Supported versions 32 | 33 | Each release of `keytar` includes prebuilt binaries for the versions of Node and Electron that are actively supported by these projects. Please refer to the release documentation for [Node](https://github.com/nodejs/Release) and [Electron](https://electronjs.org/docs/tutorial/support) to see what is supported currently. 34 | 35 | ## Bindings from other languages 36 | 37 | - [Rust](https://crates.io/crates/keytar) 38 | 39 | ## Docs 40 | 41 | ```javascript 42 | const keytar = require('keytar') 43 | ``` 44 | 45 | Every function in keytar is asynchronous and returns a promise. The promise will be rejected with any error that occurs or will be resolved with the function's "yields" value. 46 | 47 | ### getPassword(service, account) 48 | 49 | Get the stored password for the `service` and `account`. 50 | 51 | `service` - The string service name. 52 | 53 | `account` - The string account name. 54 | 55 | Yields the string password or `null` if an entry for the given service and account was not found. 56 | 57 | ### setPassword(service, account, password) 58 | 59 | Save the `password` for the `service` and `account` to the keychain. Adds a new entry if necessary, or updates an existing entry if one exists. 60 | 61 | `service` - The string service name. 62 | 63 | `account` - The string account name. 64 | 65 | `password` - The string password. 66 | 67 | Yields nothing. 68 | 69 | ### deletePassword(service, account) 70 | 71 | Delete the stored password for the `service` and `account`. 72 | 73 | `service` - The string service name. 74 | 75 | `account` - The string account name. 76 | 77 | Yields `true` if a password was deleted, or `false` if an entry with the given service and account was not found. 78 | 79 | ### findCredentials(service) 80 | 81 | Find all accounts and password for the `service` in the keychain. 82 | 83 | `service` - The string service name. 84 | 85 | Yields an array of `{ account: 'foo', password: 'bar' }`. 86 | 87 | ### findPassword(service) 88 | 89 | Find a password for the `service` in the keychain. This is ideal for scenarios where an `account` is not required. 90 | 91 | `service` - The string service name. 92 | 93 | Yields the string password, or `null` if an entry for the given service was not found. 94 | 95 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'keytar', 5 | 'defines': [ 6 | "NAPI_VERSION=<(napi_build_version)", 7 | ], 8 | 'cflags!': [ '-fno-exceptions' ], 9 | 'cflags_cc!': [ '-fno-exceptions' ], 10 | 'xcode_settings': { 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', 11 | 'CLANG_CXX_LIBRARY': 'libc++', 12 | 'MACOSX_DEPLOYMENT_TARGET': '10.7', 13 | }, 14 | 'msvs_settings': { 15 | 'VCCLCompilerTool': { 16 | 'ExceptionHandling': 1, 17 | 'AdditionalOptions': [ 18 | '/Qspectre', 19 | '/guard:cf' 20 | ] 21 | }, 22 | 'VCLinkerTool': { 23 | 'AdditionalOptions': [ 24 | '/guard:cf' 25 | ] 26 | } 27 | }, 28 | 'include_dirs': [", Brendan Forster , Hari Juturu 2 | // Adapted from DefinitelyTyped: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/keytar/index.d.ts 3 | 4 | /** 5 | * Get the stored password for the service and account. 6 | * 7 | * @param service The string service name. 8 | * @param account The string account name. 9 | * 10 | * @returns A promise for the password string. 11 | */ 12 | export declare function getPassword(service: string, account: string): Promise; 13 | 14 | /** 15 | * Add the password for the service and account to the keychain. 16 | * 17 | * @param service The string service name. 18 | * @param account The string account name. 19 | * @param password The string password. 20 | * 21 | * @returns A promise for the set password completion. 22 | */ 23 | export declare function setPassword(service: string, account: string, password: string): Promise; 24 | 25 | /** 26 | * Delete the stored password for the service and account. 27 | * 28 | * @param service The string service name. 29 | * @param account The string account name. 30 | * 31 | * @returns A promise for the deletion status. True on success. 32 | */ 33 | export declare function deletePassword(service: string, account: string): Promise; 34 | 35 | /** 36 | * Find a password for the service in the keychain. 37 | * 38 | * @param service The string service name. 39 | * 40 | * @returns A promise for the password string. 41 | */ 42 | export declare function findPassword(service: string): Promise; 43 | 44 | /** 45 | * Find all accounts and passwords for `service` in the keychain. 46 | * 47 | * @param service The string service name. 48 | * 49 | * @returns A promise for the array of found credentials. 50 | */ 51 | export declare function findCredentials(service: string): Promise>; 52 | -------------------------------------------------------------------------------- /lib/keytar.js: -------------------------------------------------------------------------------- 1 | var keytar = require('../build/Release/keytar.node') 2 | 3 | function checkRequired(val, name) { 4 | if (!val || val.length <= 0) { 5 | throw new Error(name + ' is required.'); 6 | } 7 | } 8 | 9 | module.exports = { 10 | getPassword: function (service, account) { 11 | checkRequired(service, 'Service') 12 | checkRequired(account, 'Account') 13 | 14 | return keytar.getPassword(service, account) 15 | }, 16 | 17 | setPassword: function (service, account, password) { 18 | checkRequired(service, 'Service') 19 | checkRequired(account, 'Account') 20 | checkRequired(password, 'Password') 21 | 22 | return keytar.setPassword(service, account, password) 23 | }, 24 | 25 | deletePassword: function (service, account) { 26 | checkRequired(service, 'Service') 27 | checkRequired(account, 'Account') 28 | 29 | return keytar.deletePassword(service, account) 30 | }, 31 | 32 | findPassword: function (service) { 33 | checkRequired(service, 'Service') 34 | 35 | return keytar.findPassword(service) 36 | }, 37 | 38 | findCredentials: function (service) { 39 | checkRequired(service, 'Service') 40 | 41 | return keytar.findCredentials(service) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./lib/keytar.js", 3 | "typings": "keytar.d.ts", 4 | "name": "keytar", 5 | "description": "Bindings to native Mac/Linux/Windows password APIs", 6 | "version": "7.9.0", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/atom/node-keytar.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/atom/node-keytar/issues" 14 | }, 15 | "homepage": "http://atom.github.io/node-keytar", 16 | "keywords": [ 17 | "keychain", 18 | "password", 19 | "passwords", 20 | "credential", 21 | "credentials", 22 | "vault", 23 | "credential vault" 24 | ], 25 | "files": [ 26 | "lib", 27 | "src", 28 | "binding.gyp", 29 | "keytar.d.ts" 30 | ], 31 | "types": "./keytar.d.ts", 32 | "scripts": { 33 | "install": "prebuild-install || npm run build", 34 | "build": "node-gyp rebuild", 35 | "lint": "npm run cpplint", 36 | "cpplint": "node-cpplint --filters legal-copyright,build-include,build-namespaces src/*.cc", 37 | "test": "npm run lint && npm rebuild && mocha --require babel-core/register spec/", 38 | "prebuild-napi-x64": "prebuild -t 3 -r napi -a x64 --strip", 39 | "prebuild-napi-ia32": "prebuild -t 3 -r napi -a ia32 --strip", 40 | "prebuild-napi-arm64": "prebuild -t 3 -r napi -a arm64 --strip", 41 | "prebuild-napi-armv7l": "prebuild -t 3 -r napi -a armv7l --strip", 42 | "upload": "node ./script/upload.js" 43 | }, 44 | "devDependencies": { 45 | "babel-core": "^6.26.3", 46 | "babel-plugin-transform-async-to-generator": "^6.24.1", 47 | "chai": "^4.2.0", 48 | "mocha": "^9.2.0", 49 | "node-cpplint": "~0.4.0", 50 | "node-gyp": "^8.4.1", 51 | "prebuild": "^11.0.2" 52 | }, 53 | "dependencies": { 54 | "node-addon-api": "^4.3.0", 55 | "prebuild-install": "^7.0.1" 56 | }, 57 | "binary": { 58 | "napi_versions": [ 59 | 3 60 | ] 61 | }, 62 | "config": { 63 | "runtime": "napi", 64 | "target": 3 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Unlocking the keyring..." 4 | eval $(echo -n "" | /usr/bin/gnome-keyring-daemon --login) 5 | eval $(/usr/bin/gnome-keyring-daemon --components=secrets --start) 6 | export GNOME_KEYRING_CONTROL GNOME_KEYRING_PID GPG_AGENT_INFO SSH_AUTH_SOCK 7 | echo "Running tests..." 8 | npm test 9 | -------------------------------------------------------------------------------- /script/download-node-lib-win-arm64.ps1: -------------------------------------------------------------------------------- 1 | # This script can be removed as soon as official Windows arm64 builds are published: 2 | # https://github.com/nodejs/build/issues/2450#issuecomment-705853342 3 | 4 | $nodeVersion = $args[0] 5 | $fallbackVersion = $args[1] 6 | 7 | If ($null -eq $nodeVersion -Or $null -eq $fallbackVersion) { 8 | Write-Error "No NodeJS version given as argument to this file. Run it like download-nodejs-win-arm64.ps1 NODE_VERSION NODE_FALLBACK_VERSION" 9 | exit 1 10 | } 11 | 12 | $url = "https://unofficial-builds.nodejs.org/download/release/v$nodeVersion/win-arm64/node.lib" 13 | $fallbackUrl = "https://unofficial-builds.nodejs.org/download/release/v$fallbackVersion/win-arm64/node.lib" 14 | 15 | # Always write to the $nodeVersion cache folder, even if we're using the fallbackVersion 16 | $cacheFolder = "$env:TEMP\prebuild\napi\$nodeVersion\arm64" 17 | 18 | If (!(Test-Path $cacheFolder)) { 19 | New-Item -ItemType Directory -Force -Path $cacheFolder 20 | } 21 | 22 | $output = "$cacheFolder\node.lib" 23 | $start_time = Get-Date 24 | 25 | Try { 26 | Invoke-WebRequest -Uri $url -OutFile $output 27 | $downloadedNodeVersion = $nodeVersion 28 | } Catch { 29 | If ($_.Exception.Response -And $_.Exception.Response.StatusCode -eq "NotFound") { 30 | Write-Output "No arm64 node.lib found for Node Windows $nodeVersion, trying fallback version $fallbackVersion..." 31 | Invoke-WebRequest -Uri $fallbackUrl -OutFile $output 32 | $downloadedNodeVersion = $fallbackVersion 33 | } 34 | } 35 | 36 | Write-Output "Downloaded arm64 NodeJS lib v$downloadedNodeVersion to $output in $((Get-Date).Subtract($start_time).Seconds) second(s)" 37 | -------------------------------------------------------------------------------- /script/upload.js: -------------------------------------------------------------------------------- 1 | // to ensure that env not in the CI server log 2 | 3 | const path = require("path") 4 | const { spawnSync } = require("child_process") 5 | 6 | spawnSync(path.join(__dirname, '../node_modules/.bin/prebuild' + (process.platform === "win32" ? '.cmd' : '')), ['--upload-all', process.env.GITHUB_AUTH_TOKEN], {stdio: 'inherit'}) 7 | -------------------------------------------------------------------------------- /spec/keytar-spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert 2 | var keytar = require('../') 3 | 4 | describe("keytar", function() { 5 | var service = 'keytar tests' 6 | var service2 = 'other tests' 7 | var account = 'buster' 8 | var password = 'secret' 9 | var account2 = 'buster2' 10 | var password2 = 'secret2' 11 | 12 | var object = {} 13 | object.toString = function () { 14 | throw new Error("Whoops! Time to seg fault") 15 | } 16 | 17 | beforeEach(async function() { 18 | await keytar.deletePassword(service, account), 19 | await keytar.deletePassword(service, account2) 20 | await keytar.deletePassword(service2, account) 21 | 22 | }) 23 | 24 | afterEach(async function() { 25 | await keytar.deletePassword(service, account), 26 | await keytar.deletePassword(service, account2) 27 | await keytar.deletePassword(service2, account) 28 | }) 29 | 30 | describe("setPassword/getPassword(service, account)", function() { 31 | it("sets and yields the password for the service and account", async function() { 32 | await keytar.setPassword(service, account, password) 33 | assert.equal(await keytar.getPassword(service, account), password) 34 | await keytar.setPassword(service, account, password2) 35 | assert.equal(await keytar.getPassword(service, account), password2) 36 | }) 37 | 38 | it("yields null when the password was not found", async function() { 39 | assert.equal(await keytar.getPassword(service, account), null) 40 | }) 41 | 42 | describe("error handling", function () { 43 | describe('setPassword', () => { 44 | it("handles when an object is provided for service", async function () { 45 | try { 46 | await keytar.setPassword(object, account, password) 47 | } catch (err) { 48 | assert.equal(err.message, "Parameter 'service' must be a string") 49 | } 50 | }) 51 | 52 | it("handles when an object is provided for username", async function () { 53 | try { 54 | await keytar.setPassword(service, object, password) 55 | } catch (err) { 56 | assert.equal(err.message, "Parameter 'username' must be a string") 57 | } 58 | }) 59 | 60 | it("handles when an object is provided for password", async function () { 61 | try { 62 | await keytar.setPassword(service, account, object) 63 | } catch (err) { 64 | assert.equal(err.message, "Parameter 'password' must be a string") 65 | } 66 | }) 67 | }) 68 | 69 | describe('getPassword', () => { 70 | it("handles when an object is provided for service", async function () { 71 | try { 72 | await keytar.getPassword(object, account) 73 | } catch (err) { 74 | assert.equal(err.message, "Parameter 'service' must be a string") 75 | } 76 | }) 77 | 78 | it("handles when an object is provided for username", async function () { 79 | try { 80 | await keytar.getPassword(service, object) 81 | } catch (err) { 82 | assert.equal(err.message, "Parameter 'username' must be a string") 83 | } 84 | }) 85 | }) 86 | }) 87 | 88 | describe("Unicode support", function() { 89 | const service = "se®vi\u00C7e" 90 | const account = "shi\u0191\u2020ke\u00A5" 91 | const password = "p\u00E5ssw\u00D8®\u2202" 92 | 93 | it("handles unicode strings everywhere", async function() { 94 | await keytar.setPassword(service, account, password) 95 | assert.equal(await keytar.getPassword(service, account), password) 96 | }) 97 | 98 | afterEach(async function() { 99 | await keytar.deletePassword(service, account) 100 | }) 101 | }) 102 | }) 103 | 104 | describe("deletePassword(service, account)", function() { 105 | it("yields true when the password was deleted", async function() { 106 | await keytar.setPassword(service, account, password) 107 | assert.equal(await keytar.deletePassword(service, account), true) 108 | }) 109 | 110 | it("yields false when the password didn't exist", async function() { 111 | assert.equal(await keytar.deletePassword(service, account), false) 112 | }) 113 | 114 | describe("error handling", function () { 115 | it("handles when an object is provided for service", async function () { 116 | try { 117 | await keytar.deletePassword(object, account) 118 | } catch (err) { 119 | assert.equal(err.message, "Parameter 'service' must be a string") 120 | } 121 | }) 122 | 123 | it("handles when an object is provided for username", async function () { 124 | try { 125 | await keytar.deletePassword(service, object) 126 | } catch (err) { 127 | assert.equal(err.message, "Parameter 'username' must be a string") 128 | } 129 | }) 130 | }) 131 | }) 132 | 133 | describe("findPassword(service)", function() { 134 | this.timeout(5000); 135 | 136 | it("yields a password for the service", async function() { 137 | await keytar.setPassword(service, account, password), 138 | await keytar.setPassword(service, account2, password2) 139 | assert.include([password, password2], await keytar.findPassword(service)) 140 | }) 141 | 142 | it("yields null when no password can be found", async function() { 143 | assert.equal(await keytar.findPassword(service), null) 144 | }) 145 | 146 | it("handles when an object is provided for service", async function () { 147 | try { 148 | await keytar.findPassword(object) 149 | } catch (err) { 150 | assert.equal(err.message, "Parameter 'service' must be a string") 151 | } 152 | }) 153 | }) 154 | 155 | describe('findCredentials(service)', function() { 156 | it('yields an array of the credentials', async function() { 157 | await keytar.setPassword(service, account, password) 158 | await keytar.setPassword(service, account2, password2) 159 | await keytar.setPassword(service2, account, password) 160 | 161 | const found = await keytar.findCredentials(service) 162 | const sorted = found.sort(function(a, b) { 163 | return a.account.localeCompare(b.account) 164 | }) 165 | 166 | assert.deepEqual([{account: account, password: password}, {account: account2, password: password2}], sorted) 167 | }); 168 | 169 | it('returns an empty array when no credentials are found', async function() { 170 | const accounts = await keytar.findCredentials(service) 171 | assert.deepEqual([], accounts) 172 | }) 173 | 174 | it("handles when an object is provided for service", async function () { 175 | try { 176 | await keytar.findCredentials(object) 177 | } catch (err) { 178 | assert.equal(err.message, "Parameter 'service' must be a string") 179 | } 180 | }) 181 | 182 | describe("Unicode support", function() { 183 | const service = "se®vi\u00C7e" 184 | const account = "shi\u0191\u2020ke\u00A5" 185 | const password = "p\u00E5ssw\u00D8®\u2202" 186 | 187 | it("handles unicode strings everywhere", async function() { 188 | await keytar.setPassword(service, account, password) 189 | await keytar.setPassword(service, account2, password2) 190 | 191 | const found = await keytar.findCredentials(service) 192 | const sorted = found.sort(function(a, b) { 193 | return a.account.localeCompare(b.account) 194 | }) 195 | 196 | assert.deepEqual([{account: account2, password: password2}, {account: account, password: password}], sorted) 197 | }) 198 | 199 | afterEach(async function() { 200 | await keytar.deletePassword(service, account) 201 | }) 202 | }) 203 | }); 204 | }) 205 | -------------------------------------------------------------------------------- /src/async.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "napi.h" 5 | #include "keytar.h" 6 | #include "async.h" 7 | 8 | using keytar::KEYTAR_OP_RESULT; 9 | 10 | SetPasswordWorker::SetPasswordWorker( 11 | const std::string& service, 12 | const std::string& account, 13 | const std::string& password, 14 | const Napi::Env &env 15 | ) : AsyncWorker(env), 16 | service(service), 17 | account(account), 18 | password(password), 19 | deferred(Napi::Promise::Deferred::New(env)) {} 20 | 21 | SetPasswordWorker::~SetPasswordWorker() {} 22 | 23 | Napi::Promise SetPasswordWorker::Promise() { 24 | return deferred.Promise(); 25 | } 26 | 27 | void SetPasswordWorker::Execute() { 28 | std::string error; 29 | KEYTAR_OP_RESULT result = keytar::SetPassword(service, 30 | account, 31 | password, 32 | &error); 33 | if (result == keytar::FAIL_ERROR) { 34 | SetError(error.c_str()); 35 | } 36 | } 37 | 38 | void SetPasswordWorker::OnOK() { 39 | Napi::HandleScope scope(Env()); 40 | deferred.Resolve(Env().Undefined()); 41 | } 42 | 43 | void SetPasswordWorker::OnError(Napi::Error const &error) { 44 | Napi::HandleScope scope(Env()); 45 | deferred.Reject(error.Value()); 46 | } 47 | 48 | 49 | GetPasswordWorker::GetPasswordWorker( 50 | const std::string& service, 51 | const std::string& account, 52 | const Napi::Env &env 53 | ) : AsyncWorker(env), 54 | service(service), 55 | account(account), 56 | deferred(Napi::Promise::Deferred::New(env)) {} 57 | 58 | GetPasswordWorker::~GetPasswordWorker() {} 59 | 60 | Napi::Promise GetPasswordWorker::Promise() { 61 | return deferred.Promise(); 62 | } 63 | 64 | void GetPasswordWorker::Execute() { 65 | std::string error; 66 | KEYTAR_OP_RESULT result = keytar::GetPassword(service, 67 | account, 68 | &password, 69 | &error); 70 | if (result == keytar::FAIL_ERROR) { 71 | SetError(error.c_str()); 72 | } else if (result == keytar::FAIL_NONFATAL) { 73 | success = false; 74 | } else { 75 | success = true; 76 | } 77 | } 78 | 79 | void GetPasswordWorker::OnOK() { 80 | Napi::HandleScope scope(Env()); 81 | Napi::Value val = Env().Null(); 82 | if (success) { 83 | val = Napi::String::New(Env(), password.data(), 84 | password.length()); 85 | } 86 | deferred.Resolve(val); 87 | } 88 | 89 | void GetPasswordWorker::OnError(Napi::Error const &error) { 90 | Napi::HandleScope scope(Env()); 91 | deferred.Reject(error.Value()); 92 | } 93 | 94 | DeletePasswordWorker::DeletePasswordWorker( 95 | const std::string& service, 96 | const std::string& account, 97 | const Napi::Env &env 98 | ) : AsyncWorker(env), 99 | service(service), 100 | account(account), 101 | deferred(Napi::Promise::Deferred::New(env)) {} 102 | 103 | DeletePasswordWorker::~DeletePasswordWorker() {} 104 | 105 | Napi::Promise DeletePasswordWorker::Promise() { 106 | return deferred.Promise(); 107 | } 108 | 109 | void DeletePasswordWorker::Execute() { 110 | std::string error; 111 | KEYTAR_OP_RESULT result = keytar::DeletePassword(service, account, &error); 112 | if (result == keytar::FAIL_ERROR) { 113 | SetError(error.c_str()); 114 | } else if (result == keytar::FAIL_NONFATAL) { 115 | success = false; 116 | } else { 117 | success = true; 118 | } 119 | } 120 | 121 | void DeletePasswordWorker::OnOK() { 122 | Napi::HandleScope scope(Env()); 123 | deferred.Resolve(Napi::Boolean::New(Env(), success)); 124 | } 125 | 126 | void DeletePasswordWorker::OnError(Napi::Error const &error) { 127 | Napi::HandleScope scope(Env()); 128 | deferred.Reject(error.Value()); 129 | } 130 | 131 | FindPasswordWorker::FindPasswordWorker( 132 | const std::string& service, 133 | const Napi::Env &env 134 | ) : AsyncWorker(env), 135 | service(service), 136 | deferred(Napi::Promise::Deferred::New(env)) {} 137 | 138 | FindPasswordWorker::~FindPasswordWorker() {} 139 | 140 | Napi::Promise FindPasswordWorker::Promise() { 141 | return deferred.Promise(); 142 | } 143 | 144 | void FindPasswordWorker::Execute() { 145 | std::string error; 146 | KEYTAR_OP_RESULT result = keytar::FindPassword(service, 147 | &password, 148 | &error); 149 | if (result == keytar::FAIL_ERROR) { 150 | SetError(error.c_str()); 151 | } else if (result == keytar::FAIL_NONFATAL) { 152 | success = false; 153 | } else { 154 | success = true; 155 | } 156 | } 157 | 158 | void FindPasswordWorker::OnOK() { 159 | Napi::HandleScope scope(Env()); 160 | Napi::Value val = Env().Null(); 161 | if (success) { 162 | val = Napi::String::New(Env(), password.data(), 163 | password.length()); 164 | } 165 | deferred.Resolve(val); 166 | } 167 | 168 | void FindPasswordWorker::OnError(Napi::Error const &error) { 169 | Napi::HandleScope scope(Env()); 170 | deferred.Reject(error.Value()); 171 | } 172 | 173 | FindCredentialsWorker::FindCredentialsWorker( 174 | const std::string& service, 175 | const Napi::Env &env 176 | ) : AsyncWorker(env), 177 | service(service), 178 | deferred(Napi::Promise::Deferred::New(env)) {} 179 | 180 | FindCredentialsWorker::~FindCredentialsWorker() {} 181 | 182 | Napi::Promise FindCredentialsWorker::Promise() { 183 | return deferred.Promise(); 184 | } 185 | 186 | void FindCredentialsWorker::Execute() { 187 | std::string error; 188 | KEYTAR_OP_RESULT result = keytar::FindCredentials(service, 189 | &credentials, 190 | &error); 191 | if (result == keytar::FAIL_ERROR) { 192 | SetError(error.c_str()); 193 | } else if (result == keytar::FAIL_NONFATAL) { 194 | success = false; 195 | } else { 196 | success = true; 197 | } 198 | } 199 | 200 | void FindCredentialsWorker::OnOK() { 201 | Napi::HandleScope scope(Env()); 202 | Napi::Env env = Env(); 203 | 204 | if (success) { 205 | Napi::Array val = Napi::Array::New(env, credentials.size()); 206 | unsigned int idx = 0; 207 | std::vector::iterator it; 208 | for (it = credentials.begin(); it != credentials.end(); it++) { 209 | keytar::Credentials cred = *it; 210 | Napi::Object obj = Napi::Object::New(env); 211 | 212 | Napi::String account = Napi::String::New(env, 213 | cred.first.data(), 214 | cred.first.length()); 215 | 216 | Napi::String password = Napi::String::New(env, 217 | cred.second.data(), 218 | cred.second.length()); 219 | 220 | #ifndef _WIN32 221 | #pragma GCC diagnostic ignored "-Wunused-result" 222 | #endif 223 | obj.Set("account", account); 224 | #ifndef _WIN32 225 | #pragma GCC diagnostic ignored "-Wunused-result" 226 | #endif 227 | obj.Set("password", password); 228 | 229 | (val).Set(idx, obj); 230 | ++idx; 231 | } 232 | 233 | deferred.Resolve(val); 234 | } else { 235 | deferred.Resolve(Napi::Array::New(env, 0)); 236 | } 237 | } 238 | 239 | void FindCredentialsWorker::OnError(Napi::Error const &error) { 240 | Napi::HandleScope scope(Env()); 241 | deferred.Reject(error.Value()); 242 | } 243 | -------------------------------------------------------------------------------- /src/async.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_ASYNC_H_ 2 | #define SRC_ASYNC_H_ 3 | 4 | #include 5 | #include "napi.h" 6 | 7 | #include "credentials.h" 8 | 9 | class SetPasswordWorker : public Napi::AsyncWorker { 10 | public: 11 | SetPasswordWorker(const std::string& service, const std::string& account, const std::string& password, 12 | const Napi::Env &env); 13 | 14 | ~SetPasswordWorker(); 15 | 16 | void Execute(); 17 | void OnOK(); 18 | void OnError(Napi::Error const &error); 19 | Napi::Promise Promise(); 20 | 21 | private: 22 | const std::string service; 23 | const std::string account; 24 | const std::string password; 25 | Napi::Promise::Deferred deferred; 26 | }; 27 | 28 | class GetPasswordWorker : public Napi::AsyncWorker { 29 | public: 30 | GetPasswordWorker(const std::string& service, const std::string& account, 31 | const Napi::Env &env); 32 | 33 | ~GetPasswordWorker(); 34 | 35 | void Execute(); 36 | void OnOK(); 37 | void OnError(Napi::Error const &error); 38 | Napi::Promise Promise(); 39 | 40 | private: 41 | const std::string service; 42 | const std::string account; 43 | std::string password; 44 | bool success; 45 | const Napi::Promise::Deferred deferred; 46 | }; 47 | 48 | class DeletePasswordWorker : public Napi::AsyncWorker { 49 | public: 50 | DeletePasswordWorker(const std::string& service, const std::string& account, 51 | const Napi::Env &env); 52 | 53 | ~DeletePasswordWorker(); 54 | 55 | void Execute(); 56 | void OnOK(); 57 | void OnError(Napi::Error const &error); 58 | Napi::Promise Promise(); 59 | 60 | private: 61 | const std::string service; 62 | const std::string account; 63 | bool success; 64 | Napi::Promise::Deferred deferred; 65 | }; 66 | 67 | class FindPasswordWorker : public Napi::AsyncWorker { 68 | public: 69 | FindPasswordWorker(const std::string& service, const Napi::Env &env); 70 | 71 | ~FindPasswordWorker(); 72 | 73 | void Execute(); 74 | void OnOK(); 75 | void OnError(Napi::Error const &error); 76 | Napi::Promise Promise(); 77 | 78 | private: 79 | const std::string service; 80 | std::string password; 81 | bool success; 82 | const Napi::Promise::Deferred deferred; 83 | }; 84 | 85 | class FindCredentialsWorker : public Napi::AsyncWorker { 86 | public: 87 | FindCredentialsWorker(const std::string& service, const Napi::Env &env); 88 | 89 | ~FindCredentialsWorker(); 90 | 91 | void Execute(); 92 | void OnOK(); 93 | void OnError(Napi::Error const &error); 94 | Napi::Promise Promise(); 95 | 96 | private: 97 | const std::string service; 98 | std::vector credentials; 99 | bool success; 100 | const Napi::Promise::Deferred deferred; 101 | }; 102 | 103 | #endif // SRC_ASYNC_H_ 104 | -------------------------------------------------------------------------------- /src/credentials.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_CREDENTIALS_H_ 2 | #define SRC_CREDENTIALS_H_ 3 | 4 | #include 5 | #include 6 | 7 | namespace keytar { 8 | 9 | typedef std::pair Credentials; 10 | 11 | } 12 | 13 | #endif // SRC_CREDENTIALS_H_ 14 | -------------------------------------------------------------------------------- /src/keytar.h: -------------------------------------------------------------------------------- 1 | #ifndef SRC_KEYTAR_H_ 2 | #define SRC_KEYTAR_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include "credentials.h" 8 | 9 | namespace keytar { 10 | 11 | enum KEYTAR_OP_RESULT { 12 | SUCCESS, 13 | FAIL_ERROR, 14 | FAIL_NONFATAL 15 | }; 16 | 17 | KEYTAR_OP_RESULT SetPassword(const std::string& service, 18 | const std::string& account, 19 | const std::string& password, 20 | std::string* error); 21 | 22 | KEYTAR_OP_RESULT GetPassword(const std::string& service, 23 | const std::string& account, 24 | std::string* password, 25 | std::string* error); 26 | 27 | KEYTAR_OP_RESULT DeletePassword(const std::string& service, 28 | const std::string& account, 29 | std::string* error); 30 | 31 | KEYTAR_OP_RESULT FindPassword(const std::string& service, 32 | std::string* password, 33 | std::string* error); 34 | 35 | KEYTAR_OP_RESULT FindCredentials(const std::string& service, 36 | std::vector*, 37 | std::string* error); 38 | 39 | } // namespace keytar 40 | 41 | #endif // SRC_KEYTAR_H_ 42 | -------------------------------------------------------------------------------- /src/keytar_mac.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "keytar.h" 3 | #include "credentials.h" 4 | 5 | 6 | namespace keytar { 7 | 8 | /** 9 | * Converts a CFString to a std::string 10 | * 11 | * This either uses CFStringGetCStringPtr or (if that fails) 12 | * CFStringGetCString, trying to be as efficient as possible. 13 | */ 14 | const std::string CFStringToStdString(CFStringRef cfstring) { 15 | const char* cstr = CFStringGetCStringPtr(cfstring, kCFStringEncodingUTF8); 16 | 17 | if (cstr != NULL) { 18 | return std::string(cstr); 19 | } 20 | 21 | CFIndex length = CFStringGetLength(cfstring); 22 | // Worst case: 2 bytes per character + NUL 23 | CFIndex cstrPtrLen = length * 2 + 1; 24 | char* cstrPtr = static_cast(malloc(cstrPtrLen)); 25 | 26 | Boolean result = CFStringGetCString(cfstring, 27 | cstrPtr, 28 | cstrPtrLen, 29 | kCFStringEncodingUTF8); 30 | 31 | std::string stdstring; 32 | if (result) { 33 | stdstring = std::string(cstrPtr); 34 | } 35 | 36 | free(cstrPtr); 37 | 38 | return stdstring; 39 | } 40 | 41 | const std::string errorStatusToString(OSStatus status) { 42 | std::string errorStr; 43 | CFStringRef errorMessageString = SecCopyErrorMessageString(status, NULL); 44 | 45 | const char* errorCStringPtr = CFStringGetCStringPtr(errorMessageString, 46 | kCFStringEncodingUTF8); 47 | if (errorCStringPtr) { 48 | errorStr = std::string(errorCStringPtr); 49 | } else { 50 | errorStr = std::string("An unknown error occurred."); 51 | } 52 | 53 | CFRelease(errorMessageString); 54 | return errorStr; 55 | } 56 | 57 | KEYTAR_OP_RESULT AddPassword(const std::string& service, 58 | const std::string& account, 59 | const std::string& password, 60 | std::string* error) { 61 | OSStatus status = SecKeychainAddGenericPassword(NULL, 62 | service.length(), 63 | service.data(), 64 | account.length(), 65 | account.data(), 66 | password.length(), 67 | password.data(), 68 | NULL); 69 | 70 | if (status != errSecSuccess) { 71 | *error = errorStatusToString(status); 72 | return FAIL_ERROR; 73 | } 74 | 75 | return SUCCESS; 76 | } 77 | 78 | KEYTAR_OP_RESULT SetPassword(const std::string& service, 79 | const std::string& account, 80 | const std::string& password, 81 | std::string* error) { 82 | SecKeychainItemRef item; 83 | OSStatus result = SecKeychainFindGenericPassword(NULL, 84 | service.length(), 85 | service.data(), 86 | account.length(), 87 | account.data(), 88 | NULL, 89 | NULL, 90 | &item); 91 | 92 | if (result == errSecItemNotFound) { 93 | return AddPassword(service, account, password, error); 94 | } else if (result != errSecSuccess) { 95 | *error = errorStatusToString(result); 96 | return FAIL_ERROR; 97 | } 98 | 99 | result = SecKeychainItemModifyAttributesAndData(item, 100 | NULL, 101 | password.length(), 102 | password.data()); 103 | CFRelease(item); 104 | if (result != errSecSuccess) { 105 | *error = errorStatusToString(result); 106 | return FAIL_ERROR; 107 | } 108 | 109 | return SUCCESS; 110 | } 111 | 112 | KEYTAR_OP_RESULT GetPassword(const std::string& service, 113 | const std::string& account, 114 | std::string* password, 115 | std::string* error) { 116 | void *data; 117 | UInt32 length; 118 | OSStatus status = SecKeychainFindGenericPassword(NULL, 119 | service.length(), 120 | service.data(), 121 | account.length(), 122 | account.data(), 123 | &length, 124 | &data, 125 | NULL); 126 | 127 | if (status == errSecItemNotFound) { 128 | return FAIL_NONFATAL; 129 | } else if (status != errSecSuccess) { 130 | *error = errorStatusToString(status); 131 | return FAIL_ERROR; 132 | } 133 | 134 | *password = std::string(reinterpret_cast(data), length); 135 | SecKeychainItemFreeContent(NULL, data); 136 | return SUCCESS; 137 | } 138 | 139 | KEYTAR_OP_RESULT DeletePassword(const std::string& service, 140 | const std::string& account, 141 | std::string* error) { 142 | SecKeychainItemRef item; 143 | OSStatus status = SecKeychainFindGenericPassword(NULL, 144 | service.length(), 145 | service.data(), 146 | account.length(), 147 | account.data(), 148 | NULL, 149 | NULL, 150 | &item); 151 | if (status == errSecItemNotFound) { 152 | // Item could not be found, so already deleted. 153 | return FAIL_NONFATAL; 154 | } else if (status != errSecSuccess) { 155 | *error = errorStatusToString(status); 156 | return FAIL_ERROR; 157 | } 158 | 159 | status = SecKeychainItemDelete(item); 160 | CFRelease(item); 161 | if (status != errSecSuccess) { 162 | *error = errorStatusToString(status); 163 | return FAIL_ERROR; 164 | } 165 | 166 | return SUCCESS; 167 | } 168 | 169 | KEYTAR_OP_RESULT FindPassword(const std::string& service, 170 | std::string* password, 171 | std::string* error) { 172 | SecKeychainItemRef item; 173 | void *data; 174 | UInt32 length; 175 | 176 | OSStatus status = SecKeychainFindGenericPassword(NULL, 177 | service.length(), 178 | service.data(), 179 | 0, 180 | NULL, 181 | &length, 182 | &data, 183 | &item); 184 | if (status == errSecItemNotFound) { 185 | return FAIL_NONFATAL; 186 | } else if (status != errSecSuccess) { 187 | *error = errorStatusToString(status); 188 | return FAIL_ERROR; 189 | } 190 | 191 | *password = std::string(reinterpret_cast(data), length); 192 | SecKeychainItemFreeContent(NULL, data); 193 | CFRelease(item); 194 | return SUCCESS; 195 | } 196 | 197 | Credentials getCredentialsForItem(CFDictionaryRef item) { 198 | CFStringRef service = (CFStringRef) CFDictionaryGetValue(item, 199 | kSecAttrService); 200 | CFStringRef account = (CFStringRef) CFDictionaryGetValue(item, 201 | kSecAttrAccount); 202 | 203 | CFMutableDictionaryRef query = CFDictionaryCreateMutable( 204 | NULL, 205 | 0, 206 | &kCFTypeDictionaryKeyCallBacks, 207 | &kCFTypeDictionaryValueCallBacks); 208 | 209 | CFDictionaryAddValue(query, kSecAttrService, service); 210 | CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword); 211 | CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitOne); 212 | CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue); 213 | CFDictionaryAddValue(query, kSecReturnData, kCFBooleanTrue); 214 | CFDictionaryAddValue(query, kSecAttrAccount, account); 215 | 216 | Credentials cred; 217 | CFTypeRef result = NULL; 218 | OSStatus status = SecItemCopyMatching((CFDictionaryRef) query, &result); 219 | 220 | CFRelease(query); 221 | 222 | if (status == errSecSuccess) { 223 | CFDataRef passwordData = (CFDataRef) CFDictionaryGetValue( 224 | (CFDictionaryRef) result, 225 | CFSTR("v_Data")); 226 | CFStringRef password = CFStringCreateFromExternalRepresentation( 227 | NULL, 228 | passwordData, 229 | kCFStringEncodingUTF8); 230 | 231 | cred = Credentials( 232 | CFStringToStdString(account), 233 | CFStringToStdString(password)); 234 | 235 | CFRelease(password); 236 | } 237 | 238 | if (result != NULL) { 239 | CFRelease(result); 240 | } 241 | 242 | return cred; 243 | } 244 | 245 | KEYTAR_OP_RESULT FindCredentials(const std::string& service, 246 | std::vector* credentials, 247 | std::string* error) { 248 | CFStringRef serviceStr = CFStringCreateWithCString( 249 | NULL, 250 | service.c_str(), 251 | kCFStringEncodingUTF8); 252 | 253 | CFMutableDictionaryRef query = CFDictionaryCreateMutable( 254 | NULL, 255 | 0, 256 | &kCFTypeDictionaryKeyCallBacks, 257 | &kCFTypeDictionaryValueCallBacks); 258 | CFDictionaryAddValue(query, kSecClass, kSecClassGenericPassword); 259 | CFDictionaryAddValue(query, kSecAttrService, serviceStr); 260 | CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll); 261 | CFDictionaryAddValue(query, kSecReturnRef, kCFBooleanTrue); 262 | CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue); 263 | 264 | CFTypeRef result = NULL; 265 | OSStatus status = SecItemCopyMatching((CFDictionaryRef) query, &result); 266 | 267 | CFRelease(serviceStr); 268 | CFRelease(query); 269 | 270 | if (status == errSecSuccess) { 271 | CFArrayRef resultArray = (CFArrayRef) result; 272 | int resultCount = CFArrayGetCount(resultArray); 273 | 274 | for (int idx = 0; idx < resultCount; idx++) { 275 | CFDictionaryRef item = (CFDictionaryRef) CFArrayGetValueAtIndex( 276 | resultArray, 277 | idx); 278 | 279 | Credentials cred = getCredentialsForItem(item); 280 | credentials->push_back(cred); 281 | } 282 | } else if (status == errSecItemNotFound) { 283 | return FAIL_NONFATAL; 284 | } else { 285 | *error = errorStatusToString(status); 286 | return FAIL_ERROR; 287 | } 288 | 289 | if (result != NULL) { 290 | CFRelease(result); 291 | } 292 | 293 | return SUCCESS; 294 | } 295 | 296 | } // namespace keytar 297 | -------------------------------------------------------------------------------- /src/keytar_posix.cc: -------------------------------------------------------------------------------- 1 | #include "keytar.h" 2 | 3 | // This is needed to make the builds on Ubuntu 14.04 / libsecret v0.16 work. 4 | // The API we use has already stabilized. 5 | #define SECRET_API_SUBJECT_TO_CHANGE 6 | #include 7 | #include 8 | #include 9 | 10 | namespace keytar { 11 | 12 | namespace { 13 | 14 | static const SecretSchema schema = { 15 | "org.freedesktop.Secret.Generic", SECRET_SCHEMA_NONE, { 16 | { "service", SECRET_SCHEMA_ATTRIBUTE_STRING }, 17 | { "account", SECRET_SCHEMA_ATTRIBUTE_STRING } 18 | } 19 | }; 20 | 21 | } // namespace 22 | 23 | KEYTAR_OP_RESULT SetPassword(const std::string& service, 24 | const std::string& account, 25 | const std::string& password, 26 | std::string* errStr) { 27 | GError* error = NULL; 28 | 29 | secret_password_store_sync( 30 | &schema, // The schema. 31 | SECRET_COLLECTION_DEFAULT, // Default collection. 32 | (service + "/" + account).c_str(), // The label. 33 | password.c_str(), // The password. 34 | NULL, // Cancellable. (unneeded) 35 | &error, // Reference to the error. 36 | "service", service.c_str(), 37 | "account", account.c_str(), 38 | NULL); // End of arguments. 39 | 40 | if (error != NULL) { 41 | *errStr = std::string(error->message); 42 | g_error_free(error); 43 | return FAIL_ERROR; 44 | } 45 | 46 | return SUCCESS; 47 | } 48 | 49 | KEYTAR_OP_RESULT GetPassword(const std::string& service, 50 | const std::string& account, 51 | std::string* password, 52 | std::string* errStr) { 53 | GError* error = NULL; 54 | 55 | gchar* raw_password = secret_password_lookup_sync( 56 | &schema, // The schema. 57 | NULL, // Cancellable. (unneeded) 58 | &error, // Reference to the error. 59 | "service", service.c_str(), 60 | "account", account.c_str(), 61 | NULL); // End of arguments. 62 | 63 | if (error != NULL) { 64 | *errStr = std::string(error->message); 65 | g_error_free(error); 66 | return FAIL_ERROR; 67 | } 68 | 69 | if (raw_password == NULL) 70 | return FAIL_NONFATAL; 71 | 72 | *password = raw_password; 73 | secret_password_free(raw_password); 74 | return SUCCESS; 75 | } 76 | 77 | KEYTAR_OP_RESULT DeletePassword(const std::string& service, 78 | const std::string& account, 79 | std::string* errStr) { 80 | GError* error = NULL; 81 | 82 | gboolean result = secret_password_clear_sync( 83 | &schema, // The schema. 84 | NULL, // Cancellable. (unneeded) 85 | &error, // Reference to the error. 86 | "service", service.c_str(), 87 | "account", account.c_str(), 88 | NULL); // End of arguments. 89 | 90 | if (error != NULL) { 91 | *errStr = std::string(error->message); 92 | g_error_free(error); 93 | return FAIL_ERROR; 94 | } 95 | 96 | if (!result) 97 | return FAIL_NONFATAL; 98 | 99 | return SUCCESS; 100 | } 101 | 102 | KEYTAR_OP_RESULT FindPassword(const std::string& service, 103 | std::string* password, 104 | std::string* errStr) { 105 | GError* error = NULL; 106 | 107 | gchar* raw_password = secret_password_lookup_sync( 108 | &schema, // The schema. 109 | NULL, // Cancellable. (unneeded) 110 | &error, // Reference to the error. 111 | "service", service.c_str(), 112 | NULL); // End of arguments. 113 | 114 | if (error != NULL) { 115 | *errStr = std::string(error->message); 116 | g_error_free(error); 117 | return FAIL_ERROR; 118 | } 119 | 120 | if (raw_password == NULL) 121 | return FAIL_NONFATAL; 122 | 123 | *password = raw_password; 124 | secret_password_free(raw_password); 125 | return SUCCESS; 126 | } 127 | 128 | KEYTAR_OP_RESULT FindCredentials(const std::string& service, 129 | std::vector* credentials, 130 | std::string* errStr) { 131 | GError* error = NULL; 132 | 133 | GHashTable* attributes = g_hash_table_new(NULL, NULL); 134 | g_hash_table_replace(attributes, 135 | (gpointer) "service", 136 | (gpointer) service.c_str()); 137 | 138 | GList* items = secret_service_search_sync( 139 | NULL, 140 | &schema, // The schema. 141 | attributes, 142 | static_cast(SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK | 143 | SECRET_SEARCH_LOAD_SECRETS), 144 | NULL, // Cancellable. (unneeded) 145 | &error); // Reference to the error. 146 | 147 | g_hash_table_destroy(attributes); 148 | 149 | if (error != NULL) { 150 | *errStr = std::string(error->message); 151 | g_error_free(error); 152 | return FAIL_ERROR; 153 | } 154 | 155 | GList* current = items; 156 | for (current = items; current != NULL; current = current->next) { 157 | SecretItem* item = reinterpret_cast(current->data); 158 | 159 | GHashTable* itemAttrs = secret_item_get_attributes(item); 160 | char* account = strdup( 161 | reinterpret_cast(g_hash_table_lookup(itemAttrs, "account"))); 162 | 163 | SecretValue* secret = secret_item_get_secret(item); 164 | char* password = strdup(secret_value_get_text(secret)); 165 | 166 | if (account == NULL || password == NULL) { 167 | if (account) 168 | free(account); 169 | 170 | if (password) 171 | free(password); 172 | 173 | continue; 174 | } 175 | 176 | credentials->push_back(Credentials(account, password)); 177 | free(account); 178 | free(password); 179 | } 180 | 181 | return SUCCESS; 182 | } 183 | 184 | } // namespace keytar 185 | -------------------------------------------------------------------------------- /src/keytar_win.cc: -------------------------------------------------------------------------------- 1 | #include "keytar.h" 2 | 3 | #define UNICODE 4 | 5 | #include 6 | #include 7 | 8 | #include "credentials.h" 9 | 10 | namespace keytar { 11 | 12 | LPWSTR utf8ToWideChar(std::string utf8) { 13 | int wide_char_length = MultiByteToWideChar(CP_UTF8, 14 | 0, 15 | utf8.c_str(), 16 | -1, 17 | NULL, 18 | 0); 19 | if (wide_char_length == 0) { 20 | return NULL; 21 | } 22 | 23 | LPWSTR result = new WCHAR[wide_char_length]; 24 | if (MultiByteToWideChar(CP_UTF8, 25 | 0, 26 | utf8.c_str(), 27 | -1, 28 | result, 29 | wide_char_length) == 0) { 30 | delete[] result; 31 | return NULL; 32 | } 33 | 34 | return result; 35 | } 36 | 37 | std::string wideCharToAnsi(LPWSTR wide_char) { 38 | if (wide_char == NULL) { 39 | return std::string(); 40 | } 41 | 42 | int ansi_length = WideCharToMultiByte(CP_ACP, 43 | 0, 44 | wide_char, 45 | -1, 46 | NULL, 47 | 0, 48 | NULL, 49 | NULL); 50 | if (ansi_length == 0) { 51 | return std::string(); 52 | } 53 | 54 | char* buffer = new char[ansi_length]; 55 | if (WideCharToMultiByte(CP_ACP, 56 | 0, 57 | wide_char, 58 | -1, 59 | buffer, 60 | ansi_length, 61 | NULL, 62 | NULL) == 0) { 63 | delete[] buffer; 64 | return std::string(); 65 | } 66 | 67 | std::string result = std::string(buffer); 68 | delete[] buffer; 69 | return result; 70 | } 71 | 72 | std::string wideCharToUtf8(LPWSTR wide_char) { 73 | if (wide_char == NULL) { 74 | return std::string(); 75 | } 76 | 77 | int utf8_length = WideCharToMultiByte(CP_UTF8, 78 | 0, 79 | wide_char, 80 | -1, 81 | NULL, 82 | 0, 83 | NULL, 84 | NULL); 85 | if (utf8_length == 0) { 86 | return std::string(); 87 | } 88 | 89 | char* buffer = new char[utf8_length]; 90 | if (WideCharToMultiByte(CP_UTF8, 91 | 0, 92 | wide_char, 93 | -1, 94 | buffer, 95 | utf8_length, 96 | NULL, 97 | NULL) == 0) { 98 | delete[] buffer; 99 | return std::string(); 100 | } 101 | 102 | std::string result = std::string(buffer); 103 | delete[] buffer; 104 | return result; 105 | } 106 | 107 | std::string getErrorMessage(DWORD errorCode) { 108 | LPWSTR errBuffer; 109 | ::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, 110 | NULL, errorCode, 0, (LPWSTR) &errBuffer, 0, NULL); 111 | std::string errMsg = wideCharToAnsi(errBuffer); 112 | LocalFree(errBuffer); 113 | return errMsg; 114 | } 115 | 116 | KEYTAR_OP_RESULT SetPassword(const std::string& service, 117 | const std::string& account, 118 | const std::string& password, 119 | std::string* errStr) { 120 | LPWSTR target_name = utf8ToWideChar(service + '/' + account); 121 | if (target_name == NULL) { 122 | return FAIL_ERROR; 123 | } 124 | 125 | LPWSTR user_name = utf8ToWideChar(account); 126 | if (user_name == NULL) { 127 | return FAIL_ERROR; 128 | } 129 | 130 | CREDENTIAL cred = { 0 }; 131 | cred.Type = CRED_TYPE_GENERIC; 132 | cred.TargetName = target_name; 133 | cred.UserName = user_name; 134 | cred.CredentialBlobSize = password.size(); 135 | cred.CredentialBlob = (LPBYTE)(password.data()); 136 | cred.Persist = CRED_PERSIST_ENTERPRISE; 137 | 138 | bool result = ::CredWrite(&cred, 0); 139 | delete[] target_name; 140 | if (!result) { 141 | *errStr = getErrorMessage(::GetLastError()); 142 | return FAIL_ERROR; 143 | } else { 144 | return SUCCESS; 145 | } 146 | } 147 | 148 | KEYTAR_OP_RESULT GetPassword(const std::string& service, 149 | const std::string& account, 150 | std::string* password, 151 | std::string* errStr) { 152 | LPWSTR target_name = utf8ToWideChar(service + '/' + account); 153 | if (target_name == NULL) { 154 | return FAIL_ERROR; 155 | } 156 | 157 | CREDENTIAL* cred; 158 | bool result = ::CredRead(target_name, CRED_TYPE_GENERIC, 0, &cred); 159 | delete[] target_name; 160 | if (!result) { 161 | DWORD code = ::GetLastError(); 162 | if (code == ERROR_NOT_FOUND) { 163 | return FAIL_NONFATAL; 164 | } else { 165 | *errStr = getErrorMessage(code); 166 | return FAIL_ERROR; 167 | } 168 | } 169 | 170 | *password = std::string(reinterpret_cast(cred->CredentialBlob), 171 | cred->CredentialBlobSize); 172 | ::CredFree(cred); 173 | return SUCCESS; 174 | } 175 | 176 | KEYTAR_OP_RESULT DeletePassword(const std::string& service, 177 | const std::string& account, 178 | std::string* errStr) { 179 | LPWSTR target_name = utf8ToWideChar(service + '/' + account); 180 | if (target_name == NULL) { 181 | return FAIL_ERROR; 182 | } 183 | 184 | bool result = ::CredDelete(target_name, CRED_TYPE_GENERIC, 0); 185 | delete[] target_name; 186 | if (!result) { 187 | DWORD code = ::GetLastError(); 188 | if (code == ERROR_NOT_FOUND) { 189 | return FAIL_NONFATAL; 190 | } else { 191 | *errStr = getErrorMessage(code); 192 | return FAIL_ERROR; 193 | } 194 | } 195 | 196 | return SUCCESS; 197 | } 198 | 199 | KEYTAR_OP_RESULT FindPassword(const std::string& service, 200 | std::string* password, 201 | std::string* errStr) { 202 | LPWSTR filter = utf8ToWideChar(service + "*"); 203 | if (filter == NULL) { 204 | return FAIL_ERROR; 205 | } 206 | 207 | DWORD count; 208 | CREDENTIAL** creds; 209 | bool result = ::CredEnumerate(filter, 0, &count, &creds); 210 | delete[] filter; 211 | if (!result) { 212 | DWORD code = ::GetLastError(); 213 | if (code == ERROR_NOT_FOUND) { 214 | return FAIL_NONFATAL; 215 | } else { 216 | *errStr = getErrorMessage(code); 217 | return FAIL_ERROR; 218 | } 219 | } 220 | 221 | *password = std::string(reinterpret_cast(creds[0]->CredentialBlob), 222 | creds[0]->CredentialBlobSize); 223 | ::CredFree(creds); 224 | return SUCCESS; 225 | } 226 | 227 | KEYTAR_OP_RESULT FindCredentials(const std::string& service, 228 | std::vector* credentials, 229 | std::string* errStr) { 230 | LPWSTR filter = utf8ToWideChar(service + "*"); 231 | if (filter == NULL) { 232 | *errStr = "Error generating credential filter"; 233 | return FAIL_ERROR; 234 | } 235 | 236 | DWORD count; 237 | CREDENTIAL **creds; 238 | 239 | bool result = ::CredEnumerate(filter, 0, &count, &creds); 240 | if (!result) { 241 | DWORD code = ::GetLastError(); 242 | if (code == ERROR_NOT_FOUND) { 243 | return FAIL_NONFATAL; 244 | } else { 245 | *errStr = getErrorMessage(code); 246 | return FAIL_ERROR; 247 | } 248 | } 249 | 250 | for (unsigned int i = 0; i < count; ++i) { 251 | CREDENTIAL* cred = creds[i]; 252 | 253 | if (cred->UserName == NULL || cred->CredentialBlobSize == NULL) { 254 | continue; 255 | } 256 | 257 | std::string login = wideCharToUtf8(cred->UserName); 258 | std::string password( 259 | reinterpret_cast( 260 | cred->CredentialBlob), 261 | cred->CredentialBlobSize); 262 | 263 | credentials->push_back(Credentials(login, password)); 264 | } 265 | 266 | CredFree(creds); 267 | 268 | return SUCCESS; 269 | } 270 | 271 | 272 | } // namespace keytar 273 | -------------------------------------------------------------------------------- /src/main.cc: -------------------------------------------------------------------------------- 1 | #include "napi.h" 2 | #include "async.h" 3 | 4 | namespace { 5 | 6 | Napi::Value SetPassword(const Napi::CallbackInfo& info) { 7 | Napi::Env env = info.Env(); 8 | 9 | if (!info[0].IsString()) { 10 | Napi::TypeError::New(env, "Parameter 'service' must be a string"). 11 | ThrowAsJavaScriptException(); 12 | return env.Null(); 13 | } 14 | 15 | std::string service = info[0].As(); 16 | 17 | if (!info[1].IsString()) { 18 | Napi::TypeError::New(env, "Parameter 'username' must be a string"). 19 | ThrowAsJavaScriptException(); 20 | return env.Null(); 21 | } 22 | 23 | std::string username = info[1].As(); 24 | 25 | if (!info[2].IsString()) { 26 | Napi::TypeError::New(env, "Parameter 'password' must be a string"). 27 | ThrowAsJavaScriptException(); 28 | return env.Null(); 29 | } 30 | 31 | std::string password = info[2].As(); 32 | 33 | SetPasswordWorker* worker = new SetPasswordWorker( 34 | service, 35 | username, 36 | password, 37 | env); 38 | worker->Queue(); 39 | return worker->Promise(); 40 | } 41 | 42 | Napi::Value GetPassword(const Napi::CallbackInfo& info) { 43 | Napi::Env env = info.Env(); 44 | if (!info[0].IsString()) { 45 | Napi::TypeError::New(env, "Parameter 'service' must be a string"). 46 | ThrowAsJavaScriptException(); 47 | return env.Null(); 48 | } 49 | 50 | std::string service = info[0].As(); 51 | 52 | if (!info[1].IsString()) { 53 | Napi::TypeError::New(env, "Parameter 'username' must be a string"). 54 | ThrowAsJavaScriptException(); 55 | return env.Null(); 56 | } 57 | 58 | std::string username = info[1].As(); 59 | 60 | GetPasswordWorker* worker = new GetPasswordWorker( 61 | service, 62 | username, 63 | env); 64 | worker->Queue(); 65 | return worker->Promise(); 66 | } 67 | 68 | Napi::Value DeletePassword(const Napi::CallbackInfo& info) { 69 | Napi::Env env = info.Env(); 70 | if (!info[0].IsString()) { 71 | Napi::TypeError::New(env, "Parameter 'service' must be a string"). 72 | ThrowAsJavaScriptException(); 73 | return env.Null(); 74 | } 75 | 76 | std::string service = info[0].As(); 77 | 78 | if (!info[1].IsString()) { 79 | Napi::TypeError::New(env, "Parameter 'username' must be a string"). 80 | ThrowAsJavaScriptException(); 81 | return env.Null(); 82 | } 83 | 84 | std::string username = info[1].As(); 85 | 86 | DeletePasswordWorker *worker = new DeletePasswordWorker( 87 | service, 88 | username, 89 | env); 90 | worker->Queue(); 91 | return worker->Promise(); 92 | } 93 | 94 | Napi::Value FindPassword(const Napi::CallbackInfo& info) { 95 | Napi::Env env = info.Env(); 96 | if (!info[0].IsString()) { 97 | Napi::TypeError::New(env, "Parameter 'service' must be a string"). 98 | ThrowAsJavaScriptException(); 99 | return env.Null(); 100 | } 101 | 102 | std::string service = info[0].As(); 103 | 104 | FindPasswordWorker* worker = new FindPasswordWorker( 105 | service, 106 | env); 107 | worker->Queue(); 108 | return worker->Promise(); 109 | } 110 | 111 | Napi::Value FindCredentials(const Napi::CallbackInfo& info) { 112 | Napi::Env env = info.Env(); 113 | if (!info[0].IsString()) { 114 | Napi::TypeError::New(env, "Parameter 'service' must be a string"). 115 | ThrowAsJavaScriptException(); 116 | return env.Null(); 117 | } 118 | 119 | std::string service = info[0].As(); 120 | 121 | FindCredentialsWorker* worker = new FindCredentialsWorker( 122 | service, 123 | env); 124 | worker->Queue(); 125 | return worker->Promise(); 126 | } 127 | 128 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 129 | exports.Set("getPassword", Napi::Function::New(env, GetPassword)); 130 | exports.Set("setPassword", Napi::Function::New(env, SetPassword)); 131 | exports.Set("deletePassword", Napi::Function::New(env, DeletePassword)); 132 | exports.Set("findPassword", Napi::Function::New(env, FindPassword)); 133 | exports.Set("findCredentials", Napi::Function::New(env, FindCredentials)); 134 | return exports; 135 | } 136 | 137 | } // namespace 138 | 139 | NODE_API_MODULE(keytar, Init) 140 | --------------------------------------------------------------------------------