Skip to content

Commit 4c26a86

Browse files
committed
Initial commit
1 parent d29522c commit 4c26a86

12 files changed

+1310
-2
lines changed

.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Deno
2+
.cache
3+
deps
4+
deno-test-results.xml
5+
6+
# TypeScript
7+
*.tsbuildinfo
8+
9+
# Node.js
10+
node_modules
11+
npm-debug.log
12+
yarn-error.log
13+
14+
# Editor/IDE
15+
.vscode
16+
.idea
17+
*.sublime-project
18+
*.sublime-workspace
19+
20+
# Build output
21+
dist
22+
23+
# Other
24+
.psd

README.md

+166-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,166 @@
1-
# apw
2-
A CLI for Apple Passwords.
1+
<a name="readme-top"></a>
2+
<div align="center">
3+
<a href="https://github.com/bendews/apw">
4+
<img src="icon.png" alt="Logo" width="80" height="80">
5+
</a>
6+
7+
<h3 align="center">Apple Passwords CLI</h3>
8+
9+
<p align="center">
10+
A CLI for access to Apple Passwords. A foundation for enabling integration and automation.
11+
<br />
12+
<a href="https://github.com/bendews/apw"><strong>Explore the docs »</strong></a>
13+
<br />
14+
<br />
15+
<a href="https://github.com/bendews/apw">View Demo</a>
16+
·
17+
<a href="https://github.com/bendews/apw/issues">Report Bug</a>
18+
·
19+
<a href="https://github.com/bendews/apw/issues">Request Feature</a>
20+
</p>
21+
22+
[![Contributors][contributors-shield]][contributors-url]
23+
[![Forks][forks-shield]][forks-url]
24+
[![Stargazers][stars-shield]][stars-url]
25+
[![Issues][issues-shield]][issues-url]
26+
[![MIT License][license-shield]][license-url]
27+
<br />
28+
</div>
29+
30+
<details>
31+
<summary>Table of Contents</summary>
32+
<ol>
33+
<li>
34+
<a href="#about-the-project">About The Project</a>
35+
</li>
36+
<li>
37+
<a href="#getting-started">Getting Started</a>
38+
<ul>
39+
<li><a href="#installation">Installation</a></li>
40+
</ul>
41+
</li>
42+
<li><a href="#usage">Usage</a></li>
43+
<li><a href="#contributing">Contributing</a></li>
44+
<li><a href="#license">License</a></li>
45+
<li><a href="#contact">Contact</a></li>
46+
<li><a href="#acknowledgments">Acknowledgments</a></li>
47+
</ol>
48+
</details>
49+
50+
51+
52+
<!-- ABOUT THE PROJECT -->
53+
## About The Project
54+
55+
This project introduces a CLI interface designed to access iCloud passwords and OTP tokens. The core objective is to provide a secure and straightforward way to retrieve iCloud passwords, facilitating integration with other systems or for personal convenience.
56+
57+
It utilises a built in helper tool in macOS 14 and above to facilitate this functionality.
58+
59+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
60+
61+
62+
## Getting Started
63+
64+
Ensure homebrew is installed or build from source.
65+
66+
### Installation
67+
68+
Installation is via Homebrew:
69+
70+
```
71+
brew install apw
72+
```
73+
74+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
75+
76+
## Usage
77+
78+
Enable the daemon to run on startup:
79+
80+
`brew enable apw`
81+
82+
Authenticate the daemon interactively:
83+
84+
_This is required every time the daemon starts i.e on boot_
85+
86+
`apw auth`
87+
88+
Query for available passwords:
89+
90+
`apw pw list google.com`
91+
92+
View more commands & help:
93+
94+
`apw --help`
95+
96+
```shell
97+
Options:
98+
99+
-h, --help - Show this help.
100+
-V, --version - Show the version number for this program.
101+
102+
Commands:
103+
104+
auth - Authenticate CLI with daemon.
105+
pw - Interactively list accounts/passwords.
106+
otp - Interactively list accounts/OTPs.
107+
start - Start the daemon.
108+
```
109+
110+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
111+
112+
113+
<!-- CONTRIBUTING -->
114+
## Contributing
115+
116+
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
117+
118+
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
119+
Don't forget to give the project a star! Thanks again!
120+
121+
1. Fork the Project
122+
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
123+
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
124+
4. Push to the Branch (`git push origin feature/AmazingFeature`)
125+
5. Open a Pull Request
126+
127+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
128+
129+
## License
130+
131+
Distributed under the GPL V3.0 License. See `LICENSE` for more information.
132+
133+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
134+
135+
## Contact
136+
137+
Ben Dews - [#](https://bendews.com)
138+
139+
Project Link: [https://github.com/bendews/apw](https://github.com/bendews/apw)
140+
141+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
142+
143+
144+
145+
<!-- ACKNOWLEDGMENTS -->
146+
## Acknowledgments
147+
148+
* [au2001 - iCloud Passwords for Firefox](https://github.com/au2001/icloud-passwords-firefox) - their SRP implementation was _so_ much better than mine it's embarassing. It is now based on their far superior implementation.
149+
150+
<p align="right">(<a href="#readme-top">back to top</a>)</p>
151+
152+
153+
154+
<!-- MARKDOWN LINKS & IMAGES -->
155+
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
156+
[contributors-shield]: https://img.shields.io/github/contributors/bendews/apw.svg?style=for-the-badge
157+
[contributors-url]: https://github.com/bendews/apw/graphs/contributors
158+
[forks-shield]: https://img.shields.io/github/forks/bendews/apw.svg?style=for-the-badge
159+
[forks-url]: https://github.com/bendews/apw/network/members
160+
[stars-shield]: https://img.shields.io/github/stars/bendews/apw.svg?style=for-the-badge
161+
[stars-url]: https://github.com/bendews/apw/stargazers
162+
[issues-shield]: https://img.shields.io/github/issues/bendews/apw.svg?style=for-the-badge
163+
[issues-url]: https://github.com/bendews/apw/issues
164+
[license-shield]: https://img.shields.io/github/license/bendews/apw.svg?style=for-the-badge
165+
[license-url]: https://github.com/bendews/apw/blob/master/LICENSE.txt
166+
[product-screenshot]: images/screenshot.png

icon.png

14.6 KB
Loading

package.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "apw",
3+
"version": "0.1.0",
4+
"description": "A CLI for Apple Passwords (APW) on MacOS",
5+
"dependencies": {
6+
"deno": "^0.1.1"
7+
},
8+
"devDependencies": {
9+
"@typescript-eslint/eslint-plugin": "^6.21.0",
10+
"eslint": "^8.57.0",
11+
"eslint-config-standard-with-typescript": "^43.0.1",
12+
"eslint-plugin-import": "^2.29.1",
13+
"eslint-plugin-n": "^16.6.2",
14+
"eslint-plugin-promise": "^6.1.1",
15+
"typescript": "^5.4.3"
16+
}
17+
}

src/cli.ts

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import {
2+
Input,
3+
Select,
4+
} from "https://deno.land/x/[email protected]/prompt/mod.ts";
5+
import { Command } from "https://deno.land/x/[email protected]/command/mod.ts";
6+
import { Daemon } from "./daemon.ts";
7+
import { ApplePasswordManager } from "./client.ts";
8+
import { readBigInt, toBase64 } from "./utils.ts";
9+
import { Buffer } from "node:buffer";
10+
import {
11+
type Payload,
12+
type RenamedPasswordEntry,
13+
type TOTPEntry,
14+
} from "./types.ts";
15+
16+
const PrintEntries = (payload: Payload) => {
17+
const entries = payload.Entries.map((entry) => {
18+
if ("USR" in entry) {
19+
return {
20+
username: entry.USR,
21+
domain: entry.sites[0],
22+
password: entry.PWD || "Not Included",
23+
} as RenamedPasswordEntry;
24+
} else if ("code" in entry) {
25+
return {
26+
username: entry.username,
27+
domain: entry.domain,
28+
code: entry.code || "Not Included",
29+
} as TOTPEntry;
30+
}
31+
});
32+
console.log(JSON.stringify(entries));
33+
};
34+
35+
const client = new ApplePasswordManager();
36+
37+
const otp = new Command()
38+
.description("Interactively list accounts/OTPs.")
39+
.action(async () => {
40+
const action: string = await Select.prompt({
41+
message: "Choose an action: ",
42+
options: ["list OTPs", "get OTPs"],
43+
});
44+
const url = await Input.prompt({
45+
message: "Enter URL: ",
46+
});
47+
if (action === "list OTPs") {
48+
PrintEntries(await client.listOTPForURL(url));
49+
} else if (action === "get OTPs") {
50+
PrintEntries(await client.getOTPForURL(url));
51+
}
52+
})
53+
.command("get", "Get a OTP for a website.")
54+
.arguments("<url:string>")
55+
.action(async (_, url: string) => {
56+
if (!url) {
57+
throw new Error("Missing required argument 'url'.");
58+
}
59+
PrintEntries(await client.getOTPForURL(url));
60+
})
61+
.command("list", "List available OTPs for a website.")
62+
.arguments("<url:string>")
63+
.action(async (_, url: string) => {
64+
if (!url) {
65+
throw new Error("Missing required argument 'url'.");
66+
}
67+
PrintEntries(await client.listOTPForURL(url));
68+
});
69+
70+
const pw = new Command()
71+
.description("Interactively list accounts/passwords.")
72+
.action(async () => {
73+
const action: string = await Select.prompt({
74+
message: "Choose an action: ",
75+
options: ["list accounts", "get password"],
76+
});
77+
const url = await Input.prompt({
78+
message: "Enter URL: ",
79+
});
80+
if (action === "list accounts") {
81+
PrintEntries(await client.getLoginNamesForURL(url));
82+
} else if (action === "get password") {
83+
PrintEntries(await client.getPasswordForURL(url));
84+
}
85+
})
86+
.command("get", "Get a password for a website.")
87+
.arguments("<url:string> [username:string]")
88+
.action(async (_, url: string, username?: string) => {
89+
if (!url) {
90+
throw new Error("Missing required argument 'url'.");
91+
}
92+
PrintEntries(await client.getPasswordForURL(url, username));
93+
})
94+
.command("list", "List available accounts for a website.")
95+
.arguments("<url:string>")
96+
.action(async (_, url: string) => {
97+
if (!url) {
98+
throw new Error("Missing required argument 'url'.");
99+
}
100+
PrintEntries(await client.getLoginNamesForURL(url));
101+
});
102+
103+
const daemon = new Command()
104+
.description("Start the daemon.")
105+
.option("-p, --port <port:number>", "Port to listen on.", { default: 10000 })
106+
.action(Daemon);
107+
108+
const auth = new Command()
109+
.description("Authenticate CLI with daemon.")
110+
.action(async () => {
111+
await client.requestChallenge();
112+
const password = await Input.prompt({
113+
message: "Enter PIN: ",
114+
minLength: 6,
115+
maxLength: 6,
116+
});
117+
await client.verifyChallenge(password);
118+
})
119+
.command("request", "Request a challenge from the daemon.")
120+
.action(async () => {
121+
await client.requestChallenge();
122+
console.log(JSON.stringify({
123+
salt: toBase64(client.session.salt),
124+
serverKey: toBase64(client.session.serverPublicKey),
125+
username: client.session.username,
126+
clientKey: toBase64(client.session.clientPrivateKey),
127+
}));
128+
})
129+
.command("response", "Respond to a challenge from the daemon.")
130+
.option("-p, --pin <pin>", "challenge-response pin.", { required: true })
131+
.option("-s, --salt <salt>", "request salt.", { required: true })
132+
.option("-sk, --serverKey <serverKey>", "server public key.", {
133+
required: true,
134+
})
135+
.option("-ck, --clientKey <clientKey>", "client public key.", {
136+
required: true,
137+
})
138+
.option("-u, --username <username>", "client username.", { required: true })
139+
.action(async (options) => {
140+
const { serverKey, salt, username, clientKey, pin } = options;
141+
const serverPublicKey = readBigInt(Buffer.from(serverKey, "base64"));
142+
const clientPrivateKey = readBigInt(Buffer.from(clientKey, "base64"));
143+
const saltResponse = readBigInt(Buffer.from(salt, "base64"));
144+
client.session.username = username;
145+
client.session.clientPrivateKey = clientPrivateKey;
146+
client.session.salt = saltResponse;
147+
client.session.serverPublicKey = serverPublicKey;
148+
await client.verifyChallenge(pin);
149+
console.log(JSON.stringify({ status: "success" }));
150+
});
151+
152+
await new Command()
153+
.name("apw-cli")
154+
.version("0.1.0")
155+
.description("🔑 a CLI for Apple Passwords 🔒")
156+
.command("auth", auth)
157+
.command("pw", pw)
158+
.command("otp", otp)
159+
.command("start", daemon)
160+
.parse(Deno.args)
161+
.finally(() => Deno.exit());

0 commit comments

Comments
 (0)