mirror of
https://github.com/nab138/isideload.git
synced 2026-03-02 06:26:16 +01:00
first commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
3258
Cargo.lock
generated
Normal file
3258
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "isideload"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
plist = { version = "1.7.2" }
|
||||
icloud_auth = {path = "./apple-private-apis/icloud-auth" }
|
||||
uuid = "1.17.0"
|
||||
zip = "4.3.0"
|
||||
hex = "0.4.3"
|
||||
sha1 = "0.10.6"
|
||||
idevice = "0.1.37"
|
||||
9
README.md
Normal file
9
README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# isideload
|
||||
|
||||
A Rust library for sideloading iOS applications. Designed for use in [YCode](https://github.com/nab138/YCode).
|
||||
|
||||
### Licensing
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
A lot of the authentication code came from https://github.com/SideStore/apple-private-apis/, but the original project was left unfinished. This repository contains a (more) complete implementation of the package. That part of the code has been kept under its original license, MPL-2.0.
|
||||
10
apple-private-apis/.gitignore
vendored
Normal file
10
apple-private-apis/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/target
|
||||
*/target
|
||||
*/Cargo.lock
|
||||
Cargo.lock
|
||||
ignore_this_test.js
|
||||
|
||||
# IDE generated files
|
||||
.idea
|
||||
|
||||
anisette_test/
|
||||
5
apple-private-apis/Cargo.toml
Normal file
5
apple-private-apis/Cargo.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"omnisette",
|
||||
"icloud-auth"
|
||||
]
|
||||
373
apple-private-apis/LICENSE
Normal file
373
apple-private-apis/LICENSE
Normal file
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
3
apple-private-apis/icloud-auth/.gitignore
vendored
Normal file
3
apple-private-apis/icloud-auth/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
*.py
|
||||
32
apple-private-apis/icloud-auth/Cargo.toml
Normal file
32
apple-private-apis/icloud-auth/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "icloud_auth"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = { version = "1.0.142" }
|
||||
base64 = "0.13.1"
|
||||
srp = { version = "0.6.0", path = "./rustcrypto-srp" }
|
||||
pbkdf2 = { version = "0.11.0" }
|
||||
sha2 = { version = "0.10.6" }
|
||||
rand = { version = "0.8.5" }
|
||||
rustls = { version = "0.20.7" }
|
||||
rustls-pemfile = { version = "1.0.1" }
|
||||
plist = { version = "1.7.2" }
|
||||
hmac = "0.12.1"
|
||||
num-bigint = "0.4.3"
|
||||
cbc = { version = "0.1.2", features = ["std"] }
|
||||
aes = "0.8.2"
|
||||
pkcs7 = "0.3.0"
|
||||
reqwest = { version = "0.11.14", features = ["blocking", "json", "default-tls"] }
|
||||
omnisette = {path = "../omnisette", features = ["remote-anisette-v3"]}
|
||||
thiserror = "1.0.58"
|
||||
tokio = "1"
|
||||
botan = { version = "0.11.1", features = ["vendored"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
41
apple-private-apis/icloud-auth/rustcrypto-srp/CHANGELOG.md
Normal file
41
apple-private-apis/icloud-auth/rustcrypto-srp/CHANGELOG.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 0.6.0 (2022-01-22)
|
||||
### Changed
|
||||
- Use `modpow` for constant time modular exponentiation ([#78])
|
||||
- Rebuild library ([#79])
|
||||
|
||||
[#78]: https://github.com/RustCrypto/PAKEs/pull/78
|
||||
[#79]: https://github.com/RustCrypto/PAKEs/pull/79
|
||||
|
||||
## 0.5.0 (2020-10-07)
|
||||
|
||||
## 0.4.3 (2019-11-07)
|
||||
|
||||
## 0.4.2 (2019-11-06)
|
||||
|
||||
## 0.4.1 (2019-11-07)
|
||||
|
||||
## 0.4.0 (2018-12-20)
|
||||
|
||||
## 0.3.0 (2018-10-22)
|
||||
|
||||
## 0.2.5 (2018-04-14)
|
||||
|
||||
## 0.2.4 (2017-11-01)
|
||||
|
||||
## 0.2.3 (2017-08-17)
|
||||
|
||||
## 0.2.2 (2017-08-14)
|
||||
|
||||
## 0.2.1 (2017-08-14)
|
||||
|
||||
## 0.2.0 (2017-08-14)
|
||||
|
||||
## 0.1.1 (2017-08-13)
|
||||
|
||||
## 0.1.0 (2017-08-13)
|
||||
28
apple-private-apis/icloud-auth/rustcrypto-srp/Cargo.toml
Normal file
28
apple-private-apis/icloud-auth/rustcrypto-srp/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "srp"
|
||||
version = "0.6.0"
|
||||
authors = ["RustCrypto Developers"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Secure Remote Password (SRP) protocol implementation"
|
||||
documentation = "https://docs.rs/srp"
|
||||
repository = "https://github.com/RustCrypto/PAKEs"
|
||||
keywords = ["crypto", "pake", "authentication"]
|
||||
categories = ["cryptography", "authentication"]
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
|
||||
[dependencies]
|
||||
num-bigint = "0.4"
|
||||
generic-array = "0.14"
|
||||
digest = "0.10"
|
||||
lazy_static = "1.2"
|
||||
subtle = "2.4"
|
||||
base64 = "0.21.0"
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.3"
|
||||
num-traits = "0.2"
|
||||
rand = "0.8"
|
||||
sha1 = "0.10.6"
|
||||
sha2 = "0.10.8"
|
||||
201
apple-private-apis/icloud-auth/rustcrypto-srp/LICENSE-APACHE
Normal file
201
apple-private-apis/icloud-auth/rustcrypto-srp/LICENSE-APACHE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
25
apple-private-apis/icloud-auth/rustcrypto-srp/LICENSE-MIT
Normal file
25
apple-private-apis/icloud-auth/rustcrypto-srp/LICENSE-MIT
Normal file
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2017 Artyom Pavlov
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
73
apple-private-apis/icloud-auth/rustcrypto-srp/README.md
Normal file
73
apple-private-apis/icloud-auth/rustcrypto-srp/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# [RustCrypto]: SRP
|
||||
|
||||
[![crate][crate-image]][crate-link]
|
||||
[![Docs][docs-image]][docs-link]
|
||||
![Apache2/MIT licensed][license-image]
|
||||
![Rust Version][rustc-image]
|
||||
[![Project Chat][chat-image]][chat-link]
|
||||
[![Build Status][build-image]][build-link]
|
||||
|
||||
Pure Rust implementation of the [Secure Remote Password] password-authenticated
|
||||
key-exchange algorithm.
|
||||
|
||||
[Documentation][docs-link]
|
||||
|
||||
## About
|
||||
|
||||
This implementation is generic over hash functions using the [`Digest`] trait,
|
||||
so you will need to choose a hash function, e.g. `Sha256` from [`sha2`] crate.
|
||||
|
||||
Additionally this crate allows to use a specialized password hashing
|
||||
algorithm for private key computation instead of method described in the
|
||||
SRP literature.
|
||||
|
||||
Compatibility with other implementations has not yet been tested.
|
||||
|
||||
## ⚠️ Security Warning
|
||||
|
||||
This crate has never received an independent third party audit for security and
|
||||
correctness.
|
||||
|
||||
USE AT YOUR OWN RISK!
|
||||
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
Rust **1.56** or higher.
|
||||
|
||||
Minimum supported Rust version can be changed in the future, but it will be
|
||||
done with a minor version bump.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of:
|
||||
|
||||
* [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* [MIT license](http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
|
||||
[//]: # (badges)
|
||||
|
||||
[crate-image]: https://img.shields.io/crates/v/srp.svg
|
||||
[crate-link]: https://crates.io/crates/srp
|
||||
[docs-image]: https://docs.rs/srp/badge.svg
|
||||
[docs-link]: https://docs.rs/srp/
|
||||
[license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg
|
||||
[rustc-image]: https://img.shields.io/badge/rustc-1.56+-blue.svg
|
||||
[chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg
|
||||
[chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/260045-PAKEs
|
||||
[build-image]: https://github.com/RustCrypto/PAKEs/actions/workflows/srp.yml/badge.svg
|
||||
[build-link]: https://github.com/RustCrypto/PAKEs/actions/workflows/srp.yml
|
||||
|
||||
[//]: # (general links)
|
||||
|
||||
[RustCrypto]: https://github.com/RustCrypto
|
||||
[Secure Remote Password]: https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol
|
||||
[`Digest`]: https://docs.rs/digest
|
||||
[`sha2`]: https://crates.io/crates/sha2
|
||||
248
apple-private-apis/icloud-auth/rustcrypto-srp/src/client.rs
Normal file
248
apple-private-apis/icloud-auth/rustcrypto-srp/src/client.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
//! SRP client implementation.
|
||||
//!
|
||||
//! # Usage
|
||||
//! First create SRP client struct by passing to it SRP parameters (shared
|
||||
//! between client and server).
|
||||
//!
|
||||
//! You can use SHA1 from SRP-6a, but it's highly recommended to use specialized
|
||||
//! password hashing algorithm instead (e.g. PBKDF2, argon2 or scrypt).
|
||||
//!
|
||||
//! ```rust
|
||||
//! use crate::srp::groups::G_2048;
|
||||
//! use sha2::Sha256; // Note: You should probably use a proper password KDF
|
||||
//! # use crate::srp::client::SrpClient;
|
||||
//!
|
||||
//! let client = SrpClient::<Sha256>::new(&G_2048);
|
||||
//! ```
|
||||
//!
|
||||
//! Next send handshake data (username and `a_pub`) to the server and receive
|
||||
//! `salt` and `b_pub`:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # let client = crate::srp::client::SrpClient::<sha2::Sha256>::new(&crate::srp::groups::G_2048);
|
||||
//! # fn server_response()-> (Vec<u8>, Vec<u8>) { (vec![], vec![]) }
|
||||
//!
|
||||
//! let mut a = [0u8; 64];
|
||||
//! // rng.fill_bytes(&mut a);
|
||||
//! let a_pub = client.compute_public_ephemeral(&a);
|
||||
//! let (salt, b_pub) = server_response();
|
||||
//! ```
|
||||
//!
|
||||
//! Process the server response and create verifier instance.
|
||||
//! process_reply can return error in case of malicious `b_pub`.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # let client = crate::srp::client::SrpClient::<sha2::Sha256>::new(&crate::srp::groups::G_2048);
|
||||
//! # let a = [0u8; 64];
|
||||
//! # let username = b"username";
|
||||
//! # let password = b"password";
|
||||
//! # let salt = b"salt";
|
||||
//! # let b_pub = b"b_pub";
|
||||
//!
|
||||
//! let private_key = (username, password, salt);
|
||||
//! let verifier = client.process_reply(&a, username, password, salt, b_pub);
|
||||
//! ```
|
||||
//!
|
||||
//! Finally verify the server: first generate user proof,
|
||||
//! send it to the server and verify server proof in the reply. Note that
|
||||
//! `verify_server` method will return error in case of incorrect server reply.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # let client = crate::srp::client::SrpClient::<sha2::Sha256>::new(&crate::srp::groups::G_2048);
|
||||
//! # let verifier = client.process_reply(b"", b"", b"", b"", b"1").unwrap();
|
||||
//! # fn send_proof(_: &[u8]) -> Vec<u8> { vec![173, 202, 13, 26, 207, 73, 0, 46, 121, 238, 48, 170, 96, 146, 60, 49, 88, 76, 12, 184, 152, 76, 207, 220, 140, 205, 190, 189, 117, 6, 131, 63] }
|
||||
//!
|
||||
//! let client_proof = verifier.proof();
|
||||
//! let server_proof = send_proof(client_proof);
|
||||
//! verifier.verify_server(&server_proof).unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! `key` contains shared secret key between user and the server. You can extract shared secret
|
||||
//! key using `key()` method.
|
||||
//! ```rust
|
||||
//! # let client = crate::srp::client::SrpClient::<sha2::Sha256>::new(&crate::srp::groups::G_2048);
|
||||
//! # let verifier = client.process_reply(b"", b"", b"", b"", b"1").unwrap();
|
||||
//!
|
||||
//! verifier.key();
|
||||
//!```
|
||||
//!
|
||||
//!
|
||||
//! For user registration on the server first generate salt (e.g. 32 bytes long)
|
||||
//! and get password verifier which depends on private key. Send username, salt
|
||||
//! and password verifier over protected channel to protect against
|
||||
//! Man-in-the-middle (MITM) attack for registration.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # let client = crate::srp::client::SrpClient::<sha2::Sha256>::new(&crate::srp::groups::G_2048);
|
||||
//! # let username = b"username";
|
||||
//! # let password = b"password";
|
||||
//! # let salt = b"salt";
|
||||
//! # fn send_registration_data(_: &[u8], _: &[u8], _: &[u8]) {}
|
||||
//!
|
||||
//! let pwd_verifier = client.compute_verifier(username, password, salt);
|
||||
//! send_registration_data(username, salt, &pwd_verifier);
|
||||
//! ```
|
||||
|
||||
use digest::{Digest, Output};
|
||||
use num_bigint::BigUint;
|
||||
use std::marker::PhantomData;
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use crate::types::{SrpAuthError, SrpGroup};
|
||||
use crate::utils::{compute_k, compute_m1, compute_m2, compute_u};
|
||||
|
||||
/// SRP client state before handshake with the server.
|
||||
pub struct SrpClient<'a, D: Digest> {
|
||||
params: &'a SrpGroup,
|
||||
d: PhantomData<D>,
|
||||
}
|
||||
|
||||
/// SRP client state after handshake with the server.
|
||||
pub struct SrpClientVerifier<D: Digest> {
|
||||
m1: Output<D>,
|
||||
m2: Output<D>,
|
||||
key: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a, D: Digest> SrpClient<'a, D> {
|
||||
/// Create new SRP client instance.
|
||||
pub fn new(params: &'a SrpGroup) -> Self {
|
||||
Self {
|
||||
params,
|
||||
d: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_a_pub(&self, a: &BigUint) -> BigUint {
|
||||
self.params.g.modpow(a, &self.params.n)
|
||||
}
|
||||
|
||||
// H(<username> | ":" | <raw password>)
|
||||
pub fn compute_identity_hash(username: &[u8], password: &[u8]) -> Output<D> {
|
||||
let mut d = D::new();
|
||||
d.update(username);
|
||||
d.update(b":");
|
||||
d.update(password);
|
||||
d.finalize()
|
||||
}
|
||||
|
||||
// x = H(<salt> | H(<username> | ":" | <raw password>))
|
||||
pub fn compute_x(identity_hash: &[u8], salt: &[u8]) -> BigUint {
|
||||
let mut x = D::new();
|
||||
x.update(salt);
|
||||
x.update(identity_hash);
|
||||
BigUint::from_bytes_be(&x.finalize())
|
||||
}
|
||||
|
||||
// (B - (k * g^x)) ^ (a + (u * x)) % N
|
||||
pub fn compute_premaster_secret(
|
||||
&self,
|
||||
b_pub: &BigUint,
|
||||
k: &BigUint,
|
||||
x: &BigUint,
|
||||
a: &BigUint,
|
||||
u: &BigUint,
|
||||
) -> BigUint {
|
||||
// (k * g^x)
|
||||
let base = (k * (self.params.g.modpow(x, &self.params.n))) % &self.params.n;
|
||||
// Because we do operation in modulo N we can get: b_pub > base. That's not good. So we add N to b_pub to make sure.
|
||||
// B - kg^x
|
||||
let base = ((&self.params.n + b_pub) - &base) % &self.params.n;
|
||||
let exp = (u * x) + a;
|
||||
// S = (B - kg^x) ^ (a + ux)
|
||||
// or
|
||||
// S = base ^ exp
|
||||
base.modpow(&exp, &self.params.n)
|
||||
}
|
||||
|
||||
// v = g^x % N
|
||||
pub fn compute_v(&self, x: &BigUint) -> BigUint {
|
||||
self.params.g.modpow(x, &self.params.n)
|
||||
}
|
||||
|
||||
/// Get password verifier (v in RFC5054) for user registration on the server.
|
||||
pub fn compute_verifier(&self, username: &[u8], password: &[u8], salt: &[u8]) -> Vec<u8> {
|
||||
let identity_hash = Self::compute_identity_hash(username, password);
|
||||
let x = Self::compute_x(identity_hash.as_slice(), salt);
|
||||
self.compute_v(&x).to_bytes_be()
|
||||
}
|
||||
|
||||
/// Get public ephemeral value for handshaking with the server.
|
||||
/// g^a % N
|
||||
pub fn compute_public_ephemeral(&self, a: &[u8]) -> Vec<u8> {
|
||||
self.compute_a_pub(&BigUint::from_bytes_be(a)).to_bytes_be()
|
||||
}
|
||||
|
||||
/// Process server reply to the handshake.
|
||||
/// a is a random value,
|
||||
/// username, password is supplied by the user
|
||||
/// salt and b_pub come from the server
|
||||
pub fn process_reply(
|
||||
&self,
|
||||
a: &[u8],
|
||||
username: &[u8],
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
b_pub: &[u8],
|
||||
) -> Result<SrpClientVerifier<D>, SrpAuthError> {
|
||||
let a = BigUint::from_bytes_be(a);
|
||||
// let a_pub = BigUint::from_bytes_be(&a_pub_bytes);
|
||||
let a_pub = Self::compute_a_pub(&self, &a);
|
||||
|
||||
let b_pub = BigUint::from_bytes_be(b_pub);
|
||||
|
||||
// Safeguard against malicious B
|
||||
if &b_pub % &self.params.n == BigUint::default() {
|
||||
return Err(SrpAuthError::IllegalParameter("b_pub".to_owned()));
|
||||
}
|
||||
|
||||
let u = compute_u::<D>(&a_pub.to_bytes_be(), &b_pub.to_bytes_be());
|
||||
let k = compute_k::<D>(self.params);
|
||||
let identity_hash = Self::compute_identity_hash(&[], password);
|
||||
let x = Self::compute_x(identity_hash.as_slice(), salt);
|
||||
|
||||
let key = self.compute_premaster_secret(&b_pub, &k, &x, &a, &u);
|
||||
let key = D::digest(key.to_bytes_be());
|
||||
|
||||
let m1 = compute_m1::<D>(
|
||||
&a_pub.to_bytes_be(),
|
||||
&b_pub.to_bytes_be(),
|
||||
&key,
|
||||
username,
|
||||
salt,
|
||||
self.params,
|
||||
);
|
||||
|
||||
let m2 = compute_m2::<D>(&a_pub.to_bytes_be(), &m1, &key);
|
||||
|
||||
Ok(SrpClientVerifier {
|
||||
m1,
|
||||
m2,
|
||||
key: key.to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Digest> SrpClientVerifier<D> {
|
||||
/// Get shared secret key without authenticating server, e.g. for using with
|
||||
/// authenticated encryption modes. DO NOT USE this method without
|
||||
/// some kind of secure authentication
|
||||
pub fn key(&self) -> &[u8] {
|
||||
&self.key
|
||||
}
|
||||
|
||||
/// Verification data for sending to the server.
|
||||
pub fn proof(&self) -> &[u8] {
|
||||
self.m1.as_slice()
|
||||
}
|
||||
|
||||
/// Verify server reply to verification data.
|
||||
pub fn verify_server(&self, reply: &[u8]) -> Result<(), SrpAuthError> {
|
||||
if self.m2.ct_eq(reply).unwrap_u8() != 1 {
|
||||
// aka == 0
|
||||
Err(SrpAuthError::BadRecordMac("server".to_owned()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
57
apple-private-apis/icloud-auth/rustcrypto-srp/src/groups.rs
Normal file
57
apple-private-apis/icloud-auth/rustcrypto-srp/src/groups.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! Groups from [RFC 5054](https://tools.ietf.org/html/rfc5054)
|
||||
//!
|
||||
//! It is strongly recommended to use them instead of custom generated
|
||||
//! groups. Additionally it is not recommended to use `G_1024` and `G_1536`,
|
||||
//! they are provided only for compatibility with the legacy software.
|
||||
use crate::types::SrpGroup;
|
||||
use lazy_static::lazy_static;
|
||||
use num_bigint::BigUint;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref G_1024: SrpGroup = SrpGroup {
|
||||
n: BigUint::from_bytes_be(include_bytes!("groups/1024.bin")),
|
||||
g: BigUint::from_bytes_be(&[2]),
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref G_1536: SrpGroup = SrpGroup {
|
||||
n: BigUint::from_bytes_be(include_bytes!("groups/1536.bin")),
|
||||
g: BigUint::from_bytes_be(&[2]),
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref G_2048: SrpGroup = SrpGroup {
|
||||
n: BigUint::from_bytes_be(include_bytes!("groups/2048.bin")),
|
||||
g: BigUint::from_bytes_be(&[2]),
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref G_3072: SrpGroup = SrpGroup {
|
||||
n: BigUint::from_bytes_be(include_bytes!("groups/3072.bin")),
|
||||
g: BigUint::from_bytes_be(&[5]),
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref G_4096: SrpGroup = SrpGroup {
|
||||
n: BigUint::from_bytes_be(include_bytes!("groups/4096.bin")),
|
||||
g: BigUint::from_bytes_be(&[5]),
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref G_6144: SrpGroup = SrpGroup {
|
||||
n: BigUint::from_bytes_be(include_bytes!("groups/6144.bin")),
|
||||
g: BigUint::from_bytes_be(&[5]),
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref G_8192: SrpGroup = SrpGroup {
|
||||
n: BigUint::from_bytes_be(include_bytes!("groups/8192.bin")),
|
||||
g: BigUint::from_bytes_be(&[19]),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
î¯
|
||||
¹³<EFBFBD>Öœ3ø
|
||||
ú<EFBFBD>Åè`ra‡uÿ<ž¢1Lœ%evÖtßt–ê<E28093>Ó8;HÖ’ÆààÕØâP¹‹äŽI\`‰ÚÑ]Ç×´aTÖ¶ÎŽôi±]I‚U›){Ï…Å)õffWìhí¼<rlÀ/ÔËô—nªšýQ8þƒvC[ŸÆ/Àëã
|
||||
@@ -0,0 +1 @@
|
||||
ťď<Żą9'z±ń*†¤{»ŰĄô™¬L€ľî©aKĚM_O_Un'ËŢQĆ©Kä`z)X<>; ĐřC€¶U»š"čÜߊ|ěgđĐ<C491>4±Čąy‰›`žăş¶=GT<47><54>ŰűüvN?KSÝťˇ‹ý>+śŚőnß•94–'Ű/Ő=$·Ä†ew.C}lŚäBsJ÷Ě·®<C2B7>|&J㩾¸Š/鸵).Z˙^‘GžŚç˘Ś$BĆó“Iš#MĎvăţŃ5ů»
|
||||
@@ -0,0 +1,2 @@
|
||||
¬kÛA2Jš›ñfÞ^‰X/¯r¶e‡îü1’”=µ`P£s)Ë´ ™í<E284A2>“àuwg¡=Õ#«K1
|
||||
ÍH©ÚýPè9ií·g°Ï`•š:³fûÕúªè)©–/“¸Uùy“ì—^ê¨
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
57
apple-private-apis/icloud-auth/rustcrypto-srp/src/lib.rs
Normal file
57
apple-private-apis/icloud-auth/rustcrypto-srp/src/lib.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo_small.png")]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
//! # Usage
|
||||
//! Add `srp` dependency to your `Cargo.toml`:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! srp = "0.6"
|
||||
//! ```
|
||||
//!
|
||||
//! Next read documentation for [`client`](client/index.html) and
|
||||
//! [`server`](server/index.html) modules.
|
||||
//!
|
||||
//! # Algorithm description
|
||||
//! Here we briefly describe implemented algorithm. For additional information
|
||||
//! refer to SRP literature. All arithmetic is done modulo `N`, where `N` is a
|
||||
//! large safe prime (`N = 2q+1`, where `q` is prime). Additionally `g` MUST be
|
||||
//! a generator modulo `N`. It's STRONGLY recommended to use SRP parameters
|
||||
//! provided by this crate in the [`groups`](groups/index.html) module.
|
||||
//!
|
||||
//! | Client | Data transfer | Server |
|
||||
//! |------------------------|-------------------|---------------------------------|
|
||||
//! |`a_pub = g^a` | — `a_pub`, `I` —> | (lookup `s`, `v` for given `I`) |
|
||||
//! |`x = PH(P, s)` | <— `b_pub`, `s` — | `b_pub = k*v + g^b` |
|
||||
//! |`u = H(a_pub ‖ b_pub)` | | `u = H(a_pub ‖ b_pub)` |
|
||||
//! |`s = (b_pub - k*g^x)^(a+u*x)` | | `S = (b_pub - k*g^x)^(a+u*x)` |
|
||||
//! |`K = H(s)` | | `K = H(s)` |
|
||||
//! |`M1 = H(A ‖ B ‖ K)` | — `M1` —> | (verify `M1`) |
|
||||
//! |(verify `M2`) | <— `M2` — | `M2 = H(A ‖ M1 ‖ K)` |
|
||||
//!
|
||||
//! Variables and notations have the following meaning:
|
||||
//!
|
||||
//! - `I` — user identity (username)
|
||||
//! - `P` — user password
|
||||
//! - `H` — one-way hash function
|
||||
//! - `PH` — password hashing algroithm, in the RFC 5054 described as
|
||||
//! `H(s ‖ H(I ‖ ":" ‖ P))`
|
||||
//! - `^` — (modular) exponentiation
|
||||
//! - `‖` — concatenation
|
||||
//! - `x` — user private key
|
||||
//! - `s` — salt generated by user and stored on the server
|
||||
//! - `v` — password verifier equal to `g^x` and stored on the server
|
||||
//! - `a`, `b` — secret ephemeral values (at least 256 bits in length)
|
||||
//! - `A`, `B` — Public ephemeral values
|
||||
//! - `u` — scrambling parameter
|
||||
//! - `k` — multiplier parameter (`k = H(N || g)` in SRP-6a)
|
||||
//!
|
||||
//! [1]: https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol
|
||||
//! [2]: https://tools.ietf.org/html/rfc5054
|
||||
|
||||
pub mod client;
|
||||
pub mod groups;
|
||||
pub mod server;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
190
apple-private-apis/icloud-auth/rustcrypto-srp/src/server.rs
Normal file
190
apple-private-apis/icloud-auth/rustcrypto-srp/src/server.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
//! SRP server implementation
|
||||
//!
|
||||
//! # Usage
|
||||
//! First receive user's username and public value `a_pub`, retrieve from a
|
||||
//! database the salt and verifier for a given username. Generate `b` and public value `b_pub`.
|
||||
//!
|
||||
//!
|
||||
//! ```rust
|
||||
//! use crate::srp::groups::G_2048;
|
||||
//! use sha2::Sha256; // Note: You should probably use a proper password KDF
|
||||
//! # use crate::srp::server::SrpServer;
|
||||
//! # fn get_client_request()-> (Vec<u8>, Vec<u8>) { (vec![], vec![])}
|
||||
//! # fn get_user(_: &[u8])-> (Vec<u8>, Vec<u8>) { (vec![], vec![])}
|
||||
//!
|
||||
//! let server = SrpServer::<Sha256>::new(&G_2048);
|
||||
//! let (username, a_pub) = get_client_request();
|
||||
//! let (salt, v) = get_user(&username);
|
||||
//! let mut b = [0u8; 64];
|
||||
//! // rng.fill_bytes(&mut b);
|
||||
//! let b_pub = server.compute_public_ephemeral(&b, &v);
|
||||
//! ```
|
||||
//!
|
||||
//! Next send to user `b_pub` and `salt` from user record
|
||||
//!
|
||||
//! Next process the user response:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # let server = crate::srp::server::SrpServer::<sha2::Sha256>::new(&crate::srp::groups::G_2048);
|
||||
//! # fn get_client_response() -> Vec<u8> { vec![1] }
|
||||
//! # let b = [0u8; 64];
|
||||
//! # let v = b"";
|
||||
//!
|
||||
//! let a_pub = get_client_response();
|
||||
//! let verifier = server.process_reply(&b, v, &a_pub).unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! And finally receive user proof, verify it and send server proof in the
|
||||
//! reply:
|
||||
//!
|
||||
//! ```rust
|
||||
//! # let server = crate::srp::server::SrpServer::<sha2::Sha256>::new(&crate::srp::groups::G_2048);
|
||||
//! # let verifier = server.process_reply(b"", b"", b"1").unwrap();
|
||||
//! # fn get_client_proof()-> Vec<u8> { vec![26, 80, 8, 243, 111, 162, 238, 171, 208, 237, 207, 46, 46, 137, 44, 213, 105, 208, 84, 224, 244, 216, 103, 145, 14, 103, 182, 56, 242, 4, 179, 57] };
|
||||
//! # fn send_proof(_: &[u8]) { };
|
||||
//!
|
||||
//! let client_proof = get_client_proof();
|
||||
//! verifier.verify_client(&client_proof).unwrap();
|
||||
//! send_proof(verifier.proof());
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! `key` contains shared secret key between user and the server. You can extract shared secret
|
||||
//! key using `key()` method.
|
||||
//! ```rust
|
||||
//! # let server = crate::srp::server::SrpServer::<sha2::Sha256>::new(&crate::srp::groups::G_2048);
|
||||
//! # let verifier = server.process_reply(b"", b"", b"1").unwrap();
|
||||
//!
|
||||
//! verifier.key();
|
||||
//!```
|
||||
//!
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use digest::{Digest, Output};
|
||||
use num_bigint::BigUint;
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use crate::types::{SrpAuthError, SrpGroup};
|
||||
use crate::utils::{compute_k, compute_m1, compute_m2, compute_u};
|
||||
|
||||
/// SRP server state
|
||||
pub struct SrpServer<'a, D: Digest> {
|
||||
params: &'a SrpGroup,
|
||||
d: PhantomData<D>,
|
||||
}
|
||||
|
||||
/// SRP server state after handshake with the client.
|
||||
pub struct SrpServerVerifier<D: Digest> {
|
||||
m1: Output<D>,
|
||||
m2: Output<D>,
|
||||
key: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a, D: Digest> SrpServer<'a, D> {
|
||||
/// Create new server state.
|
||||
pub fn new(params: &'a SrpGroup) -> Self {
|
||||
Self {
|
||||
params,
|
||||
d: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// k*v + g^b % N
|
||||
pub fn compute_b_pub(&self, b: &BigUint, k: &BigUint, v: &BigUint) -> BigUint {
|
||||
let inter = (k * v) % &self.params.n;
|
||||
(inter + self.params.g.modpow(b, &self.params.n)) % &self.params.n
|
||||
}
|
||||
|
||||
// <premaster secret> = (A * v^u) ^ b % N
|
||||
pub fn compute_premaster_secret(
|
||||
&self,
|
||||
a_pub: &BigUint,
|
||||
v: &BigUint,
|
||||
u: &BigUint,
|
||||
b: &BigUint,
|
||||
) -> BigUint {
|
||||
// (A * v^u)
|
||||
let base = (a_pub * v.modpow(u, &self.params.n)) % &self.params.n;
|
||||
base.modpow(b, &self.params.n)
|
||||
}
|
||||
|
||||
/// Get public ephemeral value for sending to the client.
|
||||
pub fn compute_public_ephemeral(&self, b: &[u8], v: &[u8]) -> Vec<u8> {
|
||||
self.compute_b_pub(
|
||||
&BigUint::from_bytes_be(b),
|
||||
&compute_k::<D>(self.params),
|
||||
&BigUint::from_bytes_be(v),
|
||||
)
|
||||
.to_bytes_be()
|
||||
}
|
||||
|
||||
/// Process client reply to the handshake.
|
||||
/// b is a random value,
|
||||
/// v is the provided during initial user registration
|
||||
pub fn process_reply(
|
||||
&self,
|
||||
b: &[u8],
|
||||
v: &[u8],
|
||||
a_pub: &[u8],
|
||||
username: &[u8],
|
||||
salt: &[u8],
|
||||
) -> Result<SrpServerVerifier<D>, SrpAuthError> {
|
||||
let b = BigUint::from_bytes_be(b);
|
||||
let v = BigUint::from_bytes_be(v);
|
||||
let a_pub = BigUint::from_bytes_be(a_pub);
|
||||
|
||||
let k = compute_k::<D>(self.params);
|
||||
let b_pub = self.compute_b_pub(&b, &k, &v);
|
||||
|
||||
// Safeguard against malicious A
|
||||
if &a_pub % &self.params.n == BigUint::default() {
|
||||
return Err(SrpAuthError::IllegalParameter("a_pub".to_owned()));
|
||||
}
|
||||
|
||||
let u = compute_u::<D>(&a_pub.to_bytes_be(), &b_pub.to_bytes_be());
|
||||
|
||||
let key = self.compute_premaster_secret(&a_pub, &v, &u, &b);
|
||||
|
||||
let m1 = compute_m1::<D>(
|
||||
&a_pub.to_bytes_be(),
|
||||
&b_pub.to_bytes_be(),
|
||||
&key.to_bytes_be(),
|
||||
username,
|
||||
salt,
|
||||
self.params,
|
||||
);
|
||||
|
||||
let m2 = compute_m2::<D>(&a_pub.to_bytes_be(), &m1, &key.to_bytes_be());
|
||||
|
||||
Ok(SrpServerVerifier {
|
||||
m1,
|
||||
m2,
|
||||
key: key.to_bytes_be(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Digest> SrpServerVerifier<D> {
|
||||
/// Get shared secret between user and the server. (do not forget to verify
|
||||
/// that keys are the same!)
|
||||
pub fn key(&self) -> &[u8] {
|
||||
&self.key
|
||||
}
|
||||
|
||||
/// Verification data for sending to the client.
|
||||
pub fn proof(&self) -> &[u8] {
|
||||
// TODO not Output
|
||||
self.m2.as_slice()
|
||||
}
|
||||
|
||||
/// Process user proof of having the same shared secret.
|
||||
pub fn verify_client(&self, reply: &[u8]) -> Result<(), SrpAuthError> {
|
||||
if self.m1.ct_eq(reply).unwrap_u8() != 1 {
|
||||
// aka == 0
|
||||
Err(SrpAuthError::BadRecordMac("client".to_owned()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
45
apple-private-apis/icloud-auth/rustcrypto-srp/src/types.rs
Normal file
45
apple-private-apis/icloud-auth/rustcrypto-srp/src/types.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//! Additional SRP types.
|
||||
use num_bigint::BigUint;
|
||||
use std::fmt;
|
||||
|
||||
/// SRP authentication error.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum SrpAuthError {
|
||||
IllegalParameter(String),
|
||||
BadRecordMac(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for SrpAuthError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SrpAuthError::IllegalParameter(param) => {
|
||||
write!(f, "illegal_parameter: bad '{}' value", param)
|
||||
}
|
||||
SrpAuthError::BadRecordMac(param) => {
|
||||
write!(f, "bad_record_mac: incorrect '{}' proof", param)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Group used for SRP computations
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct SrpGroup {
|
||||
/// A large safe prime (N = 2q+1, where q is prime)
|
||||
pub n: BigUint,
|
||||
/// A generator modulo N
|
||||
pub g: BigUint,
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use crate::groups::G_1024;
|
||||
// use crate::utils::compute_k;
|
||||
// use sha1::Sha1;
|
||||
|
||||
// #[test]
|
||||
// fn test_k_1024_sha1() {
|
||||
// let k = compute_k::<Sha1>(&G_1024).to_bytes_be();
|
||||
// assert_eq!(&k, include_bytes!("test/k_sha1_1024.bin"));
|
||||
// }
|
||||
// }
|
||||
70
apple-private-apis/icloud-auth/rustcrypto-srp/src/utils.rs
Normal file
70
apple-private-apis/icloud-auth/rustcrypto-srp/src/utils.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use digest::{Digest, Output};
|
||||
use num_bigint::BigUint;
|
||||
|
||||
use crate::types::SrpGroup;
|
||||
|
||||
// u = H(PAD(A) | PAD(B))
|
||||
pub fn compute_u<D: Digest>(a_pub: &[u8], b_pub: &[u8]) -> BigUint {
|
||||
let mut u = D::new();
|
||||
u.update(a_pub);
|
||||
u.update(b_pub);
|
||||
BigUint::from_bytes_be(&u.finalize())
|
||||
}
|
||||
|
||||
// k = H(N | PAD(g))
|
||||
pub fn compute_k<D: Digest>(params: &SrpGroup) -> BigUint {
|
||||
let n = params.n.to_bytes_be();
|
||||
let g_bytes = params.g.to_bytes_be();
|
||||
let mut buf = vec![0u8; n.len()];
|
||||
let l = n.len() - g_bytes.len();
|
||||
buf[l..].copy_from_slice(&g_bytes);
|
||||
|
||||
let mut d = D::new();
|
||||
d.update(&n);
|
||||
d.update(&buf);
|
||||
BigUint::from_bytes_be(d.finalize().as_slice())
|
||||
}
|
||||
|
||||
// M1 = H(A, B, K) this doesn't follow the spec but apparently no one does for M1
|
||||
// M1 should equal = H(H(N) XOR H(g) | H(U) | s | A | B | K) according to the spec
|
||||
pub fn compute_m1<D: Digest>(
|
||||
a_pub: &[u8],
|
||||
b_pub: &[u8],
|
||||
key: &[u8],
|
||||
username: &[u8],
|
||||
salt: &[u8],
|
||||
params: &SrpGroup,
|
||||
) -> Output<D> {
|
||||
let n = params.n.to_bytes_be();
|
||||
let g_bytes = params.g.to_bytes_be();
|
||||
//pad g and n to the same length
|
||||
let mut g = vec![0; n.len() - g_bytes.len()];
|
||||
g.extend_from_slice(&g_bytes);
|
||||
|
||||
// Compute the hash of n and g
|
||||
let mut g_hash = D::digest(&g);
|
||||
let n_hash = D::digest(&n);
|
||||
|
||||
// XOR the hashes
|
||||
for i in 0..g_hash.len() {
|
||||
g_hash[i] ^= n_hash[i];
|
||||
}
|
||||
|
||||
let mut d = D::new();
|
||||
d.update(&g_hash);
|
||||
d.update(D::digest(username));
|
||||
d.update(salt);
|
||||
d.update(a_pub);
|
||||
d.update(b_pub);
|
||||
d.update(key);
|
||||
d.finalize()
|
||||
}
|
||||
|
||||
// M2 = H(A, M1, K)
|
||||
pub fn compute_m2<D: Digest>(a_pub: &[u8], m1: &Output<D>, key: &[u8]) -> Output<D> {
|
||||
let mut d = D::new();
|
||||
d.update(&a_pub);
|
||||
d.update(&m1);
|
||||
d.update(&key);
|
||||
d.finalize()
|
||||
}
|
||||
108
apple-private-apis/icloud-auth/src/anisette.rs
Normal file
108
apple-private-apis/icloud-auth/src/anisette.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use crate::Error;
|
||||
use omnisette::{AnisetteConfiguration, AnisetteHeaders};
|
||||
use std::{collections::HashMap, time::SystemTime};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnisetteData {
|
||||
pub base_headers: HashMap<String, String>,
|
||||
pub generated_at: SystemTime,
|
||||
pub config: AnisetteConfiguration,
|
||||
}
|
||||
|
||||
impl AnisetteData {
|
||||
/// Fetches the data at an anisette server
|
||||
pub async fn new(config: AnisetteConfiguration) -> Result<Self, crate::Error> {
|
||||
let mut b = AnisetteHeaders::get_anisette_headers_provider(config.clone())?;
|
||||
let base_headers = b.provider.get_authentication_headers().await?;
|
||||
|
||||
Ok(AnisetteData {
|
||||
base_headers,
|
||||
generated_at: SystemTime::now(),
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn needs_refresh(&self) -> bool {
|
||||
let elapsed = self.generated_at.elapsed().unwrap();
|
||||
elapsed.as_secs() > 60
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
let elapsed = self.generated_at.elapsed().unwrap();
|
||||
elapsed.as_secs() < 90
|
||||
}
|
||||
|
||||
pub async fn refresh(&self) -> Result<Self, crate::Error> {
|
||||
Self::new(self.config.clone()).await
|
||||
}
|
||||
|
||||
pub fn generate_headers(
|
||||
&self,
|
||||
cpd: bool,
|
||||
client_info: bool,
|
||||
app_info: bool,
|
||||
) -> HashMap<String, String> {
|
||||
if !self.is_valid() {
|
||||
panic!("Invalid data!")
|
||||
}
|
||||
let mut headers = self.base_headers.clone();
|
||||
let old_client_info = headers.remove("X-Mme-Client-Info");
|
||||
if client_info {
|
||||
let client_info = match old_client_info {
|
||||
Some(v) => {
|
||||
let temp = v.as_str();
|
||||
|
||||
temp.replace(
|
||||
temp.split('<').nth(3).unwrap().split('>').nth(0).unwrap(),
|
||||
"com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)",
|
||||
)
|
||||
}
|
||||
None => {
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
headers.insert("X-Mme-Client-Info".to_owned(), client_info.to_owned());
|
||||
}
|
||||
|
||||
if app_info {
|
||||
headers.insert(
|
||||
"X-Apple-App-Info".to_owned(),
|
||||
"com.apple.gs.xcode.auth".to_owned(),
|
||||
);
|
||||
headers.insert("X-Xcode-Version".to_owned(), "14.2 (14C18)".to_owned());
|
||||
}
|
||||
|
||||
if cpd {
|
||||
headers.insert("bootstrap".to_owned(), "true".to_owned());
|
||||
headers.insert("icscrec".to_owned(), "true".to_owned());
|
||||
headers.insert("loc".to_owned(), "en_GB".to_owned());
|
||||
headers.insert("pbe".to_owned(), "false".to_owned());
|
||||
headers.insert("prkgen".to_owned(), "true".to_owned());
|
||||
headers.insert("svct".to_owned(), "iCloud".to_owned());
|
||||
}
|
||||
|
||||
headers
|
||||
}
|
||||
|
||||
pub fn to_plist(&self, cpd: bool, client_info: bool, app_info: bool) -> plist::Dictionary {
|
||||
let mut plist = plist::Dictionary::new();
|
||||
for (key, value) in self.generate_headers(cpd, client_info, app_info).iter() {
|
||||
plist.insert(key.to_owned(), plist::Value::String(value.to_owned()));
|
||||
}
|
||||
|
||||
plist
|
||||
}
|
||||
|
||||
pub fn get_header(&self, header: &str) -> Result<String, Error> {
|
||||
let headers = self
|
||||
.generate_headers(true, true, true)
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_lowercase(), v.to_lowercase()))
|
||||
.collect::<HashMap<String, String>>();
|
||||
|
||||
match headers.get(&header.to_lowercase()) {
|
||||
Some(v) => Ok(v.to_string()),
|
||||
None => Err(Error::Parse),
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
apple-private-apis/icloud-auth/src/apple_root.der
Normal file
BIN
apple-private-apis/icloud-auth/src/apple_root.der
Normal file
Binary file not shown.
827
apple-private-apis/icloud-auth/src/client.rs
Normal file
827
apple-private-apis/icloud-auth/src/client.rs
Normal file
@@ -0,0 +1,827 @@
|
||||
use crate::{anisette::AnisetteData, Error};
|
||||
use aes::cipher::block_padding::Pkcs7;
|
||||
use botan::Cipher;
|
||||
use cbc::cipher::{BlockDecryptMut, KeyIvInit};
|
||||
use hmac::{Hmac, Mac};
|
||||
use omnisette::AnisetteConfiguration;
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderName, HeaderValue},
|
||||
Certificate, Client, ClientBuilder, Response,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use srp::{
|
||||
client::{SrpClient, SrpClientVerifier},
|
||||
groups::G_2048,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const GSA_ENDPOINT: &str = "https://gsa.apple.com/grandslam/GsService2";
|
||||
const APPLE_ROOT: &[u8] = include_bytes!("./apple_root.der");
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InitRequestBody {
|
||||
#[serde(rename = "A2k")]
|
||||
a_pub: plist::Value,
|
||||
cpd: plist::Dictionary,
|
||||
#[serde(rename = "o")]
|
||||
operation: String,
|
||||
ps: Vec<String>,
|
||||
#[serde(rename = "u")]
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct RequestHeader {
|
||||
#[serde(rename = "Version")]
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InitRequest {
|
||||
#[serde(rename = "Header")]
|
||||
header: RequestHeader,
|
||||
#[serde(rename = "Request")]
|
||||
request: InitRequestBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ChallengeRequestBody {
|
||||
#[serde(rename = "M1")]
|
||||
m: plist::Value,
|
||||
cpd: plist::Dictionary,
|
||||
c: String,
|
||||
#[serde(rename = "o")]
|
||||
operation: String,
|
||||
#[serde(rename = "u")]
|
||||
username: String,
|
||||
}
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ChallengeRequest {
|
||||
#[serde(rename = "Header")]
|
||||
header: RequestHeader,
|
||||
#[serde(rename = "Request")]
|
||||
request: ChallengeRequestBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthTokenRequestBody {
|
||||
app: Vec<String>,
|
||||
c: plist::Value,
|
||||
cpd: plist::Dictionary,
|
||||
#[serde(rename = "o")]
|
||||
operation: String,
|
||||
t: String,
|
||||
u: String,
|
||||
checksum: plist::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthTokenRequest {
|
||||
#[serde(rename = "Header")]
|
||||
header: RequestHeader,
|
||||
#[serde(rename = "Request")]
|
||||
request: AuthTokenRequestBody,
|
||||
}
|
||||
|
||||
pub struct AppleAccount {
|
||||
//TODO: move this to omnisette
|
||||
pub anisette: Mutex<AnisetteData>,
|
||||
// pub spd: Option<plist::Dictionary>,
|
||||
//mutable spd
|
||||
pub spd: Option<plist::Dictionary>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppToken {
|
||||
pub app_tokens: plist::Dictionary,
|
||||
pub auth_token: String,
|
||||
pub app: String,
|
||||
}
|
||||
//Just make it return a custom enum, with LoggedIn(account: AppleAccount) or Needs2FA(FinishLoginDel: fn(i32) -> TFAResponse)
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
pub enum LoginState {
|
||||
LoggedIn,
|
||||
// NeedsSMS2FASent(Send2FAToDevices),
|
||||
NeedsDevice2FA,
|
||||
Needs2FAVerification,
|
||||
NeedsSMS2FA,
|
||||
NeedsSMS2FAVerification(VerifyBody),
|
||||
NeedsExtraStep(String),
|
||||
NeedsLogin,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct VerifyCode {
|
||||
code: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
struct PhoneNumber {
|
||||
id: u32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VerifyBody {
|
||||
phone_number: PhoneNumber,
|
||||
mode: String,
|
||||
security_code: Option<VerifyCode>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TrustedPhoneNumber {
|
||||
pub number_with_dial_code: String,
|
||||
pub last_two_digits: String,
|
||||
pub push_mode: String,
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthenticationExtras {
|
||||
pub trusted_phone_numbers: Vec<TrustedPhoneNumber>,
|
||||
pub recovery_url: Option<String>,
|
||||
pub cant_use_phone_number_url: Option<String>,
|
||||
pub dont_have_access_url: Option<String>,
|
||||
pub recovery_web_url: Option<String>,
|
||||
pub repair_phone_number_url: Option<String>,
|
||||
pub repair_phone_number_web_url: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub new_state: Option<LoginState>,
|
||||
}
|
||||
|
||||
async fn parse_response(
|
||||
res: Result<Response, reqwest::Error>,
|
||||
) -> Result<plist::Dictionary, crate::Error> {
|
||||
let res = res?.text().await?;
|
||||
let res: plist::Dictionary = plist::from_bytes(res.as_bytes())?;
|
||||
let res: plist::Value = res.get("Response").unwrap().to_owned();
|
||||
match res {
|
||||
plist::Value::Dictionary(dict) => Ok(dict),
|
||||
_ => Err(crate::Error::Parse),
|
||||
}
|
||||
}
|
||||
|
||||
impl AppleAccount {
|
||||
pub async fn new(config: AnisetteConfiguration) -> Result<Self, crate::Error> {
|
||||
let anisette = AnisetteData::new(config).await?;
|
||||
Ok(Self::new_with_anisette(anisette)?)
|
||||
}
|
||||
|
||||
pub fn new_with_anisette(anisette: AnisetteData) -> Result<Self, crate::Error> {
|
||||
let client = ClientBuilder::new()
|
||||
.add_root_certificate(Certificate::from_der(APPLE_ROOT)?)
|
||||
// uncomment when debugging w/ charles proxy
|
||||
// .danger_accept_invalid_certs(true)
|
||||
.http1_title_case_headers()
|
||||
.connection_verbose(true)
|
||||
.build()?;
|
||||
|
||||
Ok(AppleAccount {
|
||||
client,
|
||||
anisette: Mutex::new(anisette),
|
||||
spd: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
appleid_closure: impl Fn() -> Result<(String, String), String>,
|
||||
tfa_closure: impl Fn() -> Result<String, String>,
|
||||
config: AnisetteConfiguration,
|
||||
) -> Result<AppleAccount, Error> {
|
||||
let anisette = AnisetteData::new(config).await?;
|
||||
AppleAccount::login_with_anisette(appleid_closure, tfa_closure, anisette).await
|
||||
}
|
||||
|
||||
pub async fn get_anisette(&self) -> AnisetteData {
|
||||
let mut locked = self.anisette.lock().await;
|
||||
if locked.needs_refresh() {
|
||||
*locked = locked.refresh().await.unwrap();
|
||||
}
|
||||
locked.clone()
|
||||
}
|
||||
|
||||
pub async fn get_app_token(&self, app_name: &str) -> Result<AppToken, Error> {
|
||||
let spd = self.spd.as_ref().unwrap();
|
||||
let dsid = spd.get("adsid").unwrap().as_string().unwrap();
|
||||
let auth_token = spd.get("GsIdmsToken").unwrap().as_string().unwrap();
|
||||
|
||||
let valid_anisette = self.get_anisette().await;
|
||||
|
||||
let sk = spd.get("sk").unwrap().as_data().unwrap();
|
||||
let c = spd.get("c").unwrap().as_data().unwrap();
|
||||
|
||||
let checksum = Self::create_checksum(&sk.to_vec(), dsid, app_name);
|
||||
|
||||
let mut gsa_headers = HeaderMap::new();
|
||||
gsa_headers.insert(
|
||||
"Content-Type",
|
||||
HeaderValue::from_str("text/x-xml-plist").unwrap(),
|
||||
);
|
||||
gsa_headers.insert("Accept", HeaderValue::from_str("*/*").unwrap());
|
||||
gsa_headers.insert(
|
||||
"User-Agent",
|
||||
HeaderValue::from_str("akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0").unwrap(),
|
||||
);
|
||||
gsa_headers.insert(
|
||||
"X-MMe-Client-Info",
|
||||
HeaderValue::from_str(&valid_anisette.get_header("x-mme-client-info")?).unwrap(),
|
||||
);
|
||||
|
||||
let header = RequestHeader {
|
||||
version: "1.0.1".to_string(),
|
||||
};
|
||||
let body = AuthTokenRequestBody {
|
||||
cpd: valid_anisette.to_plist(true, false, false),
|
||||
app: vec![app_name.to_string()],
|
||||
c: plist::Value::Data(c.to_vec()),
|
||||
operation: "apptokens".to_owned(),
|
||||
t: auth_token.to_string(),
|
||||
u: dsid.to_string(),
|
||||
checksum: plist::Value::Data(checksum),
|
||||
};
|
||||
|
||||
let packet = AuthTokenRequest {
|
||||
header: header.clone(),
|
||||
request: body,
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
plist::to_writer_xml(&mut buffer, &packet)?;
|
||||
let buffer = String::from_utf8(buffer).unwrap();
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(GSA_ENDPOINT)
|
||||
.headers(gsa_headers.clone())
|
||||
.body(buffer)
|
||||
.send()
|
||||
.await;
|
||||
let res = parse_response(res).await?;
|
||||
let err_check = Self::check_error(&res);
|
||||
if err_check.is_err() {
|
||||
return Err(err_check.err().unwrap());
|
||||
}
|
||||
|
||||
// --- D code logic starts here ---
|
||||
let encrypted_token = res
|
||||
.get("et")
|
||||
.ok_or(Error::Parse)?
|
||||
.as_data()
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
if encrypted_token.len() < 3 + 16 + 16 {
|
||||
return Err(Error::Parse);
|
||||
}
|
||||
let header = &encrypted_token[0..3];
|
||||
if header != b"XYZ" {
|
||||
return Err(Error::AuthSrpWithMessage(
|
||||
0,
|
||||
"Encrypted token is in an unknown format.".to_string(),
|
||||
));
|
||||
}
|
||||
let iv = &encrypted_token[3..19]; // 16 bytes
|
||||
let ciphertext_and_tag = &encrypted_token[19..];
|
||||
|
||||
if sk.len() != 32 {
|
||||
return Err(Error::Parse);
|
||||
}
|
||||
if iv.len() != 16 {
|
||||
return Err(Error::Parse);
|
||||
}
|
||||
|
||||
// Botan AES-256/GCM decryption with 16-byte IV and 3-byte AAD
|
||||
// true = encrypt, false = decrypt
|
||||
let mut cipher = Cipher::new("AES-256/GCM", botan::CipherDirection::Decrypt)
|
||||
.map_err(|_| Error::Parse)?;
|
||||
cipher.set_key(sk).map_err(|_| Error::Parse)?;
|
||||
cipher
|
||||
.set_associated_data(header)
|
||||
.map_err(|_| Error::Parse)?;
|
||||
cipher.start(iv).map_err(|_| Error::Parse)?;
|
||||
|
||||
let mut buf = ciphertext_and_tag.to_vec();
|
||||
buf = cipher.finish(&mut buf).map_err(|_| {
|
||||
Error::AuthSrpWithMessage(
|
||||
0,
|
||||
"Failed to decrypt app token (Botan AES-256/GCM).".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let decrypted_token: plist::Dictionary =
|
||||
plist::from_bytes(&buf).map_err(|_| Error::Parse)?;
|
||||
|
||||
let t_val = decrypted_token.get("t").ok_or(Error::Parse)?;
|
||||
let app_tokens = t_val.as_dictionary().ok_or(Error::Parse)?;
|
||||
let app_token_dict = app_tokens.get(app_name).ok_or(Error::Parse)?;
|
||||
let app_token = app_token_dict.as_dictionary().ok_or(Error::Parse)?;
|
||||
let token = app_token
|
||||
.get("token")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
Ok(AppToken {
|
||||
app_tokens: app_tokens.clone(),
|
||||
auth_token: token.to_string(),
|
||||
app: app_name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn create_checksum(session_key: &Vec<u8>, dsid: &str, app_name: &str) -> Vec<u8> {
|
||||
Hmac::<Sha256>::new_from_slice(&session_key)
|
||||
.unwrap()
|
||||
.chain_update("apptokens".as_bytes())
|
||||
.chain_update(dsid.as_bytes())
|
||||
.chain_update(app_name.as_bytes())
|
||||
.finalize()
|
||||
.into_bytes()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `appleid_closure` - A closure that takes no arguments and returns a tuple of the Apple ID and password
|
||||
/// * `tfa_closure` - A closure that takes no arguments and returns the 2FA code
|
||||
/// * `anisette` - AnisetteData
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use icloud_auth::AppleAccount;
|
||||
/// use omnisette::AnisetteData;
|
||||
///
|
||||
/// let anisette = AnisetteData::new();
|
||||
/// let account = AppleAccount::login(
|
||||
/// || ("test@waffle.me", "password")
|
||||
/// || "123123",
|
||||
/// anisette
|
||||
/// );
|
||||
/// ```
|
||||
/// Note: You would not provide the 2FA code like this, you would have to actually ask input for it.
|
||||
//TODO: add login_with_anisette and login, where login autodetcts anisette
|
||||
pub async fn login_with_anisette<F: Fn() -> Result<(String, String), String>, G: Fn() -> Result<String, String>>(
|
||||
appleid_closure: F,
|
||||
tfa_closure: G,
|
||||
anisette: AnisetteData,
|
||||
) -> Result<AppleAccount, Error> {
|
||||
let mut _self = AppleAccount::new_with_anisette(anisette)?;
|
||||
let (username, password) = appleid_closure().map_err(|e| {
|
||||
Error::AuthSrpWithMessage(0, format!("Failed to get Apple ID credentials: {}", e))
|
||||
})?;
|
||||
let mut response = _self.login_email_pass(&username, &password).await?;
|
||||
loop {
|
||||
match response {
|
||||
LoginState::NeedsDevice2FA => response = _self.send_2fa_to_devices().await?,
|
||||
LoginState::Needs2FAVerification => {
|
||||
response = _self.verify_2fa(tfa_closure().map_err(|e| {
|
||||
Error::AuthSrpWithMessage(0, format!("Failed to get 2FA code: {}", e))
|
||||
})?).await?
|
||||
}
|
||||
LoginState::NeedsSMS2FA => response = _self.send_sms_2fa_to_devices(1).await?,
|
||||
LoginState::NeedsSMS2FAVerification(body) => {
|
||||
response = _self.verify_sms_2fa(tfa_closure().map_err(|e| {
|
||||
Error::AuthSrpWithMessage(0, format!("Failed to get SMS 2FA code: {}", e))
|
||||
})?, body).await?
|
||||
}
|
||||
LoginState::NeedsLogin => {
|
||||
response = _self.login_email_pass(&username, &password).await?
|
||||
}
|
||||
LoginState::LoggedIn => return Ok(_self),
|
||||
LoginState::NeedsExtraStep(step) => {
|
||||
if _self.get_pet().is_some() {
|
||||
return Ok(_self);
|
||||
} else {
|
||||
return Err(Error::ExtraStep(step));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_pet(&self) -> Option<String> {
|
||||
let Some(token) = self.spd.as_ref().unwrap().get("t") else {
|
||||
return None;
|
||||
};
|
||||
Some(
|
||||
token
|
||||
.as_dictionary()
|
||||
.unwrap()
|
||||
.get("com.apple.gs.idms.pet")
|
||||
.unwrap()
|
||||
.as_dictionary()
|
||||
.unwrap()
|
||||
.get("token")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> (String, String) {
|
||||
(
|
||||
self.spd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get("fn")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
self.spd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get("ln")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn login_email_pass(
|
||||
&mut self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<LoginState, Error> {
|
||||
let srp_client = SrpClient::<Sha256>::new(&G_2048);
|
||||
let a: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
|
||||
let a_pub = srp_client.compute_public_ephemeral(&a);
|
||||
|
||||
let valid_anisette = self.get_anisette().await;
|
||||
|
||||
let mut gsa_headers = HeaderMap::new();
|
||||
gsa_headers.insert(
|
||||
"Content-Type",
|
||||
HeaderValue::from_str("text/x-xml-plist").unwrap(),
|
||||
);
|
||||
gsa_headers.insert("Accept", HeaderValue::from_str("*/*").unwrap());
|
||||
gsa_headers.insert(
|
||||
"User-Agent",
|
||||
HeaderValue::from_str("akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0").unwrap(),
|
||||
);
|
||||
gsa_headers.insert(
|
||||
"X-MMe-Client-Info",
|
||||
HeaderValue::from_str(&valid_anisette.get_header("x-mme-client-info")?).unwrap(),
|
||||
);
|
||||
|
||||
let header = RequestHeader {
|
||||
version: "1.0.1".to_string(),
|
||||
};
|
||||
let body = InitRequestBody {
|
||||
a_pub: plist::Value::Data(a_pub),
|
||||
cpd: valid_anisette.to_plist(true, false, false),
|
||||
operation: "init".to_string(),
|
||||
ps: vec!["s2k".to_string(), "s2k_fo".to_string()],
|
||||
username: username.to_string(),
|
||||
};
|
||||
|
||||
let packet = InitRequest {
|
||||
header: header.clone(),
|
||||
request: body,
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
plist::to_writer_xml(&mut buffer, &packet)?;
|
||||
let buffer = String::from_utf8(buffer).unwrap();
|
||||
|
||||
// println!("{:?}", gsa_headers.clone());
|
||||
// println!("{:?}", buffer);
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(GSA_ENDPOINT)
|
||||
.headers(gsa_headers.clone())
|
||||
.body(buffer)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let res = parse_response(res).await?;
|
||||
let err_check = Self::check_error(&res);
|
||||
if err_check.is_err() {
|
||||
return Err(err_check.err().unwrap());
|
||||
}
|
||||
// println!("{:?}", res);
|
||||
let salt = res.get("s").unwrap().as_data().unwrap();
|
||||
let b_pub = res.get("B").unwrap().as_data().unwrap();
|
||||
let iters = res.get("i").unwrap().as_signed_integer().unwrap();
|
||||
let c = res.get("c").unwrap().as_string().unwrap();
|
||||
|
||||
let hashed_password = Sha256::digest(password.as_bytes());
|
||||
|
||||
let mut password_buf = [0u8; 32];
|
||||
pbkdf2::pbkdf2::<hmac::Hmac<Sha256>>(
|
||||
&hashed_password,
|
||||
salt,
|
||||
iters as u32,
|
||||
&mut password_buf,
|
||||
);
|
||||
|
||||
let verifier: SrpClientVerifier<Sha256> = srp_client
|
||||
.process_reply(&a, &username.as_bytes(), &password_buf, salt, b_pub)
|
||||
.unwrap();
|
||||
|
||||
let m = verifier.proof();
|
||||
|
||||
let body = ChallengeRequestBody {
|
||||
m: plist::Value::Data(m.to_vec()),
|
||||
c: c.to_string(),
|
||||
cpd: valid_anisette.to_plist(true, false, false),
|
||||
operation: "complete".to_string(),
|
||||
username: username.to_string(),
|
||||
};
|
||||
|
||||
let packet = ChallengeRequest {
|
||||
header,
|
||||
request: body,
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
plist::to_writer_xml(&mut buffer, &packet)?;
|
||||
let buffer = String::from_utf8(buffer).unwrap();
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post(GSA_ENDPOINT)
|
||||
.headers(gsa_headers.clone())
|
||||
.body(buffer)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let res = parse_response(res).await?;
|
||||
let err_check = Self::check_error(&res);
|
||||
if err_check.is_err() {
|
||||
return Err(err_check.err().unwrap());
|
||||
}
|
||||
// println!("{:?}", res);
|
||||
let m2 = res.get("M2").unwrap().as_data().unwrap();
|
||||
verifier.verify_server(&m2).unwrap();
|
||||
|
||||
let spd = res.get("spd").unwrap().as_data().unwrap();
|
||||
let decrypted_spd = Self::decrypt_cbc(&verifier, spd);
|
||||
let decoded_spd: plist::Dictionary = plist::from_bytes(&decrypted_spd).unwrap();
|
||||
|
||||
let status = res.get("Status").unwrap().as_dictionary().unwrap();
|
||||
|
||||
self.spd = Some(decoded_spd);
|
||||
|
||||
if let Some(plist::Value::String(s)) = status.get("au") {
|
||||
return match s.as_str() {
|
||||
"trustedDeviceSecondaryAuth" => Ok(LoginState::NeedsDevice2FA),
|
||||
"secondaryAuth" => Ok(LoginState::NeedsSMS2FA),
|
||||
_unk => Ok(LoginState::NeedsExtraStep(_unk.to_string())),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(LoginState::LoggedIn)
|
||||
}
|
||||
|
||||
fn create_session_key(usr: &SrpClientVerifier<Sha256>, name: &str) -> Vec<u8> {
|
||||
Hmac::<Sha256>::new_from_slice(&usr.key())
|
||||
.unwrap()
|
||||
.chain_update(name.as_bytes())
|
||||
.finalize()
|
||||
.into_bytes()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
fn decrypt_cbc(usr: &SrpClientVerifier<Sha256>, data: &[u8]) -> Vec<u8> {
|
||||
let extra_data_key = Self::create_session_key(usr, "extra data key:");
|
||||
let extra_data_iv = Self::create_session_key(usr, "extra data iv:");
|
||||
let extra_data_iv = &extra_data_iv[..16];
|
||||
|
||||
cbc::Decryptor::<aes::Aes256>::new_from_slices(&extra_data_key, extra_data_iv)
|
||||
.unwrap()
|
||||
.decrypt_padded_vec_mut::<Pkcs7>(&data)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn send_2fa_to_devices(&self) -> Result<LoginState, crate::Error> {
|
||||
let headers = self.build_2fa_headers(false);
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.get("https://gsa.apple.com/auth/verify/trusteddevice")
|
||||
.headers(headers.await)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !res.status().is_success() {
|
||||
return Err(Error::AuthSrp);
|
||||
}
|
||||
|
||||
return Ok(LoginState::Needs2FAVerification);
|
||||
}
|
||||
|
||||
pub async fn send_sms_2fa_to_devices(&self, phone_id: u32) -> Result<LoginState, crate::Error> {
|
||||
let headers = self.build_2fa_headers(true);
|
||||
|
||||
let body = VerifyBody {
|
||||
phone_number: PhoneNumber { id: phone_id },
|
||||
mode: "sms".to_string(),
|
||||
security_code: None,
|
||||
};
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.put("https://gsa.apple.com/auth/verify/phone/")
|
||||
.headers(headers.await)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !res.status().is_success() {
|
||||
return Err(Error::AuthSrp);
|
||||
}
|
||||
|
||||
return Ok(LoginState::NeedsSMS2FAVerification(body));
|
||||
}
|
||||
|
||||
pub async fn get_auth_extras(&self) -> Result<AuthenticationExtras, Error> {
|
||||
let headers = self.build_2fa_headers(true);
|
||||
|
||||
let req = self
|
||||
.client
|
||||
.get("https://gsa.apple.com/auth")
|
||||
.headers(headers.await)
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
let status = req.status().as_u16();
|
||||
let mut new_state = req.json::<AuthenticationExtras>().await?;
|
||||
if status == 201 {
|
||||
new_state.new_state = Some(LoginState::NeedsSMS2FAVerification(VerifyBody {
|
||||
phone_number: PhoneNumber {
|
||||
id: new_state.trusted_phone_numbers.first().unwrap().id,
|
||||
},
|
||||
mode: "sms".to_string(),
|
||||
security_code: None,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(new_state)
|
||||
}
|
||||
|
||||
pub async fn verify_2fa(&self, code: String) -> Result<LoginState, Error> {
|
||||
let headers = self.build_2fa_headers(false);
|
||||
// println!("Recieved code: {}", code);
|
||||
let res = self
|
||||
.client
|
||||
.get("https://gsa.apple.com/grandslam/GsService2/validate")
|
||||
.headers(headers.await)
|
||||
.header(
|
||||
HeaderName::from_str("security-code").unwrap(),
|
||||
HeaderValue::from_str(&code).unwrap(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let res: plist::Dictionary = plist::from_bytes(res.text().await?.as_bytes())?;
|
||||
|
||||
Self::check_error(&res)?;
|
||||
|
||||
Ok(LoginState::NeedsLogin)
|
||||
}
|
||||
|
||||
pub async fn verify_sms_2fa(
|
||||
&self,
|
||||
code: String,
|
||||
mut body: VerifyBody,
|
||||
) -> Result<LoginState, Error> {
|
||||
let headers = self.build_2fa_headers(true).await;
|
||||
// println!("Recieved code: {}", code);
|
||||
|
||||
body.security_code = Some(VerifyCode { code });
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.post("https://gsa.apple.com/auth/verify/phone/securitycode")
|
||||
.headers(headers)
|
||||
.header("accept", "application/json")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status() != 200 {
|
||||
return Err(Error::Bad2faCode);
|
||||
}
|
||||
|
||||
Ok(LoginState::NeedsLogin)
|
||||
}
|
||||
|
||||
fn check_error(res: &plist::Dictionary) -> Result<(), Error> {
|
||||
let res = match res.get("Status") {
|
||||
Some(plist::Value::Dictionary(d)) => d,
|
||||
_ => &res,
|
||||
};
|
||||
|
||||
if res.get("ec").unwrap().as_signed_integer().unwrap() != 0 {
|
||||
return Err(Error::AuthSrpWithMessage(
|
||||
res.get("ec").unwrap().as_signed_integer().unwrap(),
|
||||
res.get("em").unwrap().as_string().unwrap().to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn build_2fa_headers(&self, sms: bool) -> HeaderMap {
|
||||
let spd = self.spd.as_ref().unwrap();
|
||||
let dsid = spd.get("adsid").unwrap().as_string().unwrap();
|
||||
let token = spd.get("GsIdmsToken").unwrap().as_string().unwrap();
|
||||
|
||||
let identity_token = base64::encode(format!("{}:{}", dsid, token));
|
||||
|
||||
let valid_anisette = self.get_anisette().await;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
valid_anisette
|
||||
.generate_headers(false, true, true)
|
||||
.iter()
|
||||
.for_each(|(k, v)| {
|
||||
headers.append(
|
||||
HeaderName::from_bytes(k.as_bytes()).unwrap(),
|
||||
HeaderValue::from_str(v).unwrap(),
|
||||
);
|
||||
});
|
||||
|
||||
if !sms {
|
||||
headers.insert(
|
||||
"Content-Type",
|
||||
HeaderValue::from_str("text/x-xml-plist").unwrap(),
|
||||
);
|
||||
headers.insert("Accept", HeaderValue::from_str("text/x-xml-plist").unwrap());
|
||||
}
|
||||
headers.insert("User-Agent", HeaderValue::from_str("Xcode").unwrap());
|
||||
headers.insert("Accept-Language", HeaderValue::from_str("en-us").unwrap());
|
||||
headers.append(
|
||||
"X-Apple-Identity-Token",
|
||||
HeaderValue::from_str(&identity_token).unwrap(),
|
||||
);
|
||||
|
||||
headers.insert(
|
||||
"Loc",
|
||||
HeaderValue::from_str(&valid_anisette.get_header("x-apple-locale").unwrap()).unwrap(),
|
||||
);
|
||||
|
||||
headers
|
||||
}
|
||||
|
||||
pub async fn send_request(
|
||||
&self,
|
||||
url: &str,
|
||||
body: Option<plist::Dictionary>,
|
||||
) -> Result<plist::Dictionary, Error> {
|
||||
let spd = self.spd.as_ref().unwrap();
|
||||
let app_token = self.get_app_token("com.apple.gs.xcode.auth").await?;
|
||||
let valid_anisette = self.get_anisette().await;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", HeaderValue::from_static("text/x-xml-plist"));
|
||||
headers.insert("Accept", HeaderValue::from_static("text/x-xml-plist"));
|
||||
headers.insert("Accept-Language", HeaderValue::from_static("en-us"));
|
||||
headers.insert("User-Agent", HeaderValue::from_static("Xcode"));
|
||||
headers.insert(
|
||||
"X-Apple-I-Identity-Id",
|
||||
HeaderValue::from_str(spd.get("adsid").unwrap().as_string().unwrap()).unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
"X-Apple-GS-Token",
|
||||
HeaderValue::from_str(&app_token.auth_token).unwrap(),
|
||||
);
|
||||
|
||||
for (k, v) in valid_anisette.generate_headers(false, true, true) {
|
||||
headers.insert(
|
||||
HeaderName::from_bytes(k.as_bytes()).unwrap(),
|
||||
HeaderValue::from_str(&v).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(locale) = valid_anisette.get_header("x-apple-locale") {
|
||||
headers.insert("X-Apple-Locale", HeaderValue::from_str(&locale).unwrap());
|
||||
}
|
||||
|
||||
let response = if let Some(body) = body {
|
||||
let mut buf = Vec::new();
|
||||
plist::to_writer_xml(&mut buf, &body)?;
|
||||
self.client
|
||||
.post(url)
|
||||
.headers(headers)
|
||||
.body(buf)
|
||||
.send()
|
||||
.await?
|
||||
} else {
|
||||
self.client.get(url).headers(headers).send().await?
|
||||
};
|
||||
|
||||
let response = response.text().await?;
|
||||
|
||||
let response: plist::Dictionary = plist::from_bytes(response.as_bytes())?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
26
apple-private-apis/icloud-auth/src/lib.rs
Normal file
26
apple-private-apis/icloud-auth/src/lib.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
pub mod anisette;
|
||||
mod client;
|
||||
|
||||
pub use client::{AppleAccount, AuthenticationExtras, LoginState, TrustedPhoneNumber, VerifyBody};
|
||||
pub use omnisette::AnisetteConfiguration;
|
||||
|
||||
use thiserror::Error;
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Failed to parse the response")]
|
||||
Parse,
|
||||
#[error("Failed to authenticate.")]
|
||||
AuthSrp,
|
||||
#[error("Bad 2fa code.")]
|
||||
Bad2faCode,
|
||||
#[error("{1} ({0})")]
|
||||
AuthSrpWithMessage(i64, String),
|
||||
#[error("Please login to appleid.apple.com to fix this account")]
|
||||
ExtraStep(String),
|
||||
#[error("Failed to parse a plist {0}")]
|
||||
PlistError(#[from] plist::Error),
|
||||
#[error("Request failed {0}")]
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
#[error("Failed getting anisette data {0}")]
|
||||
ErrorGettingAnisette(#[from] omnisette::AnisetteError),
|
||||
}
|
||||
81
apple-private-apis/icloud-auth/tests/auth_debug.rs
Normal file
81
apple-private-apis/icloud-auth/tests/auth_debug.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
// use icloud_auth::ani
|
||||
use std::sync::Arc;
|
||||
|
||||
use num_bigint::BigUint;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use srp::{
|
||||
client::{SrpClient, SrpClientVerifier},
|
||||
groups::G_2048,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn auth_debug() {
|
||||
// not a real account
|
||||
let bytes_a = base64::decode("XChHXELsQ+ljxTFbvRMUsGJxiDIlOh9f8e+JzoegmVcOdAXXtPNzkHpAbAgSjyA+vXrTA93+BUu8EJ9+4xZu9g==").unwrap();
|
||||
let username = "apple3@f1sh.me";
|
||||
let password = "WaffleTest123";
|
||||
let salt = base64::decode("6fK6ailLUcp2kJswJVrKjQ==").unwrap();
|
||||
let iters = 20832;
|
||||
|
||||
let mut password_hasher = sha2::Sha256::new();
|
||||
password_hasher.update(&password.as_bytes());
|
||||
let hashed_password = password_hasher.finalize();
|
||||
// println!("Hashed password: {:?}", base64::encode(&hashed_password));
|
||||
|
||||
let mut password_buf = [0u8; 32];
|
||||
pbkdf2::pbkdf2::<hmac::Hmac<Sha256>>(
|
||||
&hashed_password,
|
||||
&salt,
|
||||
iters as u32,
|
||||
&mut password_buf,
|
||||
);
|
||||
// println!("PBKDF2 Encrypted password: {:?}",base64::encode(&password_buf));
|
||||
|
||||
let identity_hash = SrpClient::<Sha256>::compute_identity_hash(&[], &password_buf);
|
||||
let x = SrpClient::<Sha256>::compute_x(identity_hash.as_slice(), &salt);
|
||||
|
||||
// apub: N2XHuh/4P1urPoBvDocF0RCRIl2pliZYqg9p6wGH0nnJdckJPn3M00jEqoM4teqH03HjG1murdcZiNHb5YayufW//+asW01XB7nYIIVvGiUFLRypYITEKYWBQ6h2q02GaZspYJKy98V8Fwcvr0ri+al7zJo1X1aoRKINyjV5TywhhwmTleI1qJkf+JBRYKKqO1XFtOTpQsysWD3ZJdK3K78kSgT3q0kXE3oDRMiHPAO77GFJZErYTuvI6QPRbOgcrn+RKV6AsjR5tUQAoSGRdtibdZTAQijJg788qVg+OFVCNZoY9GYVxa+Ze1bPGdkkgCYicTE8iNFG9KlJ+QpKgQ==
|
||||
|
||||
let a_random = base64::decode("ywN1O32vmBogb5Fyt9M7Tn8bbzLtDDbcYgPFpSy8n9E=").unwrap();
|
||||
let client = SrpClient::<Sha256>::new(&G_2048);
|
||||
|
||||
let a_pub_compute =
|
||||
SrpClient::<Sha256>::compute_a_pub(&client, &BigUint::from_bytes_be(&a_random));
|
||||
// expect it to be same to a_pub
|
||||
println!(
|
||||
"compute a_pub: {:?}",
|
||||
base64::encode(&a_pub_compute.to_bytes_be())
|
||||
);
|
||||
|
||||
let b_pub = base64::decode("HlWxsRmNi/9DCGxYCoqCTfdSvpbx3mrgFLQfOsgf3Rojn7MQQN/g63PwlBghUcVVB4//yAaRRnz/VIByl8thA9AKuVZl8k52PAHKSh4e7TuXSeYCFr0+GYu8/hFdMDl42219uzSuOXuaKGVKq6hxEAf3n3uXXgQRkXWtLFJ5nn1wq/emf46hYAHzc/pYyvckAdh9WDCw95IXbzKD8LcPw/0ZQoydMuXgW2ZKZ52fiyEs94IZ7L5RLL7jY1nVdwtsp2fxeqiZ3DNmVZ2GdNrbJGT//160tyd2evtUtehr8ygXNzjWdjV0cc4+1F38ywSPFyieVzVTYzDywRllgo3A5A==").unwrap();
|
||||
println!("fixed b_pub: {:?}", base64::encode(&b_pub));
|
||||
println!("");
|
||||
|
||||
println!("salt: {:?} iterations: {:?}", base64::encode(&salt), iters);
|
||||
|
||||
let verifier: SrpClientVerifier<Sha256> = SrpClient::<Sha256>::process_reply(
|
||||
&client,
|
||||
&a_random,
|
||||
// &a_pub,
|
||||
username.as_bytes(),
|
||||
&password_buf,
|
||||
&salt,
|
||||
&b_pub,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let m = verifier.proof();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_n_g() {
|
||||
// println!("Print N/G test: ");
|
||||
// println!("g2048 g: {:?}", &G_2048.g);
|
||||
// println!("g2048 n: {:?}", &G_2048.n);
|
||||
}
|
||||
}
|
||||
46
apple-private-apis/icloud-auth/tests/gsa_auth.rs
Normal file
46
apple-private-apis/icloud-auth/tests/gsa_auth.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
use icloud_auth::*;
|
||||
use omnisette::AnisetteConfiguration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn gsa_auth() {
|
||||
println!("gsa auth test");
|
||||
let email = std::env::var("apple_email").unwrap_or_else(|_| {
|
||||
println!("Enter Apple email: ");
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
input.trim().to_string()
|
||||
});
|
||||
|
||||
let password = std::env::var("apple_password").unwrap_or_else(|_| {
|
||||
println!("Enter Apple password: ");
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
input.trim().to_string()
|
||||
});
|
||||
|
||||
let appleid_closure = move || Ok((email.clone(), password.clone()));
|
||||
// ask console for 2fa code, make sure it is only 6 digits, no extra characters
|
||||
let tfa_closure = || {
|
||||
println!("Enter 2FA code: ");
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
Ok(input.trim().to_string())
|
||||
};
|
||||
let acc = AppleAccount::login(
|
||||
appleid_closure,
|
||||
tfa_closure,
|
||||
AnisetteConfiguration::new()
|
||||
.set_configuration_path(PathBuf::from_str("anisette_test").unwrap()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let account = acc.unwrap();
|
||||
println!("data {:?}", account.get_name());
|
||||
println!("PET: {}", account.get_pet().unwrap());
|
||||
return;
|
||||
}
|
||||
}
|
||||
73
apple-private-apis/icloud-auth/tests/root_write.rs
Normal file
73
apple-private-apis/icloud-auth/tests/root_write.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
const APPLE_ROOT: &[u8] = &[
|
||||
48, 130, 4, 187, 48, 130, 3, 163, 160, 3, 2, 1, 2, 2, 1, 2, 48, 13, 6, 9, 42, 134, 72, 134,
|
||||
247, 13, 1, 1, 5, 5, 0, 48, 98, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 19, 48,
|
||||
17, 6, 3, 85, 4, 10, 19, 10, 65, 112, 112, 108, 101, 32, 73, 110, 99, 46, 49, 38, 48, 36,
|
||||
6, 3, 85, 4, 11, 19, 29, 65, 112, 112, 108, 101, 32, 67, 101, 114, 116, 105, 102, 105, 99,
|
||||
97, 116, 105, 111, 110, 32, 65, 117, 116, 104, 111, 114, 105, 116, 121, 49, 22, 48, 20, 6,
|
||||
3, 85, 4, 3, 19, 13, 65, 112, 112, 108, 101, 32, 82, 111, 111, 116, 32, 67, 65, 48, 30, 23,
|
||||
13, 48, 54, 48, 52, 50, 53, 50, 49, 52, 48, 51, 54, 90, 23, 13, 51, 53, 48, 50, 48, 57, 50,
|
||||
49, 52, 48, 51, 54, 90, 48, 98, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 19, 48,
|
||||
17, 6, 3, 85, 4, 10, 19, 10, 65, 112, 112, 108, 101, 32, 73, 110, 99, 46, 49, 38, 48, 36,
|
||||
6, 3, 85, 4, 11, 19, 29, 65, 112, 112, 108, 101, 32, 67, 101, 114, 116, 105, 102, 105, 99,
|
||||
97, 116, 105, 111, 110, 32, 65, 117, 116, 104, 111, 114, 105, 116, 121, 49, 22, 48, 20, 6,
|
||||
3, 85, 4, 3, 19, 13, 65, 112, 112, 108, 101, 32, 82, 111, 111, 116, 32, 67, 65, 48, 130, 1,
|
||||
34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1,
|
||||
10, 2, 130, 1, 1, 0, 228, 145, 169, 9, 31, 145, 219, 30, 71, 80, 235, 5, 237, 94, 121, 132,
|
||||
45, 235, 54, 162, 87, 76, 85, 236, 139, 25, 137, 222, 249, 75, 108, 245, 7, 171, 34, 48, 2,
|
||||
232, 24, 62, 248, 80, 9, 211, 127, 65, 168, 152, 249, 209, 202, 102, 156, 36, 107, 17, 208,
|
||||
163, 187, 228, 27, 42, 195, 31, 149, 158, 122, 12, 164, 71, 139, 91, 212, 22, 55, 51, 203,
|
||||
196, 15, 77, 206, 20, 105, 209, 201, 25, 114, 245, 93, 14, 213, 127, 95, 155, 242, 37, 3,
|
||||
186, 85, 143, 77, 93, 13, 241, 100, 53, 35, 21, 75, 21, 89, 29, 179, 148, 247, 246, 156,
|
||||
158, 207, 80, 186, 193, 88, 80, 103, 143, 8, 180, 32, 247, 203, 172, 44, 32, 111, 112, 182,
|
||||
63, 1, 48, 140, 183, 67, 207, 15, 157, 61, 243, 43, 73, 40, 26, 200, 254, 206, 181, 185,
|
||||
14, 217, 94, 28, 214, 203, 61, 181, 58, 173, 244, 15, 14, 0, 146, 11, 177, 33, 22, 46, 116,
|
||||
213, 60, 13, 219, 98, 22, 171, 163, 113, 146, 71, 83, 85, 193, 175, 47, 65, 179, 248, 251,
|
||||
227, 112, 205, 230, 163, 76, 69, 126, 31, 76, 107, 80, 150, 65, 137, 196, 116, 98, 11, 16,
|
||||
131, 65, 135, 51, 138, 129, 177, 48, 88, 236, 90, 4, 50, 140, 104, 179, 143, 29, 222, 101,
|
||||
115, 255, 103, 94, 101, 188, 73, 216, 118, 159, 51, 20, 101, 161, 119, 148, 201, 45, 2, 3,
|
||||
1, 0, 1, 163, 130, 1, 122, 48, 130, 1, 118, 48, 14, 6, 3, 85, 29, 15, 1, 1, 255, 4, 4, 3,
|
||||
2, 1, 6, 48, 15, 6, 3, 85, 29, 19, 1, 1, 255, 4, 5, 48, 3, 1, 1, 255, 48, 29, 6, 3, 85, 29,
|
||||
14, 4, 22, 4, 20, 43, 208, 105, 71, 148, 118, 9, 254, 244, 107, 141, 46, 64, 166, 247, 71,
|
||||
77, 127, 8, 94, 48, 31, 6, 3, 85, 29, 35, 4, 24, 48, 22, 128, 20, 43, 208, 105, 71, 148,
|
||||
118, 9, 254, 244, 107, 141, 46, 64, 166, 247, 71, 77, 127, 8, 94, 48, 130, 1, 17, 6, 3, 85,
|
||||
29, 32, 4, 130, 1, 8, 48, 130, 1, 4, 48, 130, 1, 0, 6, 9, 42, 134, 72, 134, 247, 99, 100,
|
||||
5, 1, 48, 129, 242, 48, 42, 6, 8, 43, 6, 1, 5, 5, 7, 2, 1, 22, 30, 104, 116, 116, 112, 115,
|
||||
58, 47, 47, 119, 119, 119, 46, 97, 112, 112, 108, 101, 46, 99, 111, 109, 47, 97, 112, 112,
|
||||
108, 101, 99, 97, 47, 48, 129, 195, 6, 8, 43, 6, 1, 5, 5, 7, 2, 2, 48, 129, 182, 26, 129,
|
||||
179, 82, 101, 108, 105, 97, 110, 99, 101, 32, 111, 110, 32, 116, 104, 105, 115, 32, 99,
|
||||
101, 114, 116, 105, 102, 105, 99, 97, 116, 101, 32, 98, 121, 32, 97, 110, 121, 32, 112, 97,
|
||||
114, 116, 121, 32, 97, 115, 115, 117, 109, 101, 115, 32, 97, 99, 99, 101, 112, 116, 97,
|
||||
110, 99, 101, 32, 111, 102, 32, 116, 104, 101, 32, 116, 104, 101, 110, 32, 97, 112, 112,
|
||||
108, 105, 99, 97, 98, 108, 101, 32, 115, 116, 97, 110, 100, 97, 114, 100, 32, 116, 101,
|
||||
114, 109, 115, 32, 97, 110, 100, 32, 99, 111, 110, 100, 105, 116, 105, 111, 110, 115, 32,
|
||||
111, 102, 32, 117, 115, 101, 44, 32, 99, 101, 114, 116, 105, 102, 105, 99, 97, 116, 101,
|
||||
32, 112, 111, 108, 105, 99, 121, 32, 97, 110, 100, 32, 99, 101, 114, 116, 105, 102, 105,
|
||||
99, 97, 116, 105, 111, 110, 32, 112, 114, 97, 99, 116, 105, 99, 101, 32, 115, 116, 97, 116,
|
||||
101, 109, 101, 110, 116, 115, 46, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 5, 5, 0,
|
||||
3, 130, 1, 1, 0, 92, 54, 153, 76, 45, 120, 183, 237, 140, 155, 220, 243, 119, 155, 242,
|
||||
118, 210, 119, 48, 79, 193, 31, 133, 131, 133, 27, 153, 61, 71, 55, 242, 169, 155, 64, 142,
|
||||
44, 212, 177, 144, 18, 216, 190, 244, 115, 155, 238, 210, 100, 15, 203, 121, 79, 52, 216,
|
||||
162, 62, 249, 120, 255, 107, 200, 7, 236, 125, 57, 131, 139, 83, 32, 211, 56, 196, 177,
|
||||
191, 154, 79, 10, 107, 255, 43, 252, 89, 167, 5, 9, 124, 23, 64, 86, 17, 30, 116, 211, 183,
|
||||
139, 35, 59, 71, 163, 213, 111, 36, 226, 235, 209, 183, 112, 223, 15, 69, 225, 39, 202,
|
||||
241, 109, 120, 237, 231, 181, 23, 23, 168, 220, 126, 34, 53, 202, 37, 213, 217, 15, 214,
|
||||
107, 212, 162, 36, 35, 17, 247, 161, 172, 143, 115, 129, 96, 198, 27, 91, 9, 47, 146, 178,
|
||||
248, 68, 72, 240, 96, 56, 158, 21, 245, 61, 38, 103, 32, 138, 51, 106, 247, 13, 130, 207,
|
||||
222, 235, 163, 47, 249, 83, 106, 91, 100, 192, 99, 51, 119, 247, 58, 7, 44, 86, 235, 218,
|
||||
15, 33, 14, 218, 186, 115, 25, 79, 181, 217, 54, 127, 193, 135, 85, 217, 167, 153, 185, 50,
|
||||
66, 251, 216, 213, 113, 158, 126, 161, 82, 183, 27, 189, 147, 66, 36, 18, 42, 199, 15, 29,
|
||||
182, 77, 156, 94, 99, 200, 75, 128, 23, 80, 170, 138, 213, 218, 228, 252, 208, 9, 7, 55,
|
||||
176, 117, 117, 33,
|
||||
];
|
||||
#[test]
|
||||
|
||||
fn root_write() {
|
||||
// write to file src/root.der
|
||||
let mut file = File::create("src/apple_root.der").unwrap();
|
||||
file.write_all(&APPLE_ROOT).unwrap();
|
||||
}
|
||||
}
|
||||
40
apple-private-apis/omnisette/Cargo.toml
Normal file
40
apple-private-apis/omnisette/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "omnisette"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
remote-anisette = []
|
||||
async = ["dep:async-trait"]
|
||||
default = ["remote-anisette", "dep:remove-async-await"]
|
||||
remote-anisette-v3 = ["async", "dep:serde", "dep:serde_json", "dep:tokio-tungstenite", "dep:futures-util", "dep:chrono"]
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.21"
|
||||
hex = "0.4.3"
|
||||
plist = "1.4"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["blocking", "json", "rustls-tls", "gzip"] }
|
||||
rand = "0.8"
|
||||
sha2 = "0.10.8"
|
||||
uuid = { version = "1.3", features = [ "v4", "fast-rng", "macro-diagnostics" ] }
|
||||
android-loader = { git = "https://github.com/Dadoum/android-loader", branch = "bigger_pages" }
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
remove-async-await = { version = "1.0", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1.0.142", optional = true }
|
||||
tokio-tungstenite = { version = "0.20.1", optional = true, features = ["rustls-tls-webpki-roots"] }
|
||||
futures-util = { version = "0.3.28", optional = true }
|
||||
chrono = { version = "0.4.37", optional = true }
|
||||
thiserror = "1.0.58"
|
||||
anyhow = "1.0.81"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
dlopen2 = "0.4"
|
||||
objc = "0.2"
|
||||
objc-foundation = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt", "macros"] }
|
||||
simplelog = "0.12"
|
||||
381
apple-private-apis/omnisette/src/adi_proxy.rs
Normal file
381
apple-private-apis/omnisette/src/adi_proxy.rs
Normal file
@@ -0,0 +1,381 @@
|
||||
use crate::adi_proxy::ProvisioningError::InvalidResponse;
|
||||
use crate::anisette_headers_provider::AnisetteHeadersProvider;
|
||||
use crate::AnisetteError;
|
||||
use base64::engine::general_purpose::STANDARD as base64_engine;
|
||||
use base64::Engine;
|
||||
use log::debug;
|
||||
use plist::{Dictionary, Value};
|
||||
use rand::RngCore;
|
||||
#[cfg(not(feature = "async"))]
|
||||
use reqwest::blocking::{Client, ClientBuilder, Response};
|
||||
use reqwest::header::{HeaderMap, HeaderValue, InvalidHeaderValue};
|
||||
#[cfg(feature = "async")]
|
||||
use reqwest::{Client, ClientBuilder, Response};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServerError {
|
||||
pub code: i64,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProvisioningError {
|
||||
InvalidResponse,
|
||||
ServerError(ServerError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProvisioningError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ProvisioningError {}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ADIError {
|
||||
Unknown(i32),
|
||||
ProvisioningError(#[from] ProvisioningError),
|
||||
PlistError(#[from] plist::Error),
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
Base64Error(#[from] base64::DecodeError),
|
||||
InvalidHeaderValue(#[from] InvalidHeaderValue),
|
||||
IOError(#[from] io::Error)
|
||||
}
|
||||
|
||||
impl ADIError {
|
||||
pub fn resolve(error_number: i32) -> ADIError {
|
||||
ADIError::Unknown(error_number)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "async", async_trait::async_trait)]
|
||||
trait ToPlist {
|
||||
#[cfg_attr(not(feature = "async"), remove_async_await::remove_async_await)]
|
||||
async fn plist(self) -> Result<Dictionary, ADIError>;
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "async", async_trait::async_trait)]
|
||||
impl ToPlist for Response {
|
||||
#[cfg_attr(not(feature = "async"), remove_async_await::remove_async_await)]
|
||||
async fn plist(self) -> Result<Dictionary, ADIError> {
|
||||
if let Ok(property_list) = Value::from_reader_xml(&*self.bytes().await?) {
|
||||
Ok(property_list.as_dictionary().unwrap().to_owned())
|
||||
} else {
|
||||
Err(ProvisioningError::InvalidResponse.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ADIError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SynchronizeData {
|
||||
pub mid: Vec<u8>,
|
||||
pub srm: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct StartProvisioningData {
|
||||
pub cpim: Vec<u8>,
|
||||
pub session: u32,
|
||||
}
|
||||
|
||||
pub struct RequestOTPData {
|
||||
pub otp: Vec<u8>,
|
||||
pub mid: Vec<u8>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "async", async_trait::async_trait(?Send))]
|
||||
pub trait ADIProxy: Send + Sync {
|
||||
fn erase_provisioning(&mut self, ds_id: i64) -> Result<(), ADIError>;
|
||||
fn synchronize(&mut self, ds_id: i64, sim: &[u8]) -> Result<SynchronizeData, ADIError>;
|
||||
fn destroy_provisioning_session(&mut self, session: u32) -> Result<(), ADIError>;
|
||||
fn end_provisioning(&mut self, session: u32, ptm: &[u8], tk: &[u8]) -> Result<(), ADIError>;
|
||||
fn start_provisioning(
|
||||
&mut self,
|
||||
ds_id: i64,
|
||||
spim: &[u8],
|
||||
) -> Result<StartProvisioningData, ADIError>;
|
||||
fn is_machine_provisioned(&self, ds_id: i64) -> bool;
|
||||
fn request_otp(&self, ds_id: i64) -> Result<RequestOTPData, ADIError>;
|
||||
|
||||
fn set_local_user_uuid(&mut self, local_user_uuid: String);
|
||||
fn set_device_identifier(&mut self, device_identifier: String) -> Result<(), ADIError>;
|
||||
|
||||
fn get_local_user_uuid(&self) -> String;
|
||||
fn get_device_identifier(&self) -> String;
|
||||
fn get_serial_number(&self) -> String;
|
||||
}
|
||||
|
||||
pub trait ConfigurableADIProxy: ADIProxy {
|
||||
fn set_identifier(&mut self, identifier: &str) -> Result<(), ADIError>;
|
||||
fn set_provisioning_path(&mut self, path: &str) -> Result<(), ADIError>;
|
||||
}
|
||||
|
||||
pub const AKD_USER_AGENT: &str = "akd/1.0 CFNetwork/808.1.4";
|
||||
pub const CLIENT_INFO_HEADER: &str =
|
||||
"<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>";
|
||||
pub const DS_ID: i64 = -2;
|
||||
pub const IDENTIFIER_LENGTH: usize = 16;
|
||||
pub type Identifier = [u8; IDENTIFIER_LENGTH];
|
||||
|
||||
trait AppleRequestResult {
|
||||
fn check_status(&self) -> Result<(), ADIError>;
|
||||
fn get_response(&self) -> Result<&Dictionary, ADIError>;
|
||||
}
|
||||
|
||||
impl AppleRequestResult for Dictionary {
|
||||
fn check_status(&self) -> Result<(), ADIError> {
|
||||
let status = self
|
||||
.get("Status")
|
||||
.ok_or(InvalidResponse)?
|
||||
.as_dictionary()
|
||||
.unwrap();
|
||||
let code = status.get("ec").unwrap().as_signed_integer().unwrap();
|
||||
if code != 0 {
|
||||
let description = status.get("em").unwrap().as_string().unwrap().to_string();
|
||||
Err(ProvisioningError::ServerError(ServerError { code, description }).into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_response(&self) -> Result<&Dictionary, ADIError> {
|
||||
if let Some(response) = self.get("Response") {
|
||||
let response = response.as_dictionary().unwrap();
|
||||
response.check_status()?;
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(InvalidResponse.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn ADIProxy {
|
||||
fn make_http_client(&mut self) -> Result<Client, ADIError> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", HeaderValue::from_str("text/x-xml-plist")?);
|
||||
|
||||
headers.insert(
|
||||
"X-Mme-Client-Info",
|
||||
HeaderValue::from_str(CLIENT_INFO_HEADER)?,
|
||||
);
|
||||
headers.insert(
|
||||
"X-Mme-Device-Id",
|
||||
HeaderValue::from_str(self.get_device_identifier().as_str())?,
|
||||
);
|
||||
headers.insert(
|
||||
"X-Apple-I-MD-LU",
|
||||
HeaderValue::from_str(self.get_local_user_uuid().as_str())?,
|
||||
);
|
||||
headers.insert(
|
||||
"X-Apple-I-SRL-NO",
|
||||
HeaderValue::from_str(self.get_serial_number().as_str())?,
|
||||
);
|
||||
|
||||
debug!("Headers sent: {headers:?}");
|
||||
|
||||
let http_client = ClientBuilder::new()
|
||||
.http1_title_case_headers()
|
||||
.danger_accept_invalid_certs(true) // TODO: pin the apple certificate
|
||||
.user_agent(AKD_USER_AGENT)
|
||||
.default_headers(headers)
|
||||
.build()?;
|
||||
|
||||
Ok(http_client)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "async"), remove_async_await::remove_async_await)]
|
||||
async fn provision_device(&mut self) -> Result<(), ADIError> {
|
||||
let client = self.make_http_client()?;
|
||||
|
||||
let url_bag_res = client
|
||||
.get("https://gsa.apple.com/grandslam/GsService2/lookup")
|
||||
.send()
|
||||
.await?
|
||||
.plist()
|
||||
.await?;
|
||||
|
||||
let urls = url_bag_res.get("urls").unwrap().as_dictionary().unwrap();
|
||||
|
||||
let start_provisioning_url = urls
|
||||
.get("midStartProvisioning")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
let finish_provisioning_url = urls
|
||||
.get("midFinishProvisioning")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
|
||||
let mut body = plist::Dictionary::new();
|
||||
body.insert(
|
||||
"Header".to_string(),
|
||||
plist::Value::Dictionary(plist::Dictionary::new()),
|
||||
);
|
||||
body.insert(
|
||||
"Request".to_string(),
|
||||
plist::Value::Dictionary(plist::Dictionary::new()),
|
||||
);
|
||||
|
||||
let mut sp_request = Vec::new();
|
||||
plist::Value::Dictionary(body).to_writer_xml(&mut sp_request)?;
|
||||
|
||||
debug!("First provisioning request...");
|
||||
let response = client
|
||||
.post(start_provisioning_url)
|
||||
.body(sp_request)
|
||||
.send()
|
||||
.await?
|
||||
.plist()
|
||||
.await?;
|
||||
|
||||
let response = response.get_response()?;
|
||||
|
||||
let spim = response
|
||||
.get("spim")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
|
||||
let spim = base64_engine.decode(spim)?;
|
||||
let first_step = self.start_provisioning(DS_ID, spim.as_slice())?;
|
||||
|
||||
let mut body = Dictionary::new();
|
||||
let mut request = Dictionary::new();
|
||||
request.insert(
|
||||
"cpim".to_owned(),
|
||||
Value::String(base64_engine.encode(first_step.cpim)),
|
||||
);
|
||||
body.insert("Header".to_owned(), Value::Dictionary(Dictionary::new()));
|
||||
body.insert("Request".to_owned(), Value::Dictionary(request));
|
||||
|
||||
let mut fp_request = Vec::new();
|
||||
Value::Dictionary(body).to_writer_xml(&mut fp_request)?;
|
||||
|
||||
debug!("Second provisioning request...");
|
||||
let response = client
|
||||
.post(finish_provisioning_url)
|
||||
.body(fp_request)
|
||||
.send()
|
||||
.await?
|
||||
.plist()
|
||||
.await?;
|
||||
|
||||
let response = response.get_response()?;
|
||||
|
||||
let ptm = base64_engine.decode(response.get("ptm").unwrap().as_string().unwrap())?;
|
||||
let tk = base64_engine.decode(response.get("tk").unwrap().as_string().unwrap())?;
|
||||
|
||||
self.end_provisioning(first_step.session, ptm.as_slice(), tk.as_slice())?;
|
||||
debug!("Done.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ADIProxyAnisetteProvider<ProxyType: ADIProxy + 'static> {
|
||||
adi_proxy: ProxyType,
|
||||
}
|
||||
|
||||
impl<ProxyType: ADIProxy + 'static> ADIProxyAnisetteProvider<ProxyType> {
|
||||
/// If you use this method, you are expected to set the identifier yourself.
|
||||
pub fn without_identifier(adi_proxy: ProxyType) -> Result<ADIProxyAnisetteProvider<ProxyType>, ADIError> {
|
||||
Ok(ADIProxyAnisetteProvider { adi_proxy })
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
mut adi_proxy: ProxyType,
|
||||
configuration_path: PathBuf,
|
||||
) -> Result<ADIProxyAnisetteProvider<ProxyType>, ADIError> {
|
||||
let identifier_file_path = configuration_path.join("identifier");
|
||||
let mut identifier_file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(identifier_file_path)?;
|
||||
let mut identifier = [0u8; IDENTIFIER_LENGTH];
|
||||
if identifier_file.metadata()?.len() == IDENTIFIER_LENGTH as u64 {
|
||||
identifier_file.read_exact(&mut identifier)?;
|
||||
} else {
|
||||
rand::thread_rng().fill_bytes(&mut identifier);
|
||||
identifier_file.write_all(&identifier)?;
|
||||
}
|
||||
|
||||
let mut local_user_uuid_hasher = Sha256::new();
|
||||
local_user_uuid_hasher.update(identifier);
|
||||
|
||||
adi_proxy.set_device_identifier(
|
||||
uuid::Uuid::from_bytes(identifier)
|
||||
.to_string()
|
||||
.to_uppercase(),
|
||||
)?; // UUID, uppercase
|
||||
adi_proxy
|
||||
.set_local_user_uuid(hex::encode(local_user_uuid_hasher.finalize()).to_uppercase()); // 64 uppercase character hex
|
||||
|
||||
Ok(ADIProxyAnisetteProvider { adi_proxy })
|
||||
}
|
||||
|
||||
pub fn adi_proxy(&mut self) -> &mut ProxyType {
|
||||
&mut self.adi_proxy
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "async", async_trait::async_trait)]
|
||||
impl<ProxyType: ADIProxy + 'static> AnisetteHeadersProvider
|
||||
for ADIProxyAnisetteProvider<ProxyType>
|
||||
{
|
||||
#[cfg_attr(not(feature = "async"), remove_async_await::remove_async_await)]
|
||||
async fn get_anisette_headers(
|
||||
&mut self,
|
||||
skip_provisioning: bool,
|
||||
) -> Result<HashMap<String, String>, AnisetteError> {
|
||||
let adi_proxy = &mut self.adi_proxy as &mut dyn ADIProxy;
|
||||
|
||||
if !adi_proxy.is_machine_provisioned(DS_ID) && !skip_provisioning {
|
||||
adi_proxy.provision_device().await?;
|
||||
}
|
||||
|
||||
let machine_data = adi_proxy.request_otp(DS_ID)?;
|
||||
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert(
|
||||
"X-Apple-I-MD".to_string(),
|
||||
base64_engine.encode(machine_data.otp),
|
||||
);
|
||||
headers.insert(
|
||||
"X-Apple-I-MD-M".to_string(),
|
||||
base64_engine.encode(machine_data.mid),
|
||||
);
|
||||
headers.insert("X-Apple-I-MD-RINFO".to_string(), "17106176".to_string());
|
||||
headers.insert(
|
||||
"X-Apple-I-MD-LU".to_string(),
|
||||
adi_proxy.get_local_user_uuid(),
|
||||
);
|
||||
headers.insert(
|
||||
"X-Apple-I-SRL-NO".to_string(),
|
||||
adi_proxy.get_serial_number(),
|
||||
);
|
||||
headers.insert(
|
||||
"X-Mme-Client-Info".to_string(),
|
||||
CLIENT_INFO_HEADER.to_string(),
|
||||
);
|
||||
headers.insert(
|
||||
"X-Mme-Device-Id".to_string(),
|
||||
adi_proxy.get_device_identifier(),
|
||||
);
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::AnisetteError;
|
||||
|
||||
#[cfg_attr(feature = "async", async_trait::async_trait)]
|
||||
pub trait AnisetteHeadersProvider: Send + Sync {
|
||||
#[cfg_attr(not(feature = "async"), remove_async_await::remove_async_await)]
|
||||
async fn get_anisette_headers(
|
||||
&mut self,
|
||||
skip_provisioning: bool,
|
||||
) -> Result<HashMap<String, String>, AnisetteError>;
|
||||
|
||||
#[cfg_attr(not(feature = "async"), remove_async_await::remove_async_await)]
|
||||
async fn get_authentication_headers(&mut self) -> Result<HashMap<String, String>, AnisetteError> {
|
||||
let headers = self.get_anisette_headers(false).await?;
|
||||
Ok(self.normalize_headers(headers))
|
||||
}
|
||||
|
||||
/// Normalizes headers to ensure that all the required headers are given.
|
||||
fn normalize_headers(
|
||||
&mut self,
|
||||
mut headers: HashMap<String, String>,
|
||||
) -> HashMap<String, String> {
|
||||
if let Some(client_info) = headers.remove("X-MMe-Client-Info") {
|
||||
headers.insert("X-Mme-Client-Info".to_string(), client_info);
|
||||
}
|
||||
|
||||
headers
|
||||
}
|
||||
}
|
||||
124
apple-private-apis/omnisette/src/aos_kit.rs
Normal file
124
apple-private-apis/omnisette/src/aos_kit.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use crate::anisette_headers_provider::AnisetteHeadersProvider;
|
||||
use anyhow::Result;
|
||||
|
||||
use dlopen2::symbor::Library;
|
||||
use objc::{msg_send, runtime::Class, sel, sel_impl};
|
||||
use objc_foundation::{INSString, NSObject, NSString};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
pub struct AOSKitAnisetteProvider<'lt> {
|
||||
aos_utilities: &'lt Class,
|
||||
ak_device: &'lt Class,
|
||||
}
|
||||
|
||||
impl<'lt> AOSKitAnisetteProvider<'lt> {
|
||||
pub fn new() -> Result<AOSKitAnisetteProvider<'lt>> {
|
||||
Library::open("/System/Library/PrivateFrameworks/AOSKit.framework/AOSKit")?;
|
||||
Library::open("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit")?;
|
||||
Ok(AOSKitAnisetteProvider {
|
||||
aos_utilities: Class::get("AOSUtilities").ok_or(AOSKitError::ClassLoadFailed)?,
|
||||
ak_device: Class::get("AKDevice").ok_or(AOSKitError::ClassLoadFailed)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "async", async_trait::async_trait(?Send))]
|
||||
impl<'lt> AnisetteHeadersProvider for AOSKitAnisetteProvider<'lt> {
|
||||
#[cfg_attr(not(feature = "async"), remove_async_await::remove_async_await)]
|
||||
async fn get_anisette_headers(
|
||||
&mut self,
|
||||
_skip_provisioning: bool,
|
||||
) -> Result<HashMap<String, String>> {
|
||||
let mut headers_map = HashMap::new();
|
||||
|
||||
let headers: *const NSObject = unsafe {
|
||||
msg_send![self.aos_utilities, retrieveOTPHeadersForDSID: NSString::from_str("-2")]
|
||||
};
|
||||
|
||||
let otp: *const NSString =
|
||||
unsafe { msg_send![headers, valueForKey: NSString::from_str("X-Apple-MD")] };
|
||||
headers_map.insert(
|
||||
"X-Apple-I-MD".to_string(),
|
||||
unsafe { (*otp).as_str() }.to_string(),
|
||||
);
|
||||
|
||||
let mid: *const NSString =
|
||||
unsafe { msg_send![headers, valueForKey: NSString::from_str("X-Apple-MD-M")] };
|
||||
headers_map.insert(
|
||||
"X-Apple-I-MD-M".to_string(),
|
||||
unsafe { (*mid).as_str() }.to_string(),
|
||||
);
|
||||
|
||||
let machine_serial_number: *const NSString =
|
||||
unsafe { msg_send![self.aos_utilities, machineSerialNumber] };
|
||||
headers_map.insert(
|
||||
"X-Apple-SRL-NO".to_string(),
|
||||
unsafe { (*machine_serial_number).as_str() }.to_string(),
|
||||
);
|
||||
|
||||
let current_device: *const NSObject = unsafe { msg_send![self.ak_device, currentDevice] };
|
||||
|
||||
let local_user_uuid: *const NSString = unsafe { msg_send![current_device, localUserUUID] };
|
||||
headers_map.insert(
|
||||
"X-Apple-I-MD-LU".to_string(),
|
||||
unsafe { (*local_user_uuid).as_str() }.to_string(),
|
||||
);
|
||||
|
||||
let locale: *const NSObject = unsafe { msg_send![current_device, locale] };
|
||||
let locale: *const NSString = unsafe { msg_send![locale, localeIdentifier] };
|
||||
headers_map.insert(
|
||||
"X-Apple-Locale".to_string(),
|
||||
unsafe { (*locale).as_str() }.to_string(),
|
||||
); // FIXME maybe not the right header name
|
||||
|
||||
let server_friendly_description: *const NSString =
|
||||
unsafe { msg_send![current_device, serverFriendlyDescription] };
|
||||
headers_map.insert(
|
||||
"X-Mme-Client-Info".to_string(),
|
||||
unsafe { (*server_friendly_description).as_str() }.to_string(),
|
||||
);
|
||||
|
||||
let unique_device_identifier: *const NSString =
|
||||
unsafe { msg_send![current_device, uniqueDeviceIdentifier] };
|
||||
headers_map.insert(
|
||||
"X-Mme-Device-Id".to_string(),
|
||||
unsafe { (*unique_device_identifier).as_str() }.to_string(),
|
||||
);
|
||||
|
||||
Ok(headers_map)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AOSKitError {
|
||||
ClassLoadFailed,
|
||||
}
|
||||
|
||||
impl Display for AOSKitError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for AOSKitError {}
|
||||
|
||||
#[cfg(all(test, not(feature = "async")))]
|
||||
mod tests {
|
||||
use crate::anisette_headers_provider::AnisetteHeadersProvider;
|
||||
use crate::aos_kit::AOSKitAnisetteProvider;
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
|
||||
#[test]
|
||||
fn fetch_anisette_aoskit() -> Result<()> {
|
||||
crate::tests::init_logger();
|
||||
|
||||
let mut provider = AOSKitAnisetteProvider::new()?;
|
||||
info!(
|
||||
"AOSKit headers: {:?}",
|
||||
(&mut provider as &mut dyn AnisetteHeadersProvider).get_authentication_headers()?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
231
apple-private-apis/omnisette/src/lib.rs
Normal file
231
apple-private-apis/omnisette/src/lib.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
//! A library to generate "anisette" data. Docs are coming soon.
|
||||
//!
|
||||
//! If you want an async API, enable the `async` feature.
|
||||
//!
|
||||
//! If you want remote anisette, make sure the `remote-anisette` feature is enabled. (it's currently on by default)
|
||||
|
||||
use crate::adi_proxy::{ADIProxyAnisetteProvider, ConfigurableADIProxy};
|
||||
use crate::anisette_headers_provider::AnisetteHeadersProvider;
|
||||
use adi_proxy::ADIError;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod adi_proxy;
|
||||
pub mod anisette_headers_provider;
|
||||
pub mod store_services_core;
|
||||
|
||||
#[cfg(feature = "remote-anisette-v3")]
|
||||
pub mod remote_anisette_v3;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod aos_kit;
|
||||
|
||||
#[cfg(feature = "remote-anisette")]
|
||||
pub mod remote_anisette;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct AnisetteHeaders;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AnisetteError {
|
||||
#[allow(dead_code)]
|
||||
#[error("Unsupported device")]
|
||||
UnsupportedDevice,
|
||||
#[error("Invalid argument {0}")]
|
||||
InvalidArgument(String),
|
||||
#[error("Anisette not provisioned!")]
|
||||
AnisetteNotProvisioned,
|
||||
#[error("Plist serialization error {0}")]
|
||||
PlistError(#[from] plist::Error),
|
||||
#[error("Request Error {0}")]
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
#[cfg(feature = "remote-anisette-v3")]
|
||||
#[error("Provisioning socket error {0}")]
|
||||
WsError(#[from] tokio_tungstenite::tungstenite::error::Error),
|
||||
#[cfg(feature = "remote-anisette-v3")]
|
||||
#[error("JSON error {0}")]
|
||||
SerdeError(#[from] serde_json::Error),
|
||||
#[error("IO error {0}")]
|
||||
IOError(#[from] io::Error),
|
||||
#[error("ADI error {0}")]
|
||||
ADIError(#[from] ADIError),
|
||||
#[error("Invalid library format")]
|
||||
InvalidLibraryFormat,
|
||||
#[error("Misc")]
|
||||
Misc,
|
||||
#[error("Missing Libraries")]
|
||||
MissingLibraries,
|
||||
#[error("{0}")]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub const DEFAULT_ANISETTE_URL: &str = "https://ani.f1sh.me/";
|
||||
|
||||
pub const DEFAULT_ANISETTE_URL_V3: &str = "https://ani.sidestore.io";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnisetteConfiguration {
|
||||
anisette_url: String,
|
||||
anisette_url_v3: String,
|
||||
configuration_path: PathBuf,
|
||||
macos_serial: String,
|
||||
}
|
||||
|
||||
impl Default for AnisetteConfiguration {
|
||||
fn default() -> Self {
|
||||
AnisetteConfiguration::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AnisetteConfiguration {
|
||||
pub fn new() -> AnisetteConfiguration {
|
||||
AnisetteConfiguration {
|
||||
anisette_url: DEFAULT_ANISETTE_URL.to_string(),
|
||||
anisette_url_v3: DEFAULT_ANISETTE_URL_V3.to_string(),
|
||||
configuration_path: PathBuf::new(),
|
||||
macos_serial: "0".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn anisette_url(&self) -> &String {
|
||||
&self.anisette_url
|
||||
}
|
||||
|
||||
pub fn configuration_path(&self) -> &PathBuf {
|
||||
&self.configuration_path
|
||||
}
|
||||
|
||||
pub fn set_anisette_url(mut self, anisette_url: String) -> AnisetteConfiguration {
|
||||
self.anisette_url = anisette_url;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_macos_serial(mut self, macos_serial: String) -> AnisetteConfiguration {
|
||||
self.macos_serial = macos_serial;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_configuration_path(mut self, configuration_path: PathBuf) -> AnisetteConfiguration {
|
||||
self.configuration_path = configuration_path;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub enum AnisetteHeadersProviderType {
|
||||
Local,
|
||||
Remote,
|
||||
}
|
||||
|
||||
pub struct AnisetteHeadersProviderRes {
|
||||
pub provider: Box<dyn AnisetteHeadersProvider>,
|
||||
pub provider_type: AnisetteHeadersProviderType,
|
||||
}
|
||||
|
||||
impl AnisetteHeadersProviderRes {
|
||||
pub fn local(provider: Box<dyn AnisetteHeadersProvider>) -> AnisetteHeadersProviderRes {
|
||||
AnisetteHeadersProviderRes {
|
||||
provider,
|
||||
provider_type: AnisetteHeadersProviderType::Local,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remote(provider: Box<dyn AnisetteHeadersProvider>) -> AnisetteHeadersProviderRes {
|
||||
AnisetteHeadersProviderRes {
|
||||
provider,
|
||||
provider_type: AnisetteHeadersProviderType::Remote,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnisetteHeaders {
|
||||
pub fn get_anisette_headers_provider(
|
||||
configuration: AnisetteConfiguration,
|
||||
) -> Result<AnisetteHeadersProviderRes, AnisetteError> {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Ok(prov) = aos_kit::AOSKitAnisetteProvider::new() {
|
||||
return Ok(AnisetteHeadersProviderRes::local(Box::new(prov)));
|
||||
}
|
||||
|
||||
// TODO: handle Err because it will just go to remote anisette and not tell the user anything
|
||||
if let Ok(ssc_anisette_headers_provider) =
|
||||
AnisetteHeaders::get_ssc_anisette_headers_provider(configuration.clone())
|
||||
{
|
||||
return Ok(ssc_anisette_headers_provider);
|
||||
}
|
||||
|
||||
#[cfg(feature = "remote-anisette-v3")]
|
||||
return Ok(AnisetteHeadersProviderRes::remote(Box::new(
|
||||
remote_anisette_v3::RemoteAnisetteProviderV3::new(
|
||||
configuration.anisette_url_v3,
|
||||
configuration.configuration_path.clone(),
|
||||
configuration.macos_serial.clone(),
|
||||
),
|
||||
)));
|
||||
|
||||
#[cfg(feature = "remote-anisette")]
|
||||
#[allow(unreachable_code)]
|
||||
return Ok(AnisetteHeadersProviderRes::remote(Box::new(
|
||||
remote_anisette::RemoteAnisetteProvider::new(configuration.anisette_url),
|
||||
)));
|
||||
|
||||
#[cfg(not(feature = "remote-anisette"))]
|
||||
bail!(AnisetteMetaError::UnsupportedDevice)
|
||||
}
|
||||
|
||||
pub fn get_ssc_anisette_headers_provider(
|
||||
configuration: AnisetteConfiguration,
|
||||
) -> Result<AnisetteHeadersProviderRes, AnisetteError> {
|
||||
let mut ssc_adi_proxy = store_services_core::StoreServicesCoreADIProxy::new(
|
||||
configuration.configuration_path(),
|
||||
)?;
|
||||
let config_path = configuration.configuration_path();
|
||||
ssc_adi_proxy.set_provisioning_path(config_path.to_str().ok_or(
|
||||
AnisetteError::InvalidArgument("configuration.configuration_path".to_string()),
|
||||
)?)?;
|
||||
Ok(AnisetteHeadersProviderRes::local(Box::new(
|
||||
ADIProxyAnisetteProvider::new(ssc_adi_proxy, config_path.to_path_buf())?,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use log::LevelFilter;
|
||||
use simplelog::{ColorChoice, ConfigBuilder, TermLogger, TerminalMode};
|
||||
|
||||
pub fn init_logger() {
|
||||
if TermLogger::init(
|
||||
LevelFilter::Trace,
|
||||
ConfigBuilder::new()
|
||||
.set_target_level(LevelFilter::Error)
|
||||
.add_filter_allow_str("omnisette")
|
||||
.build(),
|
||||
TerminalMode::Mixed,
|
||||
ColorChoice::Auto,
|
||||
)
|
||||
.is_ok()
|
||||
{}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "async"))]
|
||||
#[test]
|
||||
fn fetch_anisette_auto() -> Result<()> {
|
||||
use crate::{AnisetteConfiguration, AnisetteHeaders};
|
||||
use log::info;
|
||||
use std::path::PathBuf;
|
||||
|
||||
crate::tests::init_logger();
|
||||
|
||||
let mut provider = AnisetteHeaders::get_anisette_headers_provider(
|
||||
AnisetteConfiguration::new()
|
||||
.set_configuration_path(PathBuf::new().join("anisette_test")),
|
||||
)?;
|
||||
info!(
|
||||
"Headers: {:?}",
|
||||
provider.provider.get_authentication_headers()?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
47
apple-private-apis/omnisette/src/remote_anisette.rs
Normal file
47
apple-private-apis/omnisette/src/remote_anisette.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::{anisette_headers_provider::AnisetteHeadersProvider, AnisetteError};
|
||||
#[cfg(not(feature = "async"))]
|
||||
use reqwest::blocking::get;
|
||||
#[cfg(feature = "async")]
|
||||
use reqwest::get;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct RemoteAnisetteProvider {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl RemoteAnisetteProvider {
|
||||
pub fn new(url: String) -> RemoteAnisetteProvider {
|
||||
RemoteAnisetteProvider { url }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "async", async_trait::async_trait)]
|
||||
impl AnisetteHeadersProvider for RemoteAnisetteProvider {
|
||||
#[cfg_attr(not(feature = "async"), remove_async_await::remove_async_await)]
|
||||
async fn get_anisette_headers(
|
||||
&mut self,
|
||||
_skip_provisioning: bool,
|
||||
) -> Result<HashMap<String, String>, AnisetteError> {
|
||||
Ok(get(&self.url).await?.json().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(feature = "async")))]
|
||||
mod tests {
|
||||
use crate::anisette_headers_provider::AnisetteHeadersProvider;
|
||||
use crate::remote_anisette::RemoteAnisetteProvider;
|
||||
use crate::DEFAULT_ANISETTE_URL;
|
||||
use log::info;
|
||||
|
||||
#[test]
|
||||
fn fetch_anisette_remote() -> Result<(), AnisetteError> {
|
||||
crate::tests::init_logger();
|
||||
|
||||
let mut provider = RemoteAnisetteProvider::new(DEFAULT_ANISETTE_URL.to_string());
|
||||
info!(
|
||||
"Remote headers: {:?}",
|
||||
(&mut provider as &mut dyn AnisetteHeadersProvider).get_authentication_headers()?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
541
apple-private-apis/omnisette/src/remote_anisette_v3.rs
Normal file
541
apple-private-apis/omnisette/src/remote_anisette_v3.rs
Normal file
@@ -0,0 +1,541 @@
|
||||
// Implementing the SideStore Anisette v3 protocol
|
||||
|
||||
use std::{collections::HashMap, fs, io::Cursor, path::PathBuf};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use base64::engine::general_purpose;
|
||||
use base64::Engine;
|
||||
use chrono::{DateTime, SubsecRound, Utc};
|
||||
use futures_util::{stream::StreamExt, SinkExt};
|
||||
use log::debug;
|
||||
use plist::{Data, Dictionary};
|
||||
use rand::Rng;
|
||||
use reqwest::{Client, ClientBuilder, RequestBuilder};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fmt::Write;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{anisette_headers_provider::AnisetteHeadersProvider, AnisetteError};
|
||||
|
||||
fn plist_to_string<T: serde::Serialize>(value: &T) -> Result<String, plist::Error> {
|
||||
plist_to_buf(value).map(|val| String::from_utf8(val).unwrap())
|
||||
}
|
||||
|
||||
fn plist_to_buf<T: serde::Serialize>(value: &T) -> Result<Vec<u8>, plist::Error> {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let writer = Cursor::new(&mut buf);
|
||||
plist::to_writer_xml(writer, &value)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn bin_serialize<S>(x: &[u8], s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
s.serialize_bytes(x)
|
||||
}
|
||||
|
||||
fn bin_serialize_opt<S>(x: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
x.clone().map(|i| Data::new(i)).serialize(s)
|
||||
}
|
||||
|
||||
fn bin_deserialize_opt<'de, D>(d: D) -> Result<Option<Vec<u8>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: Option<Data> = Deserialize::deserialize(d)?;
|
||||
Ok(s.map(|i| i.into()))
|
||||
}
|
||||
|
||||
fn bin_deserialize_16<'de, D>(d: D) -> Result<[u8; 16], D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: Data = Deserialize::deserialize(d)?;
|
||||
let s: Vec<u8> = s.into();
|
||||
Ok(s.try_into().unwrap())
|
||||
}
|
||||
|
||||
fn encode_hex(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(bytes.len() * 2);
|
||||
for &b in bytes {
|
||||
write!(&mut s, "{:02x}", b).unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
fn base64_encode(data: &[u8]) -> String {
|
||||
general_purpose::STANDARD.encode(data)
|
||||
}
|
||||
|
||||
fn base64_decode(data: &str) -> Vec<u8> {
|
||||
general_purpose::STANDARD.decode(data.trim()).unwrap()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnisetteClientInfo {
|
||||
client_info: String,
|
||||
user_agent: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AnisetteState {
|
||||
#[serde(
|
||||
serialize_with = "bin_serialize",
|
||||
deserialize_with = "bin_deserialize_16"
|
||||
)]
|
||||
keychain_identifier: [u8; 16],
|
||||
#[serde(
|
||||
serialize_with = "bin_serialize_opt",
|
||||
deserialize_with = "bin_deserialize_opt"
|
||||
)]
|
||||
adi_pb: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Default for AnisetteState {
|
||||
fn default() -> Self {
|
||||
AnisetteState {
|
||||
keychain_identifier: rand::thread_rng().gen::<[u8; 16]>(),
|
||||
adi_pb: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnisetteState {
|
||||
pub fn new() -> AnisetteState {
|
||||
AnisetteState::default()
|
||||
}
|
||||
|
||||
pub fn is_provisioned(&self) -> bool {
|
||||
self.adi_pb.is_some()
|
||||
}
|
||||
|
||||
fn md_lu(&self) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.keychain_identifier);
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
fn device_id(&self) -> String {
|
||||
Uuid::from_bytes(self.keychain_identifier).to_string()
|
||||
}
|
||||
}
|
||||
pub struct AnisetteClient {
|
||||
client_info: AnisetteClientInfo,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct ProvisionBodyData {
|
||||
header: Dictionary,
|
||||
request: Dictionary,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AnisetteData {
|
||||
machine_id: String,
|
||||
one_time_password: String,
|
||||
routing_info: String,
|
||||
device_description: String,
|
||||
local_user_id: String,
|
||||
device_unique_identifier: String,
|
||||
}
|
||||
|
||||
impl AnisetteData {
|
||||
pub fn get_headers(&self, serial: String) -> HashMap<String, String> {
|
||||
let dt: DateTime<Utc> = Utc::now().round_subsecs(0);
|
||||
|
||||
HashMap::from_iter(
|
||||
[
|
||||
(
|
||||
"X-Apple-I-Client-Time".to_string(),
|
||||
dt.format("%+").to_string().replace("+00:00", "Z"),
|
||||
),
|
||||
("X-Apple-I-SRL-NO".to_string(), serial),
|
||||
("X-Apple-I-TimeZone".to_string(), "UTC".to_string()),
|
||||
("X-Apple-Locale".to_string(), "en_US".to_string()),
|
||||
("X-Apple-I-MD-RINFO".to_string(), self.routing_info.clone()),
|
||||
("X-Apple-I-MD-LU".to_string(), self.local_user_id.clone()),
|
||||
(
|
||||
"X-Mme-Device-Id".to_string(),
|
||||
self.device_unique_identifier.clone(),
|
||||
),
|
||||
("X-Apple-I-MD".to_string(), self.one_time_password.clone()),
|
||||
("X-Apple-I-MD-M".to_string(), self.machine_id.clone()),
|
||||
(
|
||||
"X-Mme-Client-Info".to_string(),
|
||||
self.device_description.clone(),
|
||||
),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn make_reqwest() -> Result<Client, AnisetteError> {
|
||||
Ok(ClientBuilder::new()
|
||||
.http1_title_case_headers()
|
||||
.danger_accept_invalid_certs(true) // TODO: pin the apple certificate
|
||||
.build()?)
|
||||
}
|
||||
|
||||
impl AnisetteClient {
|
||||
pub async fn new(url: String) -> Result<AnisetteClient, AnisetteError> {
|
||||
let path = format!("{}/v3/client_info", url);
|
||||
let http_client = make_reqwest()?;
|
||||
let client_info = http_client
|
||||
.get(path)
|
||||
.send()
|
||||
.await?
|
||||
.json::<AnisetteClientInfo>()
|
||||
.await?;
|
||||
Ok(AnisetteClient { client_info, url })
|
||||
}
|
||||
|
||||
fn build_apple_request(
|
||||
&self,
|
||||
state: &AnisetteState,
|
||||
builder: RequestBuilder,
|
||||
) -> RequestBuilder {
|
||||
let dt: DateTime<Utc> = Utc::now().round_subsecs(0);
|
||||
|
||||
builder
|
||||
.header("X-Mme-Client-Info", &self.client_info.client_info)
|
||||
.header("User-Agent", &self.client_info.user_agent)
|
||||
.header("Content-Type", "text/x-xml-plist")
|
||||
.header("X-Apple-I-MD-LU", encode_hex(&state.md_lu()))
|
||||
.header("X-Mme-Device-Id", state.device_id())
|
||||
.header("X-Apple-I-Client-Time", dt.format("%+").to_string())
|
||||
.header("X-Apple-I-TimeZone", "EDT")
|
||||
.header("X-Apple-Locale", "en_US")
|
||||
}
|
||||
|
||||
pub async fn get_headers(&self, state: &AnisetteState) -> Result<AnisetteData, AnisetteError> {
|
||||
let path = format!("{}/v3/get_headers", self.url);
|
||||
let http_client = make_reqwest()?;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GetHeadersBody {
|
||||
identifier: String,
|
||||
adi_pb: String,
|
||||
}
|
||||
let body = GetHeadersBody {
|
||||
identifier: base64_encode(&state.keychain_identifier),
|
||||
adi_pb: base64_encode(
|
||||
state
|
||||
.adi_pb
|
||||
.as_ref()
|
||||
.ok_or(AnisetteError::AnisetteNotProvisioned)?,
|
||||
),
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "result")]
|
||||
enum AnisetteHeaders {
|
||||
GetHeadersError {
|
||||
message: String,
|
||||
},
|
||||
Headers {
|
||||
#[serde(rename = "X-Apple-I-MD-M")]
|
||||
machine_id: String,
|
||||
#[serde(rename = "X-Apple-I-MD")]
|
||||
one_time_password: String,
|
||||
#[serde(rename = "X-Apple-I-MD-RINFO")]
|
||||
routing_info: String,
|
||||
},
|
||||
}
|
||||
|
||||
let headers = http_client
|
||||
.post(path)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.json::<AnisetteHeaders>()
|
||||
.await?;
|
||||
match headers {
|
||||
AnisetteHeaders::GetHeadersError { message } => {
|
||||
if message.contains("-45061") {
|
||||
Err(AnisetteError::AnisetteNotProvisioned)
|
||||
} else {
|
||||
panic!("Unknown error {}", message)
|
||||
}
|
||||
}
|
||||
AnisetteHeaders::Headers {
|
||||
machine_id,
|
||||
one_time_password,
|
||||
routing_info,
|
||||
} => Ok(AnisetteData {
|
||||
machine_id,
|
||||
one_time_password,
|
||||
routing_info,
|
||||
device_description: self.client_info.client_info.clone(),
|
||||
local_user_id: encode_hex(&state.md_lu()),
|
||||
device_unique_identifier: state.device_id(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn provision(&self, state: &mut AnisetteState) -> Result<(), AnisetteError> {
|
||||
debug!("Provisioning Anisette");
|
||||
let http_client = make_reqwest()?;
|
||||
let resp = self
|
||||
.build_apple_request(
|
||||
&state,
|
||||
http_client.get("https://gsa.apple.com/grandslam/GsService2/lookup"),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
let text = resp.text().await?;
|
||||
|
||||
let protocol_val = plist::Value::from_reader(Cursor::new(text.as_str()))?;
|
||||
let urls = protocol_val
|
||||
.as_dictionary()
|
||||
.unwrap()
|
||||
.get("urls")
|
||||
.unwrap()
|
||||
.as_dictionary()
|
||||
.unwrap();
|
||||
|
||||
let start_provisioning_url = urls
|
||||
.get("midStartProvisioning")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
let end_provisioning_url = urls
|
||||
.get("midFinishProvisioning")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
debug!(
|
||||
"Got provisioning urls: {} and {}",
|
||||
start_provisioning_url, end_provisioning_url
|
||||
);
|
||||
|
||||
let provision_ws_url =
|
||||
format!("{}/v3/provisioning_session", self.url).replace("https://", "wss://");
|
||||
let (mut connection, _) = connect_async(&provision_ws_url).await?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "result")]
|
||||
enum ProvisionInput {
|
||||
GiveIdentifier,
|
||||
GiveStartProvisioningData,
|
||||
GiveEndProvisioningData {
|
||||
#[allow(dead_code)] // it's not even dead, rust just has problems
|
||||
cpim: String,
|
||||
},
|
||||
ProvisioningSuccess {
|
||||
#[allow(dead_code)] // it's not even dead, rust just has problems
|
||||
adi_pb: String,
|
||||
},
|
||||
}
|
||||
|
||||
loop {
|
||||
let Some(Ok(data)) = connection.next().await else {
|
||||
continue;
|
||||
};
|
||||
if data.is_text() {
|
||||
let txt = data.to_text().unwrap();
|
||||
let msg: ProvisionInput = serde_json::from_str(txt)?;
|
||||
match msg {
|
||||
ProvisionInput::GiveIdentifier => {
|
||||
#[derive(Serialize)]
|
||||
struct Identifier {
|
||||
identifier: String, // base64
|
||||
}
|
||||
let identifier = Identifier {
|
||||
identifier: base64_encode(&state.keychain_identifier),
|
||||
};
|
||||
connection
|
||||
.send(Message::Text(serde_json::to_string(&identifier)?))
|
||||
.await?;
|
||||
}
|
||||
ProvisionInput::GiveStartProvisioningData => {
|
||||
let http_client = make_reqwest()?;
|
||||
let body_data = ProvisionBodyData {
|
||||
header: Dictionary::new(),
|
||||
request: Dictionary::new(),
|
||||
};
|
||||
let resp = self
|
||||
.build_apple_request(state, http_client.post(start_provisioning_url))
|
||||
.body(plist_to_string(&body_data)?)
|
||||
.send()
|
||||
.await?;
|
||||
let text = resp.text().await?;
|
||||
|
||||
let protocol_val = plist::Value::from_reader(Cursor::new(text.as_str()))?;
|
||||
let spim = protocol_val
|
||||
.as_dictionary()
|
||||
.unwrap()
|
||||
.get("Response")
|
||||
.unwrap()
|
||||
.as_dictionary()
|
||||
.unwrap()
|
||||
.get("spim")
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
|
||||
debug!("GiveStartProvisioningData");
|
||||
#[derive(Serialize)]
|
||||
struct Spim {
|
||||
spim: String, // base64
|
||||
}
|
||||
let spim = Spim {
|
||||
spim: spim.to_string(),
|
||||
};
|
||||
connection
|
||||
.send(Message::Text(serde_json::to_string(&spim)?))
|
||||
.await?;
|
||||
}
|
||||
ProvisionInput::GiveEndProvisioningData { cpim } => {
|
||||
let http_client = make_reqwest()?;
|
||||
let body_data = ProvisionBodyData {
|
||||
header: Dictionary::new(),
|
||||
request: Dictionary::from_iter([("cpim", cpim)].into_iter()),
|
||||
};
|
||||
let resp = self
|
||||
.build_apple_request(state, http_client.post(end_provisioning_url))
|
||||
.body(plist_to_string(&body_data)?)
|
||||
.send()
|
||||
.await?;
|
||||
let text = resp.text().await?;
|
||||
|
||||
let protocol_val = plist::Value::from_reader(Cursor::new(text.as_str()))?;
|
||||
let response = protocol_val
|
||||
.as_dictionary()
|
||||
.unwrap()
|
||||
.get("Response")
|
||||
.unwrap()
|
||||
.as_dictionary()
|
||||
.unwrap();
|
||||
|
||||
debug!("GiveEndProvisioningData");
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EndProvisioning<'t> {
|
||||
ptm: &'t str,
|
||||
tk: &'t str,
|
||||
}
|
||||
let end_provisioning = EndProvisioning {
|
||||
ptm: response.get("ptm").unwrap().as_string().unwrap(),
|
||||
tk: response.get("tk").unwrap().as_string().unwrap(),
|
||||
};
|
||||
connection
|
||||
.send(Message::Text(serde_json::to_string(&end_provisioning)?))
|
||||
.await?;
|
||||
}
|
||||
ProvisionInput::ProvisioningSuccess { adi_pb } => {
|
||||
debug!("ProvisioningSuccess");
|
||||
state.adi_pb = Some(base64_decode(&adi_pb));
|
||||
connection.close(None).await?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if data.is_close() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteAnisetteProviderV3 {
|
||||
client_url: String,
|
||||
client: Option<AnisetteClient>,
|
||||
pub state: Option<AnisetteState>,
|
||||
configuration_path: PathBuf,
|
||||
serial: String,
|
||||
}
|
||||
|
||||
impl RemoteAnisetteProviderV3 {
|
||||
pub fn new(
|
||||
url: String,
|
||||
configuration_path: PathBuf,
|
||||
serial: String,
|
||||
) -> RemoteAnisetteProviderV3 {
|
||||
RemoteAnisetteProviderV3 {
|
||||
client_url: url,
|
||||
client: None,
|
||||
state: None,
|
||||
configuration_path,
|
||||
serial,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AnisetteHeadersProvider for RemoteAnisetteProviderV3 {
|
||||
async fn get_anisette_headers(
|
||||
&mut self,
|
||||
_skip_provisioning: bool,
|
||||
) -> Result<HashMap<String, String>, AnisetteError> {
|
||||
if self.client.is_none() {
|
||||
self.client = Some(AnisetteClient::new(self.client_url.clone()).await?);
|
||||
}
|
||||
let client = self.client.as_ref().unwrap();
|
||||
|
||||
fs::create_dir_all(&self.configuration_path)?;
|
||||
|
||||
let config_path = self.configuration_path.join("state.plist");
|
||||
if self.state.is_none() {
|
||||
self.state = Some(if let Ok(text) = plist::from_file(&config_path) {
|
||||
text
|
||||
} else {
|
||||
AnisetteState::new()
|
||||
});
|
||||
}
|
||||
|
||||
let state = self.state.as_mut().unwrap();
|
||||
if !state.is_provisioned() {
|
||||
client.provision(state).await?;
|
||||
plist::to_file_xml(&config_path, state)?;
|
||||
}
|
||||
let data = match client.get_headers(&state).await {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
if matches!(err, AnisetteError::AnisetteNotProvisioned) {
|
||||
state.adi_pb = None;
|
||||
client.provision(state).await?;
|
||||
plist::to_file_xml(config_path, state)?;
|
||||
client.get_headers(&state).await?
|
||||
} else {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(data.get_headers(self.serial.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::anisette_headers_provider::AnisetteHeadersProvider;
|
||||
use crate::remote_anisette_v3::RemoteAnisetteProviderV3;
|
||||
use crate::{AnisetteError, DEFAULT_ANISETTE_URL_V3};
|
||||
use log::info;
|
||||
|
||||
#[tokio::test]
|
||||
async fn fetch_anisette_remote_v3() -> Result<(), AnisetteError> {
|
||||
crate::tests::init_logger();
|
||||
|
||||
let mut provider = RemoteAnisetteProviderV3::new(
|
||||
DEFAULT_ANISETTE_URL_V3.to_string(),
|
||||
"anisette_test".into(),
|
||||
"0".to_string(),
|
||||
);
|
||||
info!(
|
||||
"Remote headers: {:?}",
|
||||
(&mut provider as &mut dyn AnisetteHeadersProvider)
|
||||
.get_authentication_headers()
|
||||
.await?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
452
apple-private-apis/omnisette/src/store_services_core.rs
Normal file
452
apple-private-apis/omnisette/src/store_services_core.rs
Normal file
@@ -0,0 +1,452 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
mod posix_macos;
|
||||
#[cfg(target_family = "windows")]
|
||||
mod posix_windows;
|
||||
|
||||
use crate::adi_proxy::{
|
||||
ADIError, ADIProxy, ConfigurableADIProxy, RequestOTPData, StartProvisioningData,
|
||||
SynchronizeData,
|
||||
};
|
||||
use crate::AnisetteError;
|
||||
|
||||
use android_loader::android_library::AndroidLibrary;
|
||||
use android_loader::sysv64_type;
|
||||
use android_loader::{hook_manager, sysv64};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, CString};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct StoreServicesCoreADIProxy<'lt> {
|
||||
#[allow(dead_code)]
|
||||
store_services_core: AndroidLibrary<'lt>,
|
||||
|
||||
local_user_uuid: String,
|
||||
device_identifier: String,
|
||||
|
||||
adi_set_android_id: sysv64_type!(fn(id: *const u8, length: u32) -> i32),
|
||||
adi_set_provisioning_path: sysv64_type!(fn(path: *const u8) -> i32),
|
||||
|
||||
adi_provisioning_erase: sysv64_type!(fn(ds_id: i64) -> i32),
|
||||
adi_synchronize: sysv64_type!(
|
||||
fn(
|
||||
ds_id: i64,
|
||||
sim: *const u8,
|
||||
sim_length: u32,
|
||||
out_mid: *mut *const u8,
|
||||
out_mid_length: *mut u32,
|
||||
out_srm: *mut *const u8,
|
||||
out_srm_length: *mut u32,
|
||||
) -> i32
|
||||
),
|
||||
adi_provisioning_destroy: sysv64_type!(fn(session: u32) -> i32),
|
||||
adi_provisioning_end: sysv64_type!(
|
||||
fn(session: u32, ptm: *const u8, ptm_length: u32, tk: *const u8, tk_length: u32) -> i32
|
||||
),
|
||||
adi_provisioning_start: sysv64_type!(
|
||||
fn(
|
||||
ds_id: i64,
|
||||
spim: *const u8,
|
||||
spim_length: u32,
|
||||
out_cpim: *mut *const u8,
|
||||
out_cpim_length: *mut u32,
|
||||
out_session: *mut u32,
|
||||
) -> i32
|
||||
),
|
||||
adi_get_login_code: sysv64_type!(fn(ds_id: i64) -> i32),
|
||||
adi_dispose: sysv64_type!(fn(ptr: *const u8) -> i32),
|
||||
adi_otp_request: sysv64_type!(
|
||||
fn(
|
||||
ds_id: i64,
|
||||
out_mid: *mut *const u8,
|
||||
out_mid_size: *mut u32,
|
||||
out_otp: *mut *const u8,
|
||||
out_otp_size: *mut u32,
|
||||
) -> i32
|
||||
),
|
||||
}
|
||||
|
||||
impl StoreServicesCoreADIProxy<'_> {
|
||||
pub fn new<'lt>(library_path: &PathBuf) -> Result<StoreServicesCoreADIProxy<'lt>, AnisetteError> {
|
||||
Self::with_custom_provisioning_path(library_path, library_path)
|
||||
}
|
||||
|
||||
pub fn with_custom_provisioning_path<'lt>(library_path: &PathBuf, provisioning_path: &PathBuf) -> Result<StoreServicesCoreADIProxy<'lt>, AnisetteError> {
|
||||
// Should be safe if the library is correct.
|
||||
unsafe {
|
||||
LoaderHelpers::setup_hooks();
|
||||
|
||||
if !library_path.exists() {
|
||||
std::fs::create_dir(library_path)?;
|
||||
return Err(AnisetteError::MissingLibraries.into());
|
||||
}
|
||||
|
||||
let library_path = library_path.canonicalize()?;
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
const ARCH: &str = "x86_64";
|
||||
#[cfg(target_arch = "x86")]
|
||||
const ARCH: &str = "x86";
|
||||
#[cfg(target_arch = "arm")]
|
||||
const ARCH: &str = "armeabi-v7a";
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
const ARCH: &str = "arm64-v8a";
|
||||
|
||||
let native_library_path = library_path.join("lib").join(ARCH);
|
||||
|
||||
let path = native_library_path.join("libstoreservicescore.so");
|
||||
let path = path.to_str().ok_or(AnisetteError::Misc)?;
|
||||
let store_services_core = AndroidLibrary::load(path)?;
|
||||
|
||||
let adi_load_library_with_path: sysv64_type!(fn(path: *const u8) -> i32) =
|
||||
std::mem::transmute(
|
||||
store_services_core
|
||||
.get_symbol("kq56gsgHG6")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?,
|
||||
);
|
||||
|
||||
let path = CString::new(
|
||||
native_library_path
|
||||
.to_str()
|
||||
.ok_or(AnisetteError::Misc)?,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!((adi_load_library_with_path)(path.as_ptr() as *const u8), 0);
|
||||
|
||||
let adi_set_android_id = store_services_core
|
||||
.get_symbol("Sph98paBcz")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
let adi_set_provisioning_path = store_services_core
|
||||
.get_symbol("nf92ngaK92")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
|
||||
let adi_provisioning_erase = store_services_core
|
||||
.get_symbol("p435tmhbla")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
let adi_synchronize = store_services_core
|
||||
.get_symbol("tn46gtiuhw")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
let adi_provisioning_destroy = store_services_core
|
||||
.get_symbol("fy34trz2st")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
let adi_provisioning_end = store_services_core
|
||||
.get_symbol("uv5t6nhkui")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
let adi_provisioning_start = store_services_core
|
||||
.get_symbol("rsegvyrt87")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
let adi_get_login_code = store_services_core
|
||||
.get_symbol("aslgmuibau")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
let adi_dispose = store_services_core
|
||||
.get_symbol("jk24uiwqrg")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
let adi_otp_request = store_services_core
|
||||
.get_symbol("qi864985u0")
|
||||
.ok_or(AnisetteError::InvalidLibraryFormat)?;
|
||||
|
||||
let mut proxy = StoreServicesCoreADIProxy {
|
||||
store_services_core,
|
||||
|
||||
local_user_uuid: String::new(),
|
||||
device_identifier: String::new(),
|
||||
|
||||
adi_set_android_id: std::mem::transmute(adi_set_android_id),
|
||||
adi_set_provisioning_path: std::mem::transmute(adi_set_provisioning_path),
|
||||
|
||||
adi_provisioning_erase: std::mem::transmute(adi_provisioning_erase),
|
||||
adi_synchronize: std::mem::transmute(adi_synchronize),
|
||||
adi_provisioning_destroy: std::mem::transmute(adi_provisioning_destroy),
|
||||
adi_provisioning_end: std::mem::transmute(adi_provisioning_end),
|
||||
adi_provisioning_start: std::mem::transmute(adi_provisioning_start),
|
||||
adi_get_login_code: std::mem::transmute(adi_get_login_code),
|
||||
adi_dispose: std::mem::transmute(adi_dispose),
|
||||
adi_otp_request: std::mem::transmute(adi_otp_request),
|
||||
};
|
||||
|
||||
proxy.set_provisioning_path(
|
||||
provisioning_path.to_str().ok_or(AnisetteError::Misc)?,
|
||||
)?;
|
||||
|
||||
Ok(proxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ADIProxy for StoreServicesCoreADIProxy<'_> {
|
||||
fn erase_provisioning(&mut self, ds_id: i64) -> Result<(), ADIError> {
|
||||
match (self.adi_provisioning_erase)(ds_id) {
|
||||
0 => Ok(()),
|
||||
err => Err(ADIError::resolve(err)),
|
||||
}
|
||||
}
|
||||
|
||||
fn synchronize(&mut self, ds_id: i64, sim: &[u8]) -> Result<SynchronizeData, ADIError> {
|
||||
unsafe {
|
||||
let sim_size = sim.len() as u32;
|
||||
let sim_ptr = sim.as_ptr();
|
||||
|
||||
let mut mid_size: u32 = 0;
|
||||
let mut mid_ptr: *const u8 = std::ptr::null();
|
||||
let mut srm_size: u32 = 0;
|
||||
let mut srm_ptr: *const u8 = std::ptr::null();
|
||||
|
||||
match (self.adi_synchronize)(
|
||||
ds_id,
|
||||
sim_ptr,
|
||||
sim_size,
|
||||
&mut mid_ptr,
|
||||
&mut mid_size,
|
||||
&mut srm_ptr,
|
||||
&mut srm_size,
|
||||
) {
|
||||
0 => {
|
||||
let mut mid = vec![0; mid_size as usize];
|
||||
let mut srm = vec![0; srm_size as usize];
|
||||
|
||||
mid.copy_from_slice(std::slice::from_raw_parts(mid_ptr, mid_size as usize));
|
||||
srm.copy_from_slice(std::slice::from_raw_parts(srm_ptr, srm_size as usize));
|
||||
|
||||
(self.adi_dispose)(mid_ptr);
|
||||
(self.adi_dispose)(srm_ptr);
|
||||
|
||||
Ok(SynchronizeData { mid, srm })
|
||||
}
|
||||
err => Err(ADIError::resolve(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn destroy_provisioning_session(&mut self, session: u32) -> Result<(), ADIError> {
|
||||
match (self.adi_provisioning_destroy)(session) {
|
||||
0 => Ok(()),
|
||||
err => Err(ADIError::resolve(err)),
|
||||
}
|
||||
}
|
||||
|
||||
fn end_provisioning(&mut self, session: u32, ptm: &[u8], tk: &[u8]) -> Result<(), ADIError> {
|
||||
let ptm_size = ptm.len() as u32;
|
||||
let ptm_ptr = ptm.as_ptr();
|
||||
|
||||
let tk_size = tk.len() as u32;
|
||||
let tk_ptr = tk.as_ptr();
|
||||
|
||||
match (self.adi_provisioning_end)(session, ptm_ptr, ptm_size, tk_ptr, tk_size) {
|
||||
0 => Ok(()),
|
||||
err => Err(ADIError::resolve(err)),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_provisioning(
|
||||
&mut self,
|
||||
ds_id: i64,
|
||||
spim: &[u8],
|
||||
) -> Result<StartProvisioningData, ADIError> {
|
||||
unsafe {
|
||||
let spim_size = spim.len() as u32;
|
||||
let spim_ptr = spim.as_ptr();
|
||||
|
||||
let mut cpim_size: u32 = 0;
|
||||
let mut cpim_ptr: *const u8 = std::ptr::null();
|
||||
|
||||
let mut session: u32 = 0;
|
||||
|
||||
match (self.adi_provisioning_start)(
|
||||
ds_id,
|
||||
spim_ptr,
|
||||
spim_size,
|
||||
&mut cpim_ptr,
|
||||
&mut cpim_size,
|
||||
&mut session,
|
||||
) {
|
||||
0 => {
|
||||
let mut cpim = vec![0; cpim_size as usize];
|
||||
|
||||
cpim.copy_from_slice(std::slice::from_raw_parts(cpim_ptr, cpim_size as usize));
|
||||
|
||||
(self.adi_dispose)(cpim_ptr);
|
||||
|
||||
Ok(StartProvisioningData { cpim, session })
|
||||
}
|
||||
err => Err(ADIError::resolve(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_machine_provisioned(&self, ds_id: i64) -> bool {
|
||||
(self.adi_get_login_code)(ds_id) == 0
|
||||
}
|
||||
|
||||
fn request_otp(&self, ds_id: i64) -> Result<RequestOTPData, ADIError> {
|
||||
unsafe {
|
||||
let mut mid_size: u32 = 0;
|
||||
let mut mid_ptr: *const u8 = std::ptr::null();
|
||||
let mut otp_size: u32 = 0;
|
||||
let mut otp_ptr: *const u8 = std::ptr::null();
|
||||
|
||||
match (self.adi_otp_request)(
|
||||
ds_id,
|
||||
&mut mid_ptr,
|
||||
&mut mid_size,
|
||||
&mut otp_ptr,
|
||||
&mut otp_size,
|
||||
) {
|
||||
0 => {
|
||||
let mut mid = vec![0; mid_size as usize];
|
||||
let mut otp = vec![0; otp_size as usize];
|
||||
|
||||
mid.copy_from_slice(std::slice::from_raw_parts(mid_ptr, mid_size as usize));
|
||||
otp.copy_from_slice(std::slice::from_raw_parts(otp_ptr, otp_size as usize));
|
||||
|
||||
(self.adi_dispose)(mid_ptr);
|
||||
(self.adi_dispose)(otp_ptr);
|
||||
|
||||
Ok(RequestOTPData { mid, otp })
|
||||
}
|
||||
err => Err(ADIError::resolve(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_local_user_uuid(&mut self, local_user_uuid: String) {
|
||||
self.local_user_uuid = local_user_uuid;
|
||||
}
|
||||
|
||||
fn set_device_identifier(&mut self, device_identifier: String) -> Result<(), ADIError> {
|
||||
self.set_identifier(&device_identifier[0..16])?;
|
||||
self.device_identifier = device_identifier;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_local_user_uuid(&self) -> String {
|
||||
self.local_user_uuid.clone()
|
||||
}
|
||||
|
||||
fn get_device_identifier(&self) -> String {
|
||||
self.device_identifier.clone()
|
||||
}
|
||||
|
||||
fn get_serial_number(&self) -> String {
|
||||
"0".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigurableADIProxy for StoreServicesCoreADIProxy<'_> {
|
||||
fn set_identifier(&mut self, identifier: &str) -> Result<(), ADIError> {
|
||||
match (self.adi_set_android_id)(identifier.as_ptr(), identifier.len() as u32) {
|
||||
0 => Ok(()),
|
||||
err => Err(ADIError::resolve(err)),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_provisioning_path(&mut self, path: &str) -> Result<(), ADIError> {
|
||||
let path = CString::new(path).unwrap();
|
||||
match (self.adi_set_provisioning_path)(path.as_ptr() as *const u8) {
|
||||
0 => Ok(()),
|
||||
err => Err(ADIError::resolve(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoaderHelpers;
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
#[cfg(all(target_family = "unix", not(target_os = "macos")))]
|
||||
use libc::{
|
||||
chmod, close, free, fstat, ftruncate, gettimeofday, lstat, malloc, mkdir, open, read, strncpy,
|
||||
umask, write,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use posix_macos::*;
|
||||
|
||||
static mut ERRNO: i32 = 0;
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
#[sysv64]
|
||||
unsafe fn __errno_location() -> *mut i32 {
|
||||
ERRNO = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
|
||||
&mut ERRNO
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
fn arc4random() -> u32 {
|
||||
rand::thread_rng().gen()
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
unsafe fn __system_property_get(_name: *const c_char, value: *mut c_char) -> i32 {
|
||||
*value = '0' as c_char;
|
||||
return 1;
|
||||
}
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
use posix_windows::*;
|
||||
|
||||
impl LoaderHelpers {
|
||||
pub fn setup_hooks() {
|
||||
let mut hooks = HashMap::new();
|
||||
hooks.insert("arc4random".to_owned(), arc4random as usize);
|
||||
hooks.insert("chmod".to_owned(), chmod as usize);
|
||||
hooks.insert(
|
||||
"__system_property_get".to_owned(),
|
||||
__system_property_get as usize,
|
||||
);
|
||||
hooks.insert("__errno".to_owned(), __errno_location as usize);
|
||||
hooks.insert("close".to_owned(), close as usize);
|
||||
hooks.insert("free".to_owned(), free as usize);
|
||||
hooks.insert("fstat".to_owned(), fstat as usize);
|
||||
hooks.insert("ftruncate".to_owned(), ftruncate as usize);
|
||||
hooks.insert("gettimeofday".to_owned(), gettimeofday as usize);
|
||||
hooks.insert("lstat".to_owned(), lstat as usize);
|
||||
hooks.insert("malloc".to_owned(), malloc as usize);
|
||||
hooks.insert("mkdir".to_owned(), mkdir as usize);
|
||||
hooks.insert("open".to_owned(), open as usize);
|
||||
hooks.insert("read".to_owned(), read as usize);
|
||||
hooks.insert("strncpy".to_owned(), strncpy as usize);
|
||||
hooks.insert("umask".to_owned(), umask as usize);
|
||||
hooks.insert("write".to_owned(), write as usize);
|
||||
|
||||
hook_manager::add_hooks(hooks);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{AnisetteConfiguration, AnisetteHeaders};
|
||||
use log::info;
|
||||
use std::path::PathBuf;
|
||||
use crate::AnisetteError;
|
||||
|
||||
#[cfg(not(feature = "async"))]
|
||||
#[test]
|
||||
fn fetch_anisette_ssc() -> Result<(), AnisetteError> {
|
||||
crate::tests::init_logger();
|
||||
|
||||
let mut provider = AnisetteHeaders::get_ssc_anisette_headers_provider(
|
||||
AnisetteConfiguration::new()
|
||||
.set_configuration_path(PathBuf::new().join("anisette_test")),
|
||||
)?;
|
||||
info!(
|
||||
"Headers: {:?}",
|
||||
provider.provider.get_authentication_headers()?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
#[tokio::test]
|
||||
async fn fetch_anisette_ssc_async() -> Result<(), AnisetteError> {
|
||||
|
||||
crate::tests::init_logger();
|
||||
|
||||
let mut provider = AnisetteHeaders::get_ssc_anisette_headers_provider(
|
||||
AnisetteConfiguration::new()
|
||||
.set_configuration_path(PathBuf::new().join("anisette_test")),
|
||||
)?;
|
||||
info!(
|
||||
"Headers: {:?}",
|
||||
provider.provider.get_authentication_headers().await?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
pub use libc::{chmod, close, free, ftruncate, gettimeofday, malloc, mkdir, read, strncpy, umask, write};
|
||||
|
||||
use libc::{lstat as lstat_macos, fstat as fstat_macos, stat as stat_macos, open as open_macos, O_CREAT, O_WRONLY, O_RDWR, O_RDONLY};
|
||||
|
||||
use android_loader::sysv64;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct StatLinux {
|
||||
pub st_dev: u64,
|
||||
pub st_ino: u64,
|
||||
pub st_nlink: u64,
|
||||
pub st_mode: u32,
|
||||
pub st_uid: u32,
|
||||
pub st_gid: u32,
|
||||
__pad0: libc::c_int,
|
||||
pub st_rdev: u64,
|
||||
pub st_size: i64,
|
||||
pub st_blksize: i64,
|
||||
pub st_blocks: i64,
|
||||
pub st_atime: i64,
|
||||
pub st_atime_nsec: i64,
|
||||
pub st_mtime: i64,
|
||||
pub st_mtime_nsec: i64,
|
||||
pub st_ctime: i64,
|
||||
pub st_ctime_nsec: i64,
|
||||
__unused: [i64; 3],
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn lstat(path: *const libc::c_char, buf: *mut StatLinux) -> libc::c_int {
|
||||
let mut st: stat_macos = std::mem::zeroed();
|
||||
lstat_macos(path, &mut st);
|
||||
*buf = StatLinux {
|
||||
st_dev: st.st_dev as _,
|
||||
st_ino: st.st_ino as _,
|
||||
st_nlink: st.st_nlink as _,
|
||||
st_mode: st.st_mode as _,
|
||||
st_uid: st.st_uid as _,
|
||||
st_gid: st.st_gid as _,
|
||||
__pad0: 0 as _,
|
||||
st_rdev: st.st_rdev as _,
|
||||
st_size: st.st_size as _,
|
||||
st_blksize: st.st_blksize as _,
|
||||
st_blocks: st.st_blocks as _,
|
||||
st_atime: st.st_atime as _,
|
||||
st_atime_nsec: st.st_atime_nsec as _,
|
||||
st_mtime: st.st_mtime as _,
|
||||
st_mtime_nsec: st.st_mtime_nsec as _,
|
||||
st_ctime: st.st_ctime as _,
|
||||
st_ctime_nsec: st.st_ctime_nsec as _,
|
||||
__unused: [0, 0, 0],
|
||||
};
|
||||
0
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn fstat(fildes: libc::c_int, buf: *mut StatLinux) -> libc::c_int {
|
||||
let mut st: stat_macos = std::mem::zeroed();
|
||||
fstat_macos(fildes, &mut st);
|
||||
*buf = StatLinux {
|
||||
st_dev: st.st_dev as _,
|
||||
st_ino: st.st_ino as _,
|
||||
st_nlink: st.st_nlink as _,
|
||||
st_mode: st.st_mode as _,
|
||||
st_uid: st.st_uid as _,
|
||||
st_gid: st.st_gid as _,
|
||||
__pad0: 0 as _,
|
||||
st_rdev: st.st_rdev as _,
|
||||
st_size: st.st_size as _,
|
||||
st_blksize: st.st_blksize as _,
|
||||
st_blocks: st.st_blocks as _,
|
||||
st_atime: st.st_atime as _,
|
||||
st_atime_nsec: st.st_atime_nsec as _,
|
||||
st_mtime: st.st_mtime as _,
|
||||
st_mtime_nsec: st.st_mtime_nsec as _,
|
||||
st_ctime: st.st_ctime as _,
|
||||
st_ctime_nsec: st.st_ctime_nsec as _,
|
||||
__unused: [0, 0, 0],
|
||||
};
|
||||
0
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn open(path: *const libc::c_char, oflag: libc::c_int) -> libc::c_int {
|
||||
let mut win_flag = 0; // binary mode
|
||||
|
||||
if oflag & 0o100 != 0 {
|
||||
win_flag |= O_CREAT;
|
||||
}
|
||||
|
||||
if oflag & 0o1 == 1 {
|
||||
win_flag |= O_WRONLY;
|
||||
} else if oflag & 0o2 != 0 {
|
||||
win_flag |= O_RDWR;
|
||||
} else {
|
||||
win_flag |= O_RDONLY;
|
||||
}
|
||||
|
||||
let val = open_macos(path, win_flag);
|
||||
|
||||
val
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
use android_loader::sysv64;
|
||||
use libc::{O_CREAT, O_RDONLY, O_RDWR, O_WRONLY};
|
||||
use log::debug;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
#[link(name = "ucrt")]
|
||||
extern "C" {
|
||||
fn _errno() -> *mut libc::c_int;
|
||||
fn _timespec64_get(__ts: *mut libc::timespec, __base: libc::c_int) -> libc::c_int;
|
||||
fn _chsize(handle: i64, length: u64) -> usize;
|
||||
}
|
||||
|
||||
// took from cosmopolitan libc
|
||||
#[sysv64]
|
||||
pub unsafe fn umask(mask: usize) -> usize {
|
||||
debug!("umask: Windows specific implementation called!");
|
||||
mask
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn ftruncate(handle: i64, length: u64) -> usize {
|
||||
debug!(
|
||||
"ftruncate: Windows translate-call. handle: {}, length: {}",
|
||||
handle, length
|
||||
);
|
||||
let ftr = _chsize(handle, length);
|
||||
|
||||
ftr
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PosixTimeval {
|
||||
tv_sec: u64,
|
||||
tv_usec: u64, /* microseconds */
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PosixTimespec {
|
||||
tv_sec: i64,
|
||||
tv_nsec: i64, /* microseconds */
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PosixTimezone {
|
||||
tz_minuteswest: u32,
|
||||
tz_dsttime: u32, /* microseconds */
|
||||
}
|
||||
|
||||
static HECTONANOSECONDS: u64 = 10000000;
|
||||
|
||||
impl PosixTimespec {
|
||||
pub fn from_windows_time(time: u64) -> PosixTimespec {
|
||||
PosixTimespec {
|
||||
tv_sec: (time / HECTONANOSECONDS) as i64,
|
||||
tv_nsec: (time % HECTONANOSECONDS) as i64 * 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn gettimeofday(timeval: *mut PosixTimeval, _tz: *mut PosixTimezone) -> isize {
|
||||
debug!("gettimeofday: Windows specific implementation called!");
|
||||
let mut ts = MaybeUninit::<libc::timespec>::zeroed();
|
||||
|
||||
let ret = _timespec64_get(ts.as_mut_ptr(), 1);
|
||||
let ts = ts.assume_init();
|
||||
|
||||
*timeval = PosixTimeval {
|
||||
tv_sec: ts.tv_sec as _,
|
||||
tv_usec: (ts.tv_nsec / 1000) as _,
|
||||
};
|
||||
|
||||
ret as _
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct StatLinux {
|
||||
pub st_dev: u64,
|
||||
pub st_ino: u64,
|
||||
pub st_nlink: u64,
|
||||
pub st_mode: u32,
|
||||
pub st_uid: u32,
|
||||
pub st_gid: u32,
|
||||
__pad0: libc::c_int,
|
||||
pub st_rdev: u64,
|
||||
pub st_size: i64,
|
||||
pub st_blksize: i64,
|
||||
pub st_blocks: i64,
|
||||
pub st_atime: i64,
|
||||
pub st_atime_nsec: i64,
|
||||
pub st_mtime: i64,
|
||||
pub st_mtime_nsec: i64,
|
||||
pub st_ctime: i64,
|
||||
pub st_ctime_nsec: i64,
|
||||
__unused: [i64; 3],
|
||||
}
|
||||
|
||||
trait ToWindows<T> {
|
||||
unsafe fn to_windows(&self) -> T;
|
||||
}
|
||||
|
||||
impl ToWindows<CString> for CStr {
|
||||
unsafe fn to_windows(&self) -> CString {
|
||||
let path = self
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.chars()
|
||||
.map(|x| match x {
|
||||
'/' => '\\',
|
||||
c => c,
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
let path = path.trim_start_matches("\\\\?\\").to_string();
|
||||
|
||||
CString::new(path).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn lstat(path: *const libc::c_char, buf: *mut StatLinux) -> libc::c_int {
|
||||
debug!(
|
||||
"lstat: Windows translate-call, path: {:?}",
|
||||
CStr::from_ptr(path)
|
||||
);
|
||||
let mut stat_win = MaybeUninit::<libc::stat>::zeroed();
|
||||
let path = CStr::from_ptr(path).to_windows();
|
||||
|
||||
let ret = libc::stat(path.as_ptr(), stat_win.as_mut_ptr());
|
||||
let stat_win = stat_win.assume_init();
|
||||
|
||||
*buf = stat_win.to_windows();
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
impl ToWindows<StatLinux> for libc::stat {
|
||||
unsafe fn to_windows(&self) -> StatLinux {
|
||||
let atime = PosixTimespec::from_windows_time(self.st_atime as u64);
|
||||
let mtime = PosixTimespec::from_windows_time(self.st_mtime as u64);
|
||||
let ctime = PosixTimespec::from_windows_time(self.st_ctime as u64);
|
||||
|
||||
let mut mode = 0o555;
|
||||
let win_mode = self.st_mode;
|
||||
|
||||
if win_mode & 0b11 != 0 {
|
||||
mode |= 0o200;
|
||||
}
|
||||
|
||||
if win_mode & 0x4000 != 0 {
|
||||
mode |= 0o40000;
|
||||
}
|
||||
|
||||
StatLinux {
|
||||
st_dev: self.st_dev as _,
|
||||
st_ino: self.st_ino as _,
|
||||
st_nlink: self.st_nlink as _,
|
||||
st_mode: mode as _,
|
||||
st_uid: self.st_uid as _,
|
||||
st_gid: self.st_gid as _,
|
||||
__pad0: 0,
|
||||
st_rdev: self.st_rdev as _,
|
||||
st_size: self.st_size as _,
|
||||
st_blksize: 0,
|
||||
st_blocks: 0,
|
||||
st_atime: atime.tv_sec,
|
||||
st_atime_nsec: 0,
|
||||
st_mtime: mtime.tv_sec,
|
||||
st_mtime_nsec: 0,
|
||||
st_ctime: ctime.tv_sec,
|
||||
st_ctime_nsec: 0,
|
||||
__unused: [0, 0, 0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn fstat(fildes: libc::c_int, buf: *mut StatLinux) -> libc::c_int {
|
||||
debug!("fstat: Windows translate-call");
|
||||
let mut stat_win = MaybeUninit::<libc::stat>::zeroed();
|
||||
let ret = libc::fstat(fildes, stat_win.as_mut_ptr());
|
||||
let stat_win = stat_win.assume_init();
|
||||
|
||||
*buf = stat_win.to_windows();
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn malloc(size: libc::size_t) -> *mut libc::c_void {
|
||||
// debug!("malloc: Windows translate-call");
|
||||
libc::malloc(size)
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn free(p: *mut libc::c_void) {
|
||||
// debug!("free: Windows translate-call");
|
||||
libc::free(p)
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn strncpy(
|
||||
dst: *mut libc::c_char,
|
||||
src: *const libc::c_char,
|
||||
n: libc::size_t,
|
||||
) -> *mut libc::c_char {
|
||||
debug!("strncpy: Windows translate-call");
|
||||
libc::strncpy(dst, src, n)
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn chmod(path: *const libc::c_char, mode: libc::c_int) -> libc::c_int {
|
||||
debug!("chmod: Windows translate-call");
|
||||
libc::chmod(path, mode)
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn mkdir(path: *const libc::c_char) -> libc::c_int {
|
||||
debug!("mkdir: Windows translate-call");
|
||||
libc::mkdir(path)
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn open(path: *const libc::c_char, oflag: libc::c_int) -> libc::c_int {
|
||||
debug!("open: Windows translate-call oflag 0o{:o}", oflag);
|
||||
|
||||
let path = CStr::from_ptr(path).to_windows();
|
||||
|
||||
let mut win_flag = 0x8000; // binary mode
|
||||
|
||||
if oflag & 0o100 != 0 {
|
||||
win_flag |= O_CREAT;
|
||||
}
|
||||
|
||||
if oflag & 0o1 == 1 {
|
||||
win_flag |= O_WRONLY;
|
||||
} else if oflag & 0o2 != 0 {
|
||||
win_flag |= O_RDWR;
|
||||
} else {
|
||||
win_flag |= O_RDONLY;
|
||||
}
|
||||
|
||||
let val = libc::open(path.as_ptr(), win_flag);
|
||||
|
||||
val
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn close(fd: libc::c_int) -> libc::c_int {
|
||||
debug!("close: Windows translate-call");
|
||||
libc::close(fd)
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn read(fd: libc::c_int, buf: *mut libc::c_void, count: libc::c_uint) -> libc::c_int {
|
||||
debug!("read: Windows translate-call");
|
||||
|
||||
let r = libc::read(fd, buf, count);
|
||||
r
|
||||
}
|
||||
|
||||
#[sysv64]
|
||||
pub unsafe fn write(fd: libc::c_int, buf: *const libc::c_void, count: libc::c_uint) -> libc::c_int {
|
||||
debug!("write: Windows translate-call");
|
||||
libc::write(fd, buf, count)
|
||||
}
|
||||
62
src/application.rs
Normal file
62
src/application.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
||||
|
||||
use crate::bundle::Bundle;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use zip::ZipArchive;
|
||||
|
||||
pub struct Application {
|
||||
pub bundle: Bundle,
|
||||
//pub temp_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
if !path.exists() {
|
||||
panic!("Application path does not exist: {}", path.display());
|
||||
}
|
||||
|
||||
let mut bundle_path = path.clone();
|
||||
//let mut temp_path = PathBuf::new();
|
||||
|
||||
if path.is_file() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let temp_path = temp_dir.join(path.file_name().unwrap());
|
||||
if temp_path.exists() {
|
||||
std::fs::remove_dir_all(&temp_path)
|
||||
.expect("Failed to remove existing temporary files");
|
||||
}
|
||||
std::fs::create_dir_all(&temp_path).expect("Failed to create temporary directory");
|
||||
|
||||
let file = File::open(&path).expect("Failed to open application file");
|
||||
let mut archive = ZipArchive::new(file).expect("Failed to read application archive");
|
||||
archive
|
||||
.extract(&temp_path)
|
||||
.expect("Failed to extract application archive");
|
||||
|
||||
let payload_folder = temp_path.join("Payload");
|
||||
if payload_folder.exists() && payload_folder.is_dir() {
|
||||
let app_dirs: Vec<_> = std::fs::read_dir(&payload_folder)
|
||||
.expect("Failed to read Payload directory")
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
|
||||
.filter(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
|
||||
.collect();
|
||||
if app_dirs.len() == 1 {
|
||||
bundle_path = app_dirs[0].path();
|
||||
} else if app_dirs.is_empty() {
|
||||
panic!("No .app directory found in Payload");
|
||||
} else {
|
||||
panic!("Multiple .app directories found in Payload");
|
||||
}
|
||||
} else {
|
||||
panic!("No Payload directory found in the application archive");
|
||||
}
|
||||
}
|
||||
let bundle = Bundle::new(bundle_path).expect("Failed to create application bundle");
|
||||
|
||||
Application {
|
||||
bundle, /*temp_path*/
|
||||
}
|
||||
}
|
||||
}
|
||||
180
src/bundle.rs
Normal file
180
src/bundle.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
||||
|
||||
use crate::Error;
|
||||
use plist::{Dictionary, Value};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub struct Bundle {
|
||||
pub app_info: Dictionary,
|
||||
pub bundle_dir: PathBuf,
|
||||
|
||||
app_extensions: Vec<Bundle>,
|
||||
_frameworks: Vec<Bundle>,
|
||||
_libraries: Vec<String>,
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
pub fn new(bundle_dir: PathBuf) -> Result<Self, Error> {
|
||||
let mut bundle_path = bundle_dir;
|
||||
// Remove trailing slash/backslash
|
||||
if let Some(path_str) = bundle_path.to_str() {
|
||||
if path_str.ends_with('/') || path_str.ends_with('\\') {
|
||||
bundle_path = PathBuf::from(&path_str[..path_str.len() - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
let info_plist_path = bundle_path.join("Info.plist");
|
||||
assert_bundle(
|
||||
info_plist_path.exists(),
|
||||
&format!("No Info.plist here: {}", info_plist_path.display()),
|
||||
)?;
|
||||
|
||||
let plist_data = fs::read(&info_plist_path)
|
||||
.map_err(|e| Error::InvalidBundle(format!("Failed to read Info.plist: {}", e)))?;
|
||||
|
||||
let app_info = plist::from_bytes(&plist_data)
|
||||
.map_err(|e| Error::InvalidBundle(format!("Failed to parse Info.plist: {}", e)))?;
|
||||
|
||||
// Load app extensions from PlugIns directory
|
||||
let plug_ins_dir = bundle_path.join("PlugIns");
|
||||
let app_extensions = if plug_ins_dir.exists() {
|
||||
fs::read_dir(&plug_ins_dir)
|
||||
.map_err(|e| {
|
||||
Error::InvalidBundle(format!("Failed to read PlugIns directory: {}", e))
|
||||
})?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
|
||||
&& entry.path().join("Info.plist").exists()
|
||||
})
|
||||
.filter_map(|entry| Bundle::new(entry.path()).ok())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Load frameworks from Frameworks directory
|
||||
let frameworks_dir = bundle_path.join("Frameworks");
|
||||
let frameworks = if frameworks_dir.exists() {
|
||||
fs::read_dir(&frameworks_dir)
|
||||
.map_err(|e| {
|
||||
Error::InvalidBundle(format!("Failed to read Frameworks directory: {}", e))
|
||||
})?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| {
|
||||
entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
|
||||
&& entry.path().join("Info.plist").exists()
|
||||
})
|
||||
.filter_map(|entry| Bundle::new(entry.path()).ok())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Find all .dylib files in the bundle directory (recursive)
|
||||
let libraries = find_dylibs(&bundle_path, &bundle_path)?;
|
||||
|
||||
Ok(Bundle {
|
||||
app_info,
|
||||
bundle_dir: bundle_path,
|
||||
app_extensions,
|
||||
_frameworks: frameworks,
|
||||
_libraries: libraries,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_bundle_identifier(&mut self, id: &str) {
|
||||
self.app_info.insert(
|
||||
"CFBundleIdentifier".to_string(),
|
||||
Value::String(id.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn bundle_identifier(&self) -> Option<&str> {
|
||||
self.app_info
|
||||
.get("CFBundleIdentifier")
|
||||
.and_then(|v| v.as_string())
|
||||
}
|
||||
|
||||
pub fn bundle_name(&self) -> Option<&str> {
|
||||
self.app_info
|
||||
.get("CFBundleName")
|
||||
.and_then(|v| v.as_string())
|
||||
}
|
||||
|
||||
pub fn app_extensions(&self) -> &[Bundle] {
|
||||
&self.app_extensions
|
||||
}
|
||||
|
||||
pub fn app_extensions_mut(&mut self) -> &mut [Bundle] {
|
||||
&mut self.app_extensions
|
||||
}
|
||||
|
||||
pub fn write_info(&self) -> Result<(), Error> {
|
||||
let info_plist_path = self.bundle_dir.join("Info.plist");
|
||||
let result = plist::to_file_binary(&info_plist_path, &self.app_info);
|
||||
|
||||
if result.is_err() {
|
||||
return Err(Error::InvalidBundle(format!(
|
||||
"Failed to write Info.plist: {}",
|
||||
result.unwrap_err()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_bundle(condition: bool, msg: &str) -> Result<(), Error> {
|
||||
if !condition {
|
||||
Err(Error::InvalidBundle(msg.to_string()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn find_dylibs(dir: &Path, bundle_root: &Path) -> Result<Vec<String>, Error> {
|
||||
let mut libraries = Vec::new();
|
||||
|
||||
fn collect_dylibs(
|
||||
dir: &Path,
|
||||
bundle_root: &Path,
|
||||
libraries: &mut Vec<String>,
|
||||
) -> Result<(), Error> {
|
||||
let entries = fs::read_dir(dir).map_err(|e| {
|
||||
Error::InvalidBundle(format!("Failed to read directory {}: {}", dir.display(), e))
|
||||
})?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| {
|
||||
Error::InvalidBundle(format!("Failed to read directory entry: {}", e))
|
||||
})?;
|
||||
|
||||
let path = entry.path();
|
||||
let file_type = entry
|
||||
.file_type()
|
||||
.map_err(|e| Error::InvalidBundle(format!("Failed to get file type: {}", e)))?;
|
||||
|
||||
if file_type.is_file() {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.ends_with(".dylib") {
|
||||
// Get relative path from bundle root
|
||||
if let Ok(relative_path) = path.strip_prefix(bundle_root) {
|
||||
if let Some(relative_str) = relative_path.to_str() {
|
||||
libraries.push(relative_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if file_type.is_dir() {
|
||||
collect_dylibs(&path, bundle_root, libraries)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
collect_dylibs(dir, bundle_root, &mut libraries)?;
|
||||
Ok(libraries)
|
||||
}
|
||||
214
src/certificate.rs
Normal file
214
src/certificate.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
||||
|
||||
use hex;
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
rsa::Rsa,
|
||||
x509::{X509, X509Name, X509ReqBuilder},
|
||||
};
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::Error;
|
||||
use crate::developer_session::{DeveloperDeviceType, DeveloperSession, DeveloperTeam};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CertificateIdentity {
|
||||
pub certificate: Option<X509>,
|
||||
pub private_key: PKey<Private>,
|
||||
pub key_file: PathBuf,
|
||||
pub cert_file: PathBuf,
|
||||
}
|
||||
|
||||
impl CertificateIdentity {
|
||||
pub async fn new(
|
||||
configuration_path: PathBuf,
|
||||
dev_session: &DeveloperSession,
|
||||
apple_id: String,
|
||||
) -> Result<Self, Error> {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(apple_id.as_bytes());
|
||||
let hash_string = hex::encode(hasher.finalize()).to_lowercase();
|
||||
let key_path = configuration_path.join("keys").join(hash_string);
|
||||
fs::create_dir_all(&key_path)
|
||||
.map_err(|e| format!("Failed to create key directory: {}", e))?;
|
||||
|
||||
let key_file = key_path.join("key.pem");
|
||||
let cert_file = key_path.join("cert.pem");
|
||||
let teams = dev_session
|
||||
.list_teams()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list teams: {:?}", e))?;
|
||||
let team = teams.first().ok_or("No teams found")?;
|
||||
let private_key = if key_file.exists() {
|
||||
let key_data = fs::read_to_string(&key_file)
|
||||
.map_err(|e| format!("Failed to read key file: {}", e))?;
|
||||
PKey::private_key_from_pem(key_data.as_bytes())
|
||||
.map_err(|e| format!("Failed to load private key: {}", e))?
|
||||
} else {
|
||||
let rsa =
|
||||
Rsa::generate(2048).map_err(|e| format!("Failed to generate RSA key: {}", e))?;
|
||||
let key =
|
||||
PKey::from_rsa(rsa).map_err(|e| format!("Failed to create private key: {}", e))?;
|
||||
let pem_data = key
|
||||
.private_key_to_pem_pkcs8()
|
||||
.map_err(|e| format!("Failed to encode private key: {}", e))?;
|
||||
fs::write(&key_file, pem_data)
|
||||
.map_err(|e| format!("Failed to save key file: {}", e))?;
|
||||
key
|
||||
};
|
||||
|
||||
let mut cert_identity = CertificateIdentity {
|
||||
certificate: None,
|
||||
private_key,
|
||||
key_file,
|
||||
cert_file,
|
||||
};
|
||||
|
||||
if let Ok(cert) = cert_identity
|
||||
.find_matching_certificate(dev_session, team)
|
||||
.await
|
||||
{
|
||||
cert_identity.certificate = Some(cert.clone());
|
||||
|
||||
let cert_pem = cert
|
||||
.to_pem()
|
||||
.map_err(|e| format!("Failed to encode certificate to PEM: {}", e))?;
|
||||
fs::write(&cert_identity.cert_file, cert_pem)
|
||||
.map_err(|e| format!("Failed to save certificate file: {}", e))?;
|
||||
|
||||
return Ok(cert_identity);
|
||||
}
|
||||
|
||||
cert_identity
|
||||
.request_new_certificate(dev_session, team)
|
||||
.await?;
|
||||
Ok(cert_identity)
|
||||
}
|
||||
|
||||
async fn find_matching_certificate(
|
||||
&self,
|
||||
dev_session: &DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<X509, Error> {
|
||||
let certificates = dev_session
|
||||
.list_all_development_certs(DeveloperDeviceType::Ios, team)
|
||||
.await
|
||||
.map_err(|e| Error::Certificate(format!("Failed to list certificates: {:?}", e)))?;
|
||||
|
||||
let our_public_key = self
|
||||
.private_key
|
||||
.public_key_to_der()
|
||||
.map_err(|e| Error::Certificate(format!("Failed to get public key: {}", e)))?;
|
||||
|
||||
for cert in certificates
|
||||
.iter()
|
||||
.filter(|c| c.machine_name == "YCode".to_string())
|
||||
{
|
||||
if let Ok(x509_cert) = X509::from_der(&cert.cert_content) {
|
||||
if let Ok(cert_public_key) = x509_cert.public_key() {
|
||||
if let Ok(cert_public_key_der) = cert_public_key.public_key_to_der() {
|
||||
if cert_public_key_der == our_public_key {
|
||||
return Ok(x509_cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Error::Certificate("No matching certificate found".to_string())
|
||||
}
|
||||
|
||||
async fn request_new_certificate(
|
||||
&mut self,
|
||||
dev_session: &DeveloperSession,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<(), Error> {
|
||||
let mut req_builder = X509ReqBuilder::new()
|
||||
.map_err(|e| format!("Failed to create request builder: {}", e))?;
|
||||
let mut name_builder =
|
||||
X509Name::builder().map_err(|e| format!("Failed to create name builder: {}", e))?;
|
||||
|
||||
name_builder
|
||||
.append_entry_by_text("C", "US")
|
||||
.map_err(|e| format!("Failed to set country: {}", e))?;
|
||||
name_builder
|
||||
.append_entry_by_text("ST", "STATE")
|
||||
.map_err(|e| format!("Failed to set state: {}", e))?;
|
||||
name_builder
|
||||
.append_entry_by_text("L", "LOCAL")
|
||||
.map_err(|e| format!("Failed to set locality: {}", e))?;
|
||||
name_builder
|
||||
.append_entry_by_text("O", "ORGNIZATION")
|
||||
.map_err(|e| format!("Failed to set organization: {}", e))?;
|
||||
name_builder
|
||||
.append_entry_by_text("CN", "CN")
|
||||
.map_err(|e| format!("Failed to set common name: {}", e))?;
|
||||
|
||||
req_builder
|
||||
.set_subject_name(&name_builder.build())
|
||||
.map_err(|e| format!("Failed to set subject name: {}", e))?;
|
||||
req_builder
|
||||
.set_pubkey(&self.private_key)
|
||||
.map_err(|e| format!("Failed to set public key: {}", e))?;
|
||||
req_builder
|
||||
.sign(&self.private_key, MessageDigest::sha256())
|
||||
.map_err(|e| format!("Failed to sign request: {}", e))?;
|
||||
|
||||
let csr_pem = req_builder
|
||||
.build()
|
||||
.to_pem()
|
||||
.map_err(|e| format!("Failed to encode CSR: {}", e))?;
|
||||
|
||||
let certificate_id = dev_session
|
||||
.submit_development_csr(
|
||||
DeveloperDeviceType::Ios,
|
||||
team,
|
||||
String::from_utf8_lossy(&csr_pem).to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let is_7460 = match &e {
|
||||
Error::DeveloperSession(code, _) => *code == 7460,
|
||||
_ => false,
|
||||
};
|
||||
if is_7460 {
|
||||
Error::Certificate("You have too many certificates!".to_string())
|
||||
} else {
|
||||
Error::Certificate(format!("Failed to submit CSR: {:?}", e))
|
||||
}
|
||||
})?;
|
||||
|
||||
let certificates = dev_session
|
||||
.list_all_development_certs(DeveloperDeviceType::Ios, team)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list certificates: {:?}", e))?;
|
||||
|
||||
let apple_cert = certificates
|
||||
.iter()
|
||||
.find(|cert| cert.certificate_id == certificate_id)
|
||||
.ok_or("Certificate not found after submission")?;
|
||||
|
||||
let certificate = X509::from_der(&apple_cert.cert_content)
|
||||
.map_err(|e| format!("Failed to parse certificate: {}", e))?;
|
||||
|
||||
// Write certificate to disk
|
||||
let cert_pem = certificate
|
||||
.to_pem()
|
||||
.map_err(|e| format!("Failed to encode certificate to PEM: {}", e))?;
|
||||
fs::write(&self.cert_file, cert_pem)
|
||||
.map_err(|e| format!("Failed to save certificate file: {}", e))?;
|
||||
|
||||
self.certificate = Some(certificate);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_certificate_file_path(&self) -> &PathBuf {
|
||||
&self.cert_file
|
||||
}
|
||||
|
||||
pub fn get_private_key_file_path(&self) -> &PathBuf {
|
||||
&self.key_file
|
||||
}
|
||||
}
|
||||
698
src/developer_session.rs
Normal file
698
src/developer_session.rs
Normal file
@@ -0,0 +1,698 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference for the apple private endpoints
|
||||
|
||||
use crate::Error;
|
||||
use icloud_auth::{AppleAccount, Error as ICloudError};
|
||||
use plist::{Date, Dictionary, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct DeveloperSession {
|
||||
pub account: Arc<AppleAccount>,
|
||||
team: Option<DeveloperTeam>,
|
||||
}
|
||||
|
||||
impl DeveloperSession {
|
||||
pub fn new(account: Arc<AppleAccount>) -> Self {
|
||||
DeveloperSession {
|
||||
account,
|
||||
team: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_developer_request(
|
||||
&self,
|
||||
url: &str,
|
||||
body: Option<Dictionary>,
|
||||
) -> Result<Dictionary, Error> {
|
||||
let mut request = Dictionary::new();
|
||||
request.insert(
|
||||
"clientId".to_string(),
|
||||
Value::String("XABBG36SBA".to_string()),
|
||||
);
|
||||
request.insert(
|
||||
"protocolVersion".to_string(),
|
||||
Value::String("QH65B2".to_string()),
|
||||
);
|
||||
request.insert(
|
||||
"requestId".to_string(),
|
||||
Value::String(Uuid::new_v4().to_string().to_uppercase()),
|
||||
);
|
||||
request.insert(
|
||||
"userLocale".to_string(),
|
||||
Value::Array(vec![Value::String("en_US".to_string())]),
|
||||
);
|
||||
if let Some(body) = body {
|
||||
for (key, value) in body {
|
||||
request.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let response = self
|
||||
.account
|
||||
.send_request(url, Some(request))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if let ICloudError::AuthSrpWithMessage(code, message) = e {
|
||||
Error::DeveloperSession(code, format!("Developer request failed: {}", message))
|
||||
} else {
|
||||
Error::Generic
|
||||
}
|
||||
})?;
|
||||
|
||||
let status_code = response
|
||||
.get("resultCode")
|
||||
.and_then(|v| v.as_unsigned_integer())
|
||||
.unwrap_or(0);
|
||||
if status_code != 0 {
|
||||
let description = response
|
||||
.get("userString")
|
||||
.and_then(|v| v.as_string())
|
||||
.or_else(|| response.get("resultString").and_then(|v| v.as_string()))
|
||||
.unwrap_or("(null)");
|
||||
return Err(Error::DeveloperSession(
|
||||
status_code as i64,
|
||||
description.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn list_teams(&self) -> Result<Vec<DeveloperTeam>, Error> {
|
||||
let url = "https://developerservices2.apple.com/services/QH65B2/listTeams.action?clientId=XABBG36SBA";
|
||||
let response = self.send_developer_request(url, None).await?;
|
||||
|
||||
let teams = response
|
||||
.get("teams")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for team in teams {
|
||||
let dict = team.as_dictionary().ok_or(Error::Parse)?;
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let team_id = dict
|
||||
.get("teamId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
result.push(DeveloperTeam {
|
||||
_name: name,
|
||||
team_id,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_team(&self) -> Result<DeveloperTeam, Error> {
|
||||
if let Some(team) = &self.team {
|
||||
return Ok(team.clone());
|
||||
}
|
||||
let teams = self.list_teams().await?;
|
||||
if teams.is_empty() {
|
||||
return Err(Error::DeveloperSession(
|
||||
-1,
|
||||
"No developer teams found".to_string(),
|
||||
));
|
||||
}
|
||||
// TODO: Handle multiple teams
|
||||
Ok(teams[0].clone())
|
||||
}
|
||||
|
||||
pub fn set_team(&mut self, team: DeveloperTeam) {
|
||||
self.team = Some(team);
|
||||
}
|
||||
|
||||
pub async fn list_devices(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<Vec<DeveloperDevice>, Error> {
|
||||
let url = dev_url(device_type, "listDevices");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let devices = response
|
||||
.get("devices")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for device in devices {
|
||||
let dict = device.as_dictionary().ok_or(Error::Parse)?;
|
||||
let device_id = dict
|
||||
.get("deviceId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let device_number = dict
|
||||
.get("deviceNumber")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
result.push(DeveloperDevice {
|
||||
_device_id: device_id,
|
||||
_name: name,
|
||||
device_number,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn add_device(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
device_name: &str,
|
||||
udid: &str,
|
||||
) -> Result<DeveloperDevice, Error> {
|
||||
let url = dev_url(device_type, "addDevice");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("name".to_string(), Value::String(device_name.to_string()));
|
||||
body.insert("deviceNumber".to_string(), Value::String(udid.to_string()));
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let device_dict = response
|
||||
.get("device")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let device_id = device_dict
|
||||
.get("deviceId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let name = device_dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let device_number = device_dict
|
||||
.get("deviceNumber")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
|
||||
Ok(DeveloperDevice {
|
||||
_device_id: device_id,
|
||||
_name: name,
|
||||
device_number,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list_all_development_certs(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<Vec<DevelopmentCertificate>, Error> {
|
||||
let url = dev_url(device_type, "listAllDevelopmentCerts");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let certs = response
|
||||
.get("certificates")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for cert in certs {
|
||||
let dict = cert.as_dictionary().ok_or(Error::Parse)?;
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let certificate_id = dict
|
||||
.get("certificateId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let serial_number = dict
|
||||
.get("serialNumber")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let machine_name = dict
|
||||
.get("machineName")
|
||||
.and_then(|v| v.as_string())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let cert_content = dict
|
||||
.get("certContent")
|
||||
.and_then(|v| v.as_data())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_vec();
|
||||
|
||||
result.push(DevelopmentCertificate {
|
||||
name: name,
|
||||
certificate_id,
|
||||
serial_number: serial_number,
|
||||
machine_name: machine_name,
|
||||
cert_content,
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn revoke_development_cert(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
serial_number: &str,
|
||||
) -> Result<(), Error> {
|
||||
let url = dev_url(device_type, "revokeDevelopmentCert");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert(
|
||||
"serialNumber".to_string(),
|
||||
Value::String(serial_number.to_string()),
|
||||
);
|
||||
|
||||
self.send_developer_request(&url, Some(body)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn submit_development_csr(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
csr_content: String,
|
||||
) -> Result<String, Error> {
|
||||
let url = dev_url(device_type, "submitDevelopmentCSR");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("csrContent".to_string(), Value::String(csr_content));
|
||||
body.insert(
|
||||
"machineId".to_string(),
|
||||
Value::String(uuid::Uuid::new_v4().to_string().to_uppercase()),
|
||||
);
|
||||
body.insert(
|
||||
"machineName".to_string(),
|
||||
Value::String("YCode".to_string()),
|
||||
);
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
let cert_dict = response
|
||||
.get("certRequest")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let id = cert_dict
|
||||
.get("certRequestId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub async fn list_app_ids(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<ListAppIdsResponse, Error> {
|
||||
let url = dev_url(device_type, "listAppIds");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let app_ids = response
|
||||
.get("appIds")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for app_id in app_ids {
|
||||
let dict = app_id.as_dictionary().ok_or(Error::Parse)?;
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let app_id_id = dict
|
||||
.get("appIdId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let identifier = dict
|
||||
.get("identifier")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let features = dict
|
||||
.get("features")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let expiration_date = if dict.contains_key("expirationDate") {
|
||||
Some(
|
||||
dict.get("expirationDate")
|
||||
.and_then(|v| v.as_date())
|
||||
.ok_or(Error::Parse)?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
result.push(AppId {
|
||||
name,
|
||||
app_id_id,
|
||||
identifier,
|
||||
features: features.clone(),
|
||||
expiration_date,
|
||||
});
|
||||
}
|
||||
|
||||
let max_quantity = response
|
||||
.get("maxQuantity")
|
||||
.and_then(|v| v.as_unsigned_integer())
|
||||
.ok_or(Error::Parse)?;
|
||||
let available_quantity = response
|
||||
.get("availableQuantity")
|
||||
.and_then(|v| v.as_unsigned_integer())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
Ok(ListAppIdsResponse {
|
||||
app_ids: result,
|
||||
max_quantity,
|
||||
available_quantity,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn add_app_id(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
name: &str,
|
||||
identifier: &str,
|
||||
) -> Result<(), Error> {
|
||||
let url = dev_url(device_type, "addAppId");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("name".to_string(), Value::String(name.to_string()));
|
||||
body.insert(
|
||||
"identifier".to_string(),
|
||||
Value::String(identifier.to_string()),
|
||||
);
|
||||
|
||||
self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_app_id(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
app_id: &AppId,
|
||||
features: &Dictionary,
|
||||
) -> Result<Dictionary, Error> {
|
||||
let url = dev_url(device_type, "updateAppId");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert(
|
||||
"appIdId".to_string(),
|
||||
Value::String(app_id.app_id_id.clone()),
|
||||
);
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
|
||||
for (key, value) in features {
|
||||
body.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
let cert_dict = response
|
||||
.get("appId")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let feats = cert_dict
|
||||
.get("features")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
Ok(feats.clone())
|
||||
}
|
||||
|
||||
pub async fn delete_app_id(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
app_id_id: String,
|
||||
) -> Result<(), Error> {
|
||||
let url = dev_url(device_type, "deleteAppId");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("appIdId".to_string(), Value::String(app_id_id.clone()));
|
||||
|
||||
self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_application_groups(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
) -> Result<Vec<ApplicationGroup>, Error> {
|
||||
let url = dev_url(device_type, "listApplicationGroups");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let app_groups = response
|
||||
.get("applicationGroupList")
|
||||
.and_then(|v| v.as_array())
|
||||
.ok_or(Error::Parse)?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for app_group in app_groups {
|
||||
let dict = app_group.as_dictionary().ok_or(Error::Parse)?;
|
||||
let application_group = dict
|
||||
.get("applicationGroup")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let name = dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let identifier = dict
|
||||
.get("identifier")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
|
||||
result.push(ApplicationGroup {
|
||||
application_group,
|
||||
_name: name,
|
||||
identifier,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn add_application_group(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
group_identifier: &str,
|
||||
name: &str,
|
||||
) -> Result<ApplicationGroup, Error> {
|
||||
let url = dev_url(device_type, "addApplicationGroup");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert("name".to_string(), Value::String(name.to_string()));
|
||||
body.insert(
|
||||
"identifier".to_string(),
|
||||
Value::String(group_identifier.to_string()),
|
||||
);
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
let app_group_dict = response
|
||||
.get("applicationGroup")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let application_group = app_group_dict
|
||||
.get("applicationGroup")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let name = app_group_dict
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let identifier = app_group_dict
|
||||
.get("identifier")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
|
||||
Ok(ApplicationGroup {
|
||||
application_group,
|
||||
_name: name,
|
||||
identifier,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn assign_application_group_to_app_id(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
app_id: &AppId,
|
||||
app_group: &ApplicationGroup,
|
||||
) -> Result<(), Error> {
|
||||
let url = dev_url(device_type, "assignApplicationGroupToAppId");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert(
|
||||
"appIdId".to_string(),
|
||||
Value::String(app_id.app_id_id.clone()),
|
||||
);
|
||||
body.insert(
|
||||
"applicationGroups".to_string(),
|
||||
Value::String(app_group.application_group.clone()),
|
||||
);
|
||||
|
||||
self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_team_provisioning_profile(
|
||||
&self,
|
||||
device_type: DeveloperDeviceType,
|
||||
team: &DeveloperTeam,
|
||||
app_id: &AppId,
|
||||
) -> Result<ProvisioningProfile, Error> {
|
||||
let url = dev_url(device_type, "downloadTeamProvisioningProfile");
|
||||
let mut body = Dictionary::new();
|
||||
body.insert("teamId".to_string(), Value::String(team.team_id.clone()));
|
||||
body.insert(
|
||||
"appIdId".to_string(),
|
||||
Value::String(app_id.app_id_id.clone()),
|
||||
);
|
||||
|
||||
let response = self.send_developer_request(&url, Some(body)).await?;
|
||||
|
||||
let profile = response
|
||||
.get("provisioningProfile")
|
||||
.and_then(|v| v.as_dictionary())
|
||||
.ok_or(Error::Parse)?;
|
||||
let name = profile
|
||||
.get("name")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let provisioning_profile_id = profile
|
||||
.get("provisioningProfileId")
|
||||
.and_then(|v| v.as_string())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_string();
|
||||
let encoded_profile = profile
|
||||
.get("encodedProfile")
|
||||
.and_then(|v| v.as_data())
|
||||
.ok_or(Error::Parse)?
|
||||
.to_vec();
|
||||
|
||||
Ok(ProvisioningProfile {
|
||||
_name: name,
|
||||
_provisioning_profile_id: provisioning_profile_id,
|
||||
encoded_profile,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DeveloperDeviceType {
|
||||
Any,
|
||||
Ios,
|
||||
Tvos,
|
||||
Watchos,
|
||||
}
|
||||
|
||||
impl DeveloperDeviceType {
|
||||
pub fn url_segment(&self) -> &'static str {
|
||||
match self {
|
||||
DeveloperDeviceType::Any => "",
|
||||
DeveloperDeviceType::Ios => "ios/",
|
||||
DeveloperDeviceType::Tvos => "tvos/",
|
||||
DeveloperDeviceType::Watchos => "watchos/",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dev_url(device_type: DeveloperDeviceType, endpoint: &str) -> String {
|
||||
format!(
|
||||
"https://developerservices2.apple.com/services/QH65B2/{}{}.action?clientId=XABBG36SBA",
|
||||
device_type.url_segment(),
|
||||
endpoint
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeveloperDevice {
|
||||
pub _device_id: String,
|
||||
pub _name: String,
|
||||
pub device_number: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeveloperTeam {
|
||||
pub _name: String,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DevelopmentCertificate {
|
||||
pub name: String,
|
||||
pub certificate_id: String,
|
||||
pub serial_number: String,
|
||||
pub machine_name: String,
|
||||
pub cert_content: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppId {
|
||||
pub app_id_id: String,
|
||||
pub identifier: String,
|
||||
pub name: String,
|
||||
pub features: Dictionary,
|
||||
pub expiration_date: Option<Date>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListAppIdsResponse {
|
||||
pub app_ids: Vec<AppId>,
|
||||
pub max_quantity: u64,
|
||||
pub available_quantity: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApplicationGroup {
|
||||
pub application_group: String,
|
||||
pub _name: String,
|
||||
pub identifier: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProvisioningProfile {
|
||||
pub _provisioning_profile_id: String,
|
||||
pub _name: String,
|
||||
pub encoded_profile: Vec<u8>,
|
||||
}
|
||||
179
src/device.rs
Normal file
179
src/device.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use idevice::{
|
||||
IdeviceService,
|
||||
afc::AfcClient,
|
||||
installation_proxy::InstallationProxyClient,
|
||||
lockdown::LockdownClient,
|
||||
usbmuxd::{UsbmuxdAddr, UsbmuxdConnection},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct DeviceInfo {
|
||||
pub name: String,
|
||||
pub id: u32,
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
pub async fn list_devices() -> Result<Vec<DeviceInfo>, String> {
|
||||
let usbmuxd = UsbmuxdConnection::default().await;
|
||||
if usbmuxd.is_err() {
|
||||
eprintln!("Failed to connect to usbmuxd: {:?}", usbmuxd.err());
|
||||
return Err("Failed to connect to usbmuxd".to_string());
|
||||
}
|
||||
let mut usbmuxd = usbmuxd.unwrap();
|
||||
|
||||
let devs = usbmuxd.get_devices().await.unwrap();
|
||||
if devs.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let device_info_futures: Vec<_> = devs
|
||||
.iter()
|
||||
.map(|d| async move {
|
||||
let provider = d.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "y-code");
|
||||
let device_uid = d.device_id;
|
||||
|
||||
let mut lockdown_client = match LockdownClient::connect(&provider).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to connect to lockdown: {e:?}");
|
||||
return DeviceInfo {
|
||||
name: String::from("Unknown Device"),
|
||||
id: device_uid,
|
||||
uuid: d.udid.clone(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let device_name = lockdown_client
|
||||
.get_value("DeviceName", None)
|
||||
.await
|
||||
.expect("Failed to get device name")
|
||||
.as_string()
|
||||
.expect("Failed to convert device name to string")
|
||||
.to_string();
|
||||
|
||||
DeviceInfo {
|
||||
name: device_name,
|
||||
id: device_uid,
|
||||
uuid: d.udid.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(futures::future::join_all(device_info_futures).await)
|
||||
}
|
||||
|
||||
pub async fn install_app(
|
||||
device: &DeviceInfo,
|
||||
app_path: &PathBuf,
|
||||
callback: impl Fn(u64) -> (),
|
||||
) -> Result<(), String> {
|
||||
let mut usbmuxd = UsbmuxdConnection::default()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to usbmuxd: {:?}", e))?;
|
||||
let device = usbmuxd
|
||||
.get_device(&device.uuid)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get device: {:?}", e))?;
|
||||
|
||||
let provider = device.to_provider(UsbmuxdAddr::from_env_var().unwrap(), "y-code");
|
||||
|
||||
let mut afc_client = AfcClient::connect(&provider)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to AFC: {:?}", e))?;
|
||||
|
||||
let dir = format!(
|
||||
"PublicStaging/{}",
|
||||
app_path.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
afc_upload_dir(&mut afc_client, app_path, &dir)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to upload directory: {:?}", e))?;
|
||||
|
||||
let mut instproxy_client = InstallationProxyClient::connect(&provider)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to connect to installation proxy: {:?}", e))?;
|
||||
|
||||
let mut options = plist::Dictionary::new();
|
||||
options.insert("PackageType".to_string(), "Developer".into());
|
||||
instproxy_client
|
||||
.install_with_callback(
|
||||
dir,
|
||||
Some(plist::Value::Dictionary(options)),
|
||||
async |(percentage, _)| {
|
||||
callback(percentage);
|
||||
},
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to install app: {:?}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn afc_upload_dir<'a>(
|
||||
afc_client: &'a mut AfcClient,
|
||||
path: &'a PathBuf,
|
||||
afc_path: &'a str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let entries =
|
||||
std::fs::read_dir(path).map_err(|e| format!("Failed to read directory: {}", e))?;
|
||||
afc_client
|
||||
.mk_dir(afc_path)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let new_afc_path = format!(
|
||||
"{}/{}",
|
||||
afc_path,
|
||||
path.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
afc_upload_dir(afc_client, &path, &new_afc_path).await?;
|
||||
} else {
|
||||
let mut file_handle = afc_client
|
||||
.open(
|
||||
format!(
|
||||
"{}/{}",
|
||||
afc_path,
|
||||
path.file_name().unwrap().to_string_lossy()
|
||||
),
|
||||
idevice::afc::opcode::AfcFopenMode::WrOnly,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||||
let bytes =
|
||||
std::fs::read(&path).map_err(|e| format!("Failed to read file: {}", e))?;
|
||||
file_handle
|
||||
.write(&bytes)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to write file: {}", e))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn refresh_idevice(window: tauri::Window) {
|
||||
match list_devices().await {
|
||||
Ok(devices) => {
|
||||
window
|
||||
.emit("idevices", devices)
|
||||
.expect("Failed to send devices");
|
||||
}
|
||||
Err(e) => {
|
||||
window
|
||||
.emit("idevices", Vec::<DeviceInfo>::new())
|
||||
.expect("Failed to send error");
|
||||
eprintln!("Failed to list devices: {}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
21
src/lib.rs
Normal file
21
src/lib.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
pub mod application;
|
||||
pub mod bundle;
|
||||
pub mod certificate;
|
||||
pub mod developer_session;
|
||||
pub mod device;
|
||||
pub mod sideload;
|
||||
|
||||
pub use developer_session::{
|
||||
AppId, ApplicationGroup, DeveloperDevice, DeveloperDeviceType, DeveloperSession, DeveloperTeam,
|
||||
DevelopmentCertificate, ListAppIdsResponse, ProvisioningProfile,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Auth(i64, String),
|
||||
DeveloperSession(i64, String),
|
||||
Generic,
|
||||
Parse,
|
||||
InvalidBundle(String),
|
||||
Certificate(String),
|
||||
}
|
||||
400
src/sideload.rs
Normal file
400
src/sideload.rs
Normal file
@@ -0,0 +1,400 @@
|
||||
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
|
||||
|
||||
use crate::Error;
|
||||
use crate::{
|
||||
device::{DeviceInfo, install_app},
|
||||
sideloader::{
|
||||
certificate::CertificateIdentity, developer_session::DeveloperDeviceType,
|
||||
},
|
||||
};
|
||||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
pub async fn sideload_app(
|
||||
handle: &tauri::AppHandle,
|
||||
window: &tauri::Window,
|
||||
anisette_server: String,
|
||||
device: DeviceInfo,
|
||||
app_path: PathBuf,
|
||||
) -> Result<(), Error> {
|
||||
if device.uuid.is_empty() {
|
||||
return emit_error_and_return(window, "No device selected");
|
||||
}
|
||||
let dev_session = match crate::sideloader::apple::get_developer_session(
|
||||
&handle,
|
||||
window,
|
||||
anisette_server.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(acc) => acc,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!("Failed to login to Apple account: {:?}", e),
|
||||
);
|
||||
}
|
||||
};
|
||||
let team = match dev_session.get_team().await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to get team: {:?}", e));
|
||||
}
|
||||
};
|
||||
window
|
||||
.emit("build-output", "Successfully retrieved team".to_string())
|
||||
.ok();
|
||||
ensure_device_registered(&dev_session, window, &team, &device).await?;
|
||||
|
||||
let config_dir = handle.path().app_config_dir().map_err(|e| e.to_string())?;
|
||||
let cert = match CertificateIdentity::new(config_dir, &dev_session, get_apple_email()).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to get certificate: {:?}", e));
|
||||
}
|
||||
};
|
||||
window
|
||||
.emit(
|
||||
"build-output",
|
||||
"Certificate acquired succesfully".to_string(),
|
||||
)
|
||||
.ok();
|
||||
let mut list_app_id_response = match dev_session
|
||||
.list_app_ids(DeveloperDeviceType::Ios, &team)
|
||||
.await
|
||||
{
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to list app IDs: {:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let mut app = crate::sideloader::application::Application::new(app_path);
|
||||
let is_sidestore = app.bundle.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore";
|
||||
let main_app_bundle_id = match app.bundle.bundle_identifier() {
|
||||
Some(id) => id.to_string(),
|
||||
None => {
|
||||
return emit_error_and_return(window, "No bundle identifier found in IPA");
|
||||
}
|
||||
};
|
||||
let main_app_id_str = format!("{}.{}", main_app_bundle_id, team.team_id);
|
||||
let main_app_name = match app.bundle.bundle_name() {
|
||||
Some(name) => name.to_string(),
|
||||
None => {
|
||||
return emit_error_and_return(window, "No bundle name found in IPA");
|
||||
}
|
||||
};
|
||||
|
||||
let extensions = app.bundle.app_extensions_mut();
|
||||
// for each extension, ensure it has a unique bundle identifier that starts with the main app's bundle identifier
|
||||
for ext in extensions.iter_mut() {
|
||||
if let Some(id) = ext.bundle_identifier() {
|
||||
if !(id.starts_with(&main_app_bundle_id) && id.len() > main_app_bundle_id.len()) {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!(
|
||||
"Extension {} is not part of the main app bundle identifier: {}",
|
||||
ext.bundle_name().unwrap_or("Unknown"),
|
||||
id
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ext.set_bundle_identifier(&format!(
|
||||
"{}{}",
|
||||
main_app_id_str,
|
||||
&id[main_app_bundle_id.len()..]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
app.bundle.set_bundle_identifier(&main_app_id_str);
|
||||
|
||||
let extension_refs: Vec<_> = app.bundle.app_extensions().into_iter().collect();
|
||||
let mut bundles_with_app_id = vec![&app.bundle];
|
||||
bundles_with_app_id.extend(extension_refs);
|
||||
|
||||
let app_ids_to_register = bundles_with_app_id
|
||||
.iter()
|
||||
.filter(|bundle| {
|
||||
let bundle_id = bundle.bundle_identifier().unwrap_or("");
|
||||
!list_app_id_response
|
||||
.app_ids
|
||||
.iter()
|
||||
.any(|app_id| app_id.identifier == bundle_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if app_ids_to_register.len() > list_app_id_response.available_quantity.try_into().unwrap() {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!(
|
||||
"This app requires {} app ids, but you only have {} available",
|
||||
app_ids_to_register.len(),
|
||||
list_app_id_response.available_quantity
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for bundle in app_ids_to_register {
|
||||
let id = bundle.bundle_identifier().unwrap_or("");
|
||||
let name = bundle.bundle_name().unwrap_or("");
|
||||
if let Err(e) = dev_session
|
||||
.add_app_id(DeveloperDeviceType::Ios, &team, &name, &id)
|
||||
.await
|
||||
{
|
||||
return emit_error_and_return(window, &format!("Failed to register app ID: {:?}", e));
|
||||
}
|
||||
}
|
||||
list_app_id_response = match dev_session
|
||||
.list_app_ids(DeveloperDeviceType::Ios, &team)
|
||||
.await
|
||||
{
|
||||
Ok(ids) => ids,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to list app IDs: {:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let mut app_ids: Vec<_> = list_app_id_response
|
||||
.app_ids
|
||||
.into_iter()
|
||||
.filter(|app_id| {
|
||||
bundles_with_app_id
|
||||
.iter()
|
||||
.any(|bundle| app_id.identifier == bundle.bundle_identifier().unwrap_or(""))
|
||||
})
|
||||
.collect();
|
||||
let main_app_id = match app_ids
|
||||
.iter()
|
||||
.find(|app_id| app_id.identifier == main_app_id_str)
|
||||
.cloned()
|
||||
{
|
||||
Some(id) => id,
|
||||
None => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!(
|
||||
"Main app ID {} not found in registered app IDs",
|
||||
main_app_id_str
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window
|
||||
.emit("build-output", "Registered app IDs".to_string())
|
||||
.ok();
|
||||
|
||||
for app_id in app_ids.iter_mut() {
|
||||
let app_group_feature_enabled = app_id
|
||||
.features
|
||||
.get(
|
||||
"APG3427HIY", /* Gotta love apple and their magic strings! */
|
||||
)
|
||||
.and_then(|v| v.as_boolean())
|
||||
.ok_or("App group feature not found in app id")?;
|
||||
if !app_group_feature_enabled {
|
||||
let mut body = plist::Dictionary::new();
|
||||
body.insert("APG3427HIY".to_string(), plist::Value::Boolean(true));
|
||||
let new_features = match dev_session
|
||||
.update_app_id(DeveloperDeviceType::Ios, &team, &app_id, &body)
|
||||
.await
|
||||
{
|
||||
Ok(new_feats) => new_feats,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!("Failed to update app ID features: {:?}", e),
|
||||
);
|
||||
}
|
||||
};
|
||||
app_id.features = new_features;
|
||||
}
|
||||
}
|
||||
|
||||
let group_identifier = format!("group.{}", main_app_id_str);
|
||||
|
||||
if is_sidestore {
|
||||
app.bundle.app_info.insert(
|
||||
"ALTAppGroups".to_string(),
|
||||
plist::Value::Array(vec![plist::Value::String(group_identifier.clone())]),
|
||||
);
|
||||
}
|
||||
|
||||
let app_groups = match dev_session
|
||||
.list_application_groups(DeveloperDeviceType::Ios, &team)
|
||||
.await
|
||||
{
|
||||
Ok(groups) => groups,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(window, &format!("Failed to list app groups: {:?}", e));
|
||||
}
|
||||
};
|
||||
|
||||
let matching_app_groups = app_groups
|
||||
.iter()
|
||||
.filter(|group| group.identifier == group_identifier.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let app_group = if matching_app_groups.is_empty() {
|
||||
match dev_session
|
||||
.add_application_group(
|
||||
DeveloperDeviceType::Ios,
|
||||
&team,
|
||||
&group_identifier,
|
||||
&main_app_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(group) => group,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!("Failed to register app group: {:?}", e),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
matching_app_groups[0].clone()
|
||||
};
|
||||
|
||||
//let mut provisioning_profiles: HashMap<String, ProvisioningProfile> = HashMap::new();
|
||||
for app_id in app_ids {
|
||||
let assign_res = dev_session
|
||||
.assign_application_group_to_app_id(
|
||||
DeveloperDeviceType::Ios,
|
||||
&team,
|
||||
&app_id,
|
||||
&app_group,
|
||||
)
|
||||
.await;
|
||||
if assign_res.is_err() {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!(
|
||||
"Failed to assign app group to app ID: {:?}",
|
||||
assign_res.err()
|
||||
),
|
||||
);
|
||||
}
|
||||
// let provisioning_profile = match account
|
||||
// // This doesn't seem right to me, but it's what Sideloader does... Shouldn't it be downloading the provisioning profile for this app ID, not the main?
|
||||
// .download_team_provisioning_profile(DeveloperDeviceType::Ios, &team, &main_app_id)
|
||||
// .await
|
||||
// {
|
||||
// Ok(pp /* tee hee */) => pp,
|
||||
// Err(e) => {
|
||||
// return emit_error_and_return(
|
||||
// &window,
|
||||
// &format!("Failed to download provisioning profile: {:?}", e),
|
||||
// );
|
||||
// }
|
||||
// };
|
||||
// provisioning_profiles.insert(app_id.identifier.clone(), provisioning_profile);
|
||||
}
|
||||
|
||||
window
|
||||
.emit("build-output", "Registered app groups".to_string())
|
||||
.ok();
|
||||
|
||||
let provisioning_profile = match dev_session
|
||||
.download_team_provisioning_profile(DeveloperDeviceType::Ios, &team, &main_app_id)
|
||||
.await
|
||||
{
|
||||
Ok(pp /* tee hee */) => pp,
|
||||
Err(e) => {
|
||||
return emit_error_and_return(
|
||||
window,
|
||||
&format!("Failed to download provisioning profile: {:?}", e),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let profile_path = handle
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.map_err(|e| e.to_string())?
|
||||
.join(format!("{}.mobileprovision", main_app_id_str));
|
||||
|
||||
if profile_path.exists() {
|
||||
std::fs::remove_file(&profile_path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(&profile_path).map_err(|e| e.to_string())?;
|
||||
file.write_all(&provisioning_profile.encoded_profile)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Without this, zsign complains it can't find the provision file
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
file.sync_all().map_err(|e| e.to_string())?;
|
||||
drop(file);
|
||||
}
|
||||
|
||||
// TODO: Recursive for sub-bundles?
|
||||
app.bundle.write_info().map_err(|e| e.to_string())?;
|
||||
|
||||
window
|
||||
.emit("build-output", "Signining app...".to_string())
|
||||
.ok();
|
||||
|
||||
let zsign_command = handle.shell().sidecar("zsign").unwrap().args([
|
||||
"-k",
|
||||
cert.get_private_key_file_path().to_str().unwrap(),
|
||||
"-c",
|
||||
cert.get_certificate_file_path().to_str().unwrap(),
|
||||
"-m",
|
||||
profile_path.to_str().unwrap(),
|
||||
app.bundle.bundle_dir.to_str().unwrap(),
|
||||
]);
|
||||
let (mut rx, mut _child) = zsign_command.spawn().expect("Failed to spawn zsign");
|
||||
|
||||
let mut signing_failed = false;
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line_bytes) | CommandEvent::Stderr(line_bytes) => {
|
||||
let line = String::from_utf8_lossy(&line_bytes);
|
||||
window
|
||||
.emit("build-output", Some(line))
|
||||
.expect("failed to emit event");
|
||||
}
|
||||
CommandEvent::Terminated(result) => {
|
||||
if result.code != Some(0) {
|
||||
window
|
||||
.emit("build-output", "App signing failed!".to_string())
|
||||
.ok();
|
||||
signing_failed = true;
|
||||
break;
|
||||
}
|
||||
window.emit("build-output", "App signed!").ok();
|
||||
|
||||
window
|
||||
.emit(
|
||||
"build-output",
|
||||
"Installing app (Transfer)... 0%".to_string(),
|
||||
)
|
||||
.ok();
|
||||
|
||||
let res = install_app(&device, &app.bundle.bundle_dir, |percentage| {
|
||||
window
|
||||
.emit("build-output", format!("Installing app... {}%", percentage))
|
||||
.expect("failed to emit event");
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
window
|
||||
.emit("build-output", format!("Failed to install app: {:?}", e))
|
||||
.ok();
|
||||
signing_failed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if signing_failed {
|
||||
return Err("Signing or installation failed".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user